#include #include "icsneo/api/eventmanager.h" #include "icsneo/communication/message/filter/main51messagefilter.h" #include "icsneo/communication/message/extendedresponsemessage.h" #include "icsneo/device/device.h" #include "icsneo/device/extensions/deviceextension.h" #include "icsneo/disk/fat.h" #ifdef _MSC_VER #pragma warning(disable : 4996) // STL time functions #endif using namespace icsneo; struct RTCCTIME { uint8_t FracSec;// --- fractions of seconds (00-99). Note that you can write only 0x00 here! uint8_t Sec;// --- Seconds (00-59) uint8_t Min;// --- (00-59) uint8_t Hour;// --- (00-23) uint8_t DOW;// --- (01-07) uint8_t Day;// --- (01-31) uint8_t Month;// --- (01-12) uint8_t Year;// --- (00-99) }; static const uint8_t fromBase36Table[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35 }; static const char toBase36Table[36] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }; static const uint32_t toBase36Powers[7] = { 1, 36, 1296, 46656, 1679616, 60466176, 2176782336 }; #define MIN_BASE36_SERIAL (16796160) #define MAX_SERIAL (2176782335) std::string Device::SerialNumToString(uint32_t serial) { if(serial == 0 || serial > MAX_SERIAL) return "0"; std::stringstream ss; if(serial >= MIN_BASE36_SERIAL) { for (auto i = 5; i >= 0; i--) { ss << toBase36Table[serial / toBase36Powers[i]]; serial = serial % toBase36Powers[i]; } } else { ss << serial; } return ss.str(); } uint32_t Device::SerialStringToNum(const std::string& serial) { if(Device::SerialStringIsNumeric(serial)) { try { return std::stoi(serial); } catch(...) { return 0; } } if(serial.length() != 6) return 0; // Non-numeric serial numbers should be 6 characters uint32_t ret = 0; for (auto i = 0; i < 6; i++) { ret *= 36; ret += fromBase36Table[(unsigned char)serial[i]]; } return ret; } bool Device::SerialStringIsNumeric(const std::string& serial) { if(serial.length() == 0) return false; if(serial.length() == 1) return isdigit(serial[0]); // Check the first two characters, at least one should be a character if we need to do a base36 conversion return isdigit(serial[0]) && isdigit(serial[1]); } Device::~Device() { if(isMessagePollingEnabled()) disableMessagePolling(); if(isOpen()) close(); } uint16_t Device::getTimestampResolution() const { return com->decoder->timestampResolution; } std::string Device::describe() const { std::stringstream ss; ss << getProductName() << ' ' << getSerial(); return ss.str(); } bool Device::enableMessagePolling() { if(isMessagePollingEnabled()) {// We are already polling report(APIEvent::Type::DeviceCurrentlyPolling, APIEvent::Severity::Error); return false; } messagePollingCallbackID = com->addMessageCallback(std::make_shared([this](std::shared_ptr message) { pollingContainer.enqueue(message); enforcePollingMessageLimit(); })); return true; } bool Device::disableMessagePolling() { if(!isMessagePollingEnabled()) { report(APIEvent::Type::DeviceNotCurrentlyPolling, APIEvent::Severity::Error); return false; // Not currently polling } auto ret = com->removeMessageCallback(messagePollingCallbackID); getMessages(); // Flush any messages still in the container messagePollingCallbackID = 0; return ret; } // Returns a pair of {vector, bool}, where the vector contains shared_ptrs to the returned msgs and the bool is whether or not the call was successful. std::pair>, bool> Device::getMessages() { std::vector> ret; bool retBool = getMessages(ret); return std::make_pair(ret, retBool); } bool Device::getMessages(std::vector>& container, size_t limit, std::chrono::milliseconds timeout) { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return false; } if(!isOnline()) { report(APIEvent::Type::DeviceCurrentlyOffline, APIEvent::Severity::Error); return false; } if(!isMessagePollingEnabled()) { report(APIEvent::Type::DeviceNotCurrentlyPolling, APIEvent::Severity::Error); return false; } // A limit of zero indicates no limit if(limit == 0) limit = (size_t)-1; if(limit > (pollingContainer.size_approx() + 4)) limit = (pollingContainer.size_approx() + 4); if(container.size() < limit) container.resize(limit); size_t actuallyRead; if(timeout != std::chrono::milliseconds(0)) actuallyRead = pollingContainer.wait_dequeue_bulk_timed(container.data(), limit, timeout); else actuallyRead = pollingContainer.try_dequeue_bulk(container.data(), limit); if(container.size() > actuallyRead) container.resize(actuallyRead); return true; } void Device::enforcePollingMessageLimit() { while(pollingContainer.size_approx() > pollingMessageLimit) { std::shared_ptr throwAway; pollingContainer.try_dequeue(throwAway); report(APIEvent::Type::PollingMessageOverflow, APIEvent::Severity::EventWarning); } } bool Device::open(OpenFlags flags, OpenStatusHandler handler) { if(!com) { report(APIEvent::Type::Unknown, APIEvent::Severity::Error); return false; } if(!com->open()) { return false; } APIEvent::Type attemptErr = attemptToBeginCommunication(); if(attemptErr != APIEvent::Type::NoErrorFound) { // We could not communicate with the device, let's see if an extension can bool tryAgain = false; forEachExtension([&tryAgain, &flags, &handler](const std::shared_ptr& ext) -> bool { if(ext->onDeviceCommunicationDead(flags, handler)) tryAgain = true; return true; }); if(!tryAgain) { com->close(); report(attemptErr, APIEvent::Severity::Error); return false; // Extensions couldn't save us } attemptErr = attemptToBeginCommunication(); if(attemptErr != APIEvent::Type::NoErrorFound) { com->close(); report(attemptErr, APIEvent::Severity::Error); return false; } } bool block = false; forEachExtension([&block, &flags, &handler](const std::shared_ptr& ext) { if(ext->onDeviceOpen(flags, handler)) return true; block = true; return false; }); if(block) // Extensions say no return false; // Get component versions *after* the extension "onDeviceOpen" hooks (e.g. device reflashes) if(supportsComponentVersions()) { if(auto compVersions = com->getComponentVersionsSync()) componentVersions = std::move(*compVersions); else // It's possible the device is on older firmware so don't return false here report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::EventWarning); } if(!settings->disabled) { // Since we will not fail the open if a settings read fails, // downgrade any errors to warnings. Otherwise the error will // go unnoticed in the opening thread's getLastError buffer. const bool downgrading = EventManager::GetInstance().isDowngradingErrorsOnCurrentThread(); if(!downgrading) EventManager::GetInstance().downgradeErrorsOnCurrentThread(); settings->refresh(); if(!downgrading) EventManager::GetInstance().cancelErrorDowngradingOnCurrentThread(); } MessageFilter filter; filter.includeInternalInAny = true; internalHandlerCallbackID = com->addMessageCallback(std::make_shared(filter, [this](std::shared_ptr message) { handleInternalMessage(message); })); heartbeatThread = std::thread([this]() { EventManager::GetInstance().downgradeErrorsOnCurrentThread(); MessageFilter filter; filter.includeInternalInAny = true; std::condition_variable heartbeatCV; std::mutex receivedMessageMutex; bool receivedMessage = false; auto messageReceivedCallbackID = com->addMessageCallback(std::make_shared(filter, [&](std::shared_ptr message) { { std::scoped_lock lk(receivedMessageMutex); receivedMessage = true; } heartbeatCV.notify_all(); })); // Give the device time to get situated auto i = 150; while(!stopHeartbeatThread && i != 0) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); i--; } while(!stopHeartbeatThread) { // Wait for 110ms for a possible heartbeat std::this_thread::sleep_for(std::chrono::milliseconds(110)); std::unique_lock recvLk(receivedMessageMutex); if(receivedMessage) { receivedMessage = false; } else { // Some communication, such as the bootloader and extractor interfaces, must // redirect the input stream from the device as it will no longer be in the // packet format we expect here. As a result, status updates will not reach // us here and suppressDisconnects() must be used. We don't want to request // a status and then redirect the stream, as we'll then be polluting an // otherwise quiet stream. This lock makes sure suppressDisconnects() will // block until we've either gotten our status update or disconnected from // the device. std::lock_guard lk(heartbeatMutex); if(heartbeatSuppressed()) continue; // No heartbeat received, request a status com->sendCommand(Command::RequestStatusUpdate); // Check if we got a message, and if not, if settings are being applied if(heartbeatCV.wait_for(recvLk, std::chrono::milliseconds(3500), [&](){ return receivedMessage; })) { receivedMessage = false; } else { if(!stopHeartbeatThread && !isDisconnected()) { report(APIEvent::Type::DeviceDisconnected, APIEvent::Severity::Error); com->driver->close(); } break; } } } com->removeMessageCallback(messageReceivedCallbackID); }); if(supportsLiveData()) clearAllLiveData(); return true; } APIEvent::Type Device::attemptToBeginCommunication() { versions.clear(); if(!afterCommunicationOpen()) { // Very unlikely, at the time of writing this only fails if rawWrite does. // If you're looking for this error, you're probably looking for if(!serial) below. // "Communication could not be established with the device. Perhaps it is not powered with 12 volts?" return getCommunicationNotEstablishedError(); } if(!com->sendCommand(Command::EnableNetworkCommunication, false)) return getCommunicationNotEstablishedError(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); auto serial = com->getSerialNumberSync(); int i = 0; while(!serial) { serial = com->getSerialNumberSync(); if(i++ > 5) break; } if(!serial) // "Communication could not be established with the device. Perhaps it is not powered with 12 volts?" return getCommunicationNotEstablishedError(); std::string currentSerial = getNeoDevice().serial; if(currentSerial != serial->deviceSerial) return APIEvent::Type::IncorrectSerialNumber; auto maybeVersions = com->getVersionsSync(); if(!maybeVersions) return getCommunicationNotEstablishedError(); else versions = std::move(*maybeVersions); return APIEvent::Type::NoErrorFound; } bool Device::close() { if(!com) { report(APIEvent::Type::Unknown, APIEvent::Severity::Error); return false; } stopHeartbeatThread = true; if (isMessagePollingEnabled()) { disableMessagePolling(); } if(isOnline()) goOffline(); if(internalHandlerCallbackID) com->removeMessageCallback(internalHandlerCallbackID); internalHandlerCallbackID = 0; if(heartbeatThread.joinable()) heartbeatThread.join(); stopHeartbeatThread = false; forEachExtension([](const std::shared_ptr& ext) { ext->onDeviceClose(); return true; }); return com->close(); } bool Device::goOnline() { if(!com->sendCommand(Command::EnableNetworkCommunication, true)) return false; auto startTime = std::chrono::system_clock::now(); ledState = LEDState::Online; updateLEDState(); std::shared_ptr filter = std::make_shared(Network::NetID::Reset_Status); filter->includeInternalInAny = true; // Wait until communication is enabled or 5 seconds, whichever comes first while((std::chrono::system_clock::now() - startTime) < std::chrono::seconds(5)) { if(latestResetStatus && latestResetStatus->comEnabled) break; bool failOut = false; com->waitForMessageSync([this, &failOut]() { if(!com->sendCommand(Command::RequestStatusUpdate)) { failOut = true; return false; } return true; }, filter, std::chrono::milliseconds(100)); if(failOut) return false; } online = true; forEachExtension([](const std::shared_ptr& ext) { ext->onGoOnline(); return true; }); return true; } bool Device::goOffline() { forEachExtension([](const std::shared_ptr& ext) { ext->onGoOffline(); return true; }); if(isDisconnected()) { online = false; return true; } if(!com->sendCommand(Command::EnableNetworkCommunication, false)) return false; auto startTime = std::chrono::system_clock::now(); ledState = (latestResetStatus && latestResetStatus->cmRunning) ? LEDState::CoreMiniRunning : LEDState::Offline; updateLEDState(); std::shared_ptr filter = std::make_shared(Network::NetID::Reset_Status); filter->includeInternalInAny = true; // Wait until communication is disabled or 5 seconds, whichever comes first while((std::chrono::system_clock::now() - startTime) < std::chrono::seconds(5)) { if(latestResetStatus && !latestResetStatus->comEnabled) break; if(!com->sendCommand(Command::RequestStatusUpdate)) return false; com->waitForMessageSync(filter, std::chrono::milliseconds(100)); } online = false; return true; } int8_t Device::prepareScriptLoad() { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return false; } static std::shared_ptr filter = std::make_shared(Network::NetID::CoreMiniPreLoad); if(!com->sendCommand(Command::CoreMiniPreload)) return false; int8_t retVal = 0; while(retVal == 0) { auto generic = com->waitForMessageSync(filter, std::chrono::milliseconds(1000)); if(!generic) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return false; } const auto resp = std::static_pointer_cast(generic); retVal = (int8_t)resp->data[0]; } return retVal; } bool Device::startScript(Disk::MemoryType memType) { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return false; } uint8_t location = static_cast(memType); std::shared_ptr filter = std::make_shared(Network::NetID::Device); filter->includeInternalInAny = true; const auto response = com->waitForMessageSync([&]() { return com->sendCommand(Command::LoadCoreMini, location); }, filter, std::chrono::milliseconds(2000)); if(!response) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return false; } return true; } bool Device::stopScript() { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return false; } std::shared_ptr filter = std::make_shared(Network::NetID::Device); filter->includeInternalInAny = true; const auto response = com->waitForMessageSync([&]() { return com->sendCommand(Command::ClearCoreMini); }, filter); if(!response) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return false; } return true; } bool Device::uploadCoremini(std::istream& stream, Disk::MemoryType memType) { if(stream.bad()) { report(APIEvent::Type::RequiredParameterNull, APIEvent::Severity::Error); return false; } std::vector bin(std::istreambuf_iterator(stream), {}); // Read the whole stream if(bin.size() < 4) { report(APIEvent::Type::BufferInsufficient, APIEvent::Severity::Error); return false; } uint16_t scriptVersion = *(uint16_t*)(&bin[2]); // Third and fourth byte are version number stored in little endian auto scriptStatus = getScriptStatus(); if(!scriptStatus) { return false; // Already added an API error } if(scriptStatus->coreminiVersion != scriptVersion) { // Version on device and script are not the same report(APIEvent::Type::CoreminiUploadVersionMismatch, APIEvent::Severity::Error); return false; } auto startAddress = getCoreminiStartAddress(memType); if(!startAddress) { return false; } auto connected = isLogicalDiskConnected(); if(!connected) { return false; // Already added an API error } if(!(*connected)) { report(APIEvent::Type::DiskNotConnected, APIEvent::Severity::Error); return false; } if(!stopScript()) { return false; } if(!clearScript(memType)) { return false; } if(!eraseScriptMemory(memType, static_cast(bin.size()))) { return false; } auto numWritten = writeLogicalDisk(*startAddress, (uint8_t*)bin.data(), static_cast(bin.size()), std::chrono::milliseconds(2000), memType); if(!numWritten) { return false; // Already added an API error } if(*numWritten != static_cast(bin.size())) { report(APIEvent::Type::FailedToWrite, APIEvent::Severity::Error); return false; // Failed to write } return true; } bool Device::eraseScriptMemory(Disk::MemoryType memType, uint64_t amount) { static std::shared_ptr NeoEraseDone = std::make_shared(Network::NetID::NeoMemoryWriteDone); if(!supportsEraseMemory()) { return true; } auto startAddress = getCoreminiStartAddress(memType); if(!startAddress) { return false; } if(memType != Disk::MemoryType::Flash) { // Only need to erase on flash return true; } std::vector arguments(9, 0); uint32_t numWords = static_cast(amount / 2); arguments[0] = static_cast(memType); *reinterpret_cast(&arguments[1]) = static_cast(*startAddress / 512); *reinterpret_cast(&arguments[5]) = numWords; auto msg = com->waitForMessageSync([this, &arguments] { return com->sendCommand(Command::NeoEraseMemory, arguments); }, NeoEraseDone, std::chrono::milliseconds(3000)); if(!msg) { return false; } return true; } bool Device::clearScript(Disk::MemoryType memType) { if(!stopScript()) return false; auto startAddress = getCoreminiStartAddress(memType); if(!startAddress) { return false; } std::vector clearData(512, 0xCD); auto written = writeLogicalDisk(*startAddress, clearData.data(), clearData.size(), std::chrono::milliseconds(2000), memType); if(!written) { return false; } if(*written == 0) { return false; } return true; } std::optional Device::readCoreminiHeader(Disk::MemoryType memType) { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return std::nullopt; } auto startAddress = getCoreminiStartAddress(memType); if(!startAddress) { return std::nullopt; } auto connected = isLogicalDiskConnected(); if(!connected) { return std::nullopt; // Already added an API error } #pragma pack(push, 2) struct RawCoreminiHeader { uint16_t fileType; uint16_t fileVersion; uint32_t storedFileSize; uint32_t fileChecksum; union { struct { uint32_t skipDecompression : 1; uint32_t encryptedMode : 1; uint32_t reserved : 30; } bits; uint32_t word; } flags; uint8_t fileHash[32]; union { struct { uint32_t lsb; uint32_t msb; } words; uint64_t time64; } createTime; uint8_t reserved[8]; }; #pragma pack(pop) RawCoreminiHeader header = {}; auto numRead = readLogicalDisk(*startAddress, (uint8_t*)&header, sizeof(header), std::chrono::milliseconds(2000), memType); if(!numRead) { return std::nullopt; // Already added an API error } if(*numRead != sizeof(header)) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return std::nullopt; } if(header.fileType != 0x0907) { report(APIEvent::Type::MessageFormattingError, APIEvent::Severity::Error); return std::nullopt; } std::optional ret; ret.emplace(); ret->coreminiVersion = header.fileVersion; ret->storedFileSize = header.storedFileSize; ret->fileChecksum = header.fileChecksum; ret->skipDecompression = static_cast(header.flags.bits.skipDecompression); ret->encryptedMode = static_cast(header.flags.bits.encryptedMode); std::copy(std::begin(header.fileHash), std::end(header.fileHash), ret->fileHash.begin()); static constexpr std::chrono::seconds icsEpochDelta(1167609600); static constexpr uint8_t timestampResolution = 25; static constexpr uint16_t nsInUs = 1'000; ret->timestamp += icsEpochDelta + std::chrono::microseconds(header.createTime.time64 * timestampResolution / nsInUs); return ret; } bool Device::transmit(std::shared_ptr frame) { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return false; } if(!isOnline()) { report(APIEvent::Type::DeviceCurrentlyOffline, APIEvent::Severity::Error); return false; } if(!isSupportedTXNetwork(frame->network)) { report(APIEvent::Type::UnsupportedTXNetwork, APIEvent::Severity::Error); return false; } bool extensionHookedTransmit = false; bool transmitStatusFromExtension = false; forEachExtension([&](const std::shared_ptr& ext) { if(!ext->transmitHook(frame, transmitStatusFromExtension)) extensionHookedTransmit = true; return !extensionHookedTransmit; // false breaks out of the loop early }); if(extensionHookedTransmit) return transmitStatusFromExtension; std::vector packet; if(!com->encoder->encode(*com->packetizer, packet, frame)) return false; return com->sendPacket(packet); } bool Device::transmit(std::vector> frames) { for(auto& frame : frames) { if(!transmit(frame)) return false; } return true; } void Device::setWriteBlocks(bool blocks) { com->setWriteBlocks(blocks); } size_t Device::getNetworkCountByType(Network::Type type) const { size_t count = 0; for(const auto& net : getSupportedRXNetworks()) if(net.getType() == type) count++; return count; } // Indexed starting at one Network Device::getNetworkByNumber(Network::Type type, size_t index) const { size_t count = 0; for(const auto& net : getSupportedRXNetworks()) { if(net.getType() == type) { count++; if(count == index) return net; } } return Network::NetID::Invalid; } std::shared_ptr Device::getHardwareInfo(std::chrono::milliseconds timeout) { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return nullptr; } if(!isOnline()) { report(APIEvent::Type::DeviceCurrentlyOffline, APIEvent::Severity::Error); return nullptr; } auto filter = std::make_shared(Message::Type::HardwareInfo); auto response = com->waitForMessageSync([this]() { return com->sendCommand(Command::GetHardwareInfo); }, filter, timeout); if(!response) { report(APIEvent::Type::Timeout, APIEvent::Severity::Error); return nullptr; } auto hardwareInfo = std::dynamic_pointer_cast(response); if(!hardwareInfo) { report(APIEvent::Type::UnexpectedResponse, APIEvent::Severity::Error); return nullptr; } return hardwareInfo; } std::optional Device::readLogicalDisk(uint64_t pos, uint8_t* into, uint64_t amount, std::chrono::milliseconds timeout, Disk::MemoryType memType) { if(!into || timeout <= std::chrono::milliseconds(0)) { report(APIEvent::Type::RequiredParameterNull, APIEvent::Severity::Error); return std::nullopt; } if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return std::nullopt; } std::lock_guard lk(diskMutex); if(diskReadDriver->getAccess() == Disk::Access::EntireCard && diskWriteDriver->getAccess() == Disk::Access::VSA) { // We have mismatched drivers, we need to add an offset to the diskReadDriver const auto offset = Disk::FindVSAInFAT([this, &timeout, &memType](uint64_t pos, uint8_t *into, uint64_t amount) { const auto start = std::chrono::steady_clock::now(); auto ret = diskReadDriver->readLogicalDisk(*com, report, pos, into, amount, timeout, memType); timeout -= std::chrono::duration_cast(std::chrono::steady_clock::now() - start); return ret; }); if(!offset.has_value()) return std::nullopt; diskReadDriver->setVSAOffset(*offset); } return diskReadDriver->readLogicalDisk(*com, report, pos, into, amount, timeout, memType); } std::optional Device::writeLogicalDisk(uint64_t pos, const uint8_t* from, uint64_t amount, std::chrono::milliseconds timeout, Disk::MemoryType memType) { if(!from || timeout <= std::chrono::milliseconds(0)) { report(APIEvent::Type::RequiredParameterNull, APIEvent::Severity::Error); return std::nullopt; } if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return std::nullopt; } return diskWriteDriver->writeLogicalDisk(*com, report, *diskReadDriver, pos, from, amount, timeout, memType); } std::optional Device::isLogicalDiskConnected() { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return std::nullopt; } const auto info = com->getLogicalDiskInfoSync(); if(!info) { report(APIEvent::Type::Timeout, APIEvent::Severity::Error); return std::nullopt; } return info->connected; } std::optional Device::getLogicalDiskSize() { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return std::nullopt; } const auto info = com->getLogicalDiskInfoSync(); if(!info) { report(APIEvent::Type::Timeout, APIEvent::Severity::Error); return std::nullopt; } const auto reportedSize = info->getReportedSize(); if(diskReadDriver->getAccess() == Disk::Access::VSA) return reportedSize - diskReadDriver->getVSAOffset(); return reportedSize; } std::optional Device::getVSAOffsetInLogicalDisk() { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return std::nullopt; } if(diskReadDriver->getAccess() == Disk::Access::VSA || diskReadDriver->getAccess() == Disk::Access::None) return 0ull; auto offset = Disk::FindVSAInFAT([this](uint64_t pos, uint8_t *into, uint64_t amount) { return diskReadDriver->readLogicalDisk(*com, report, pos, into, amount); }); if(!offset.has_value()) return std::nullopt; if(diskReadDriver->getAccess() == Disk::Access::EntireCard && diskWriteDriver->getAccess() == Disk::Access::VSA) { // We have mismatched drivers, we need to add an offset to the diskReadDriver diskReadDriver->setVSAOffset(*offset); return 0ull; } return *offset; } std::optional Device::getDigitalIO(IO type, size_t number /* = 1 */) { if(number == 0) { // Start counting from 1 report(APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::Error); return false; } std::lock_guard lk(ioMutex); switch(type) { case IO::EthernetActivation: if(getEthernetActivationLineCount() < number) break; // ParameterOutOfRange assert(number == 1); // If you implement a device with more, you'll need to modify the accessor if(!ethActivationStatus.has_value()) report(APIEvent::Type::ValueNotYetPresent, APIEvent::Severity::Error); return ethActivationStatus; case IO::USBHostPower: if(getUSBHostPowerCount() < number) break; // ParameterOutOfRange assert(number == 1); // If you implement a device with more, you'll need to modify the accessor if(!usbHostPowerStatus.has_value()) report(APIEvent::Type::ValueNotYetPresent, APIEvent::Severity::Error); return usbHostPowerStatus; case IO::BackupPowerEnabled: if(!getBackupPowerSupported()) break; // ParameterOutOfRange assert(number == 1); // If you implement a device with more, you'll need to modify the accessor if(!backupPowerEnabled.has_value()) report(APIEvent::Type::ValueNotYetPresent, APIEvent::Severity::Error); return backupPowerEnabled; case IO::BackupPowerGood: if(!getBackupPowerSupported()) break; // ParameterOutOfRange assert(number == 1); // If you implement a device with more, you'll need to modify the accessor if(!backupPowerGood.has_value()) report(APIEvent::Type::ValueNotYetPresent, APIEvent::Severity::Error); return backupPowerGood; case IO::Misc: { bool found = false; for(const auto& misc : getMiscIO()) { if(misc.number == number) { found = misc.supportsDigitalIn; break; } } if(!found) break; // ParameterOutOfRange if(number > miscDigital.size()) break; // ParameterOutOfRange if(!miscDigital[number - 1].has_value()) report(APIEvent::Type::ValueNotYetPresent, APIEvent::Severity::Error); return miscDigital[number - 1]; } case IO::EMisc: { bool found = false; for(const auto& misc : getEMiscIO()) { if(misc.number == number) { found = misc.supportsDigitalIn; break; } } if(!found) break; // ParameterOutOfRange if(number > miscDigital.size()) break; // ParameterOutOfRange // If there is ever a device with overlapping misc IOs and emisc IOs, // you will need to make a new member variable for the emisc IOs. if(!miscDigital[number - 1].has_value()) report(APIEvent::Type::ValueNotYetPresent, APIEvent::Severity::Error); return miscDigital[number - 1]; } }; report(APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::Error); return std::nullopt; } bool Device::setDigitalIO(IO type, size_t number, bool value) { if(number == 0) { // Start counting from 1 report(APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::Error); return false; } std::lock_guard lk(ioMutex); switch(type) { case IO::EthernetActivation: if(getEthernetActivationLineCount() < number) break; // ParameterOutOfRange assert(number == 1); // If you implement a device with more, you'll need to modify the accessor ethActivationStatus = value; return com->sendCommand(Command::MiscControl, { uint8_t(1), uint8_t(value ? 1 : 0), // enetActivateSet, enetActivateValue uint8_t(0), uint8_t(0), // usbHostPowerSet, usbHostPowerValue uint8_t(0), uint8_t(0) // backupPowerSet, backupPowerValue }); case IO::USBHostPower: if(getUSBHostPowerCount() < number) break; // ParameterOutOfRange assert(number == 1); // If you implement a device with more, you'll need to modify the accessor usbHostPowerStatus = value; return com->sendCommand(Command::MiscControl, { uint8_t(0), uint8_t(0), // enetActivateSet, enetActivateValue uint8_t(1), uint8_t(value ? 1 : 0), // usbHostPowerSet, usbHostPowerValue uint8_t(0), uint8_t(0) // backupPowerSet, backupPowerValue }); case IO::BackupPowerEnabled: if(!getBackupPowerSupported()) break; // ParameterOutOfRange assert(number == 1); // If you implement a device with more, you'll need to modify the accessor backupPowerEnabled = value; return com->sendCommand(Command::MiscControl, { uint8_t(0), uint8_t(0), // enetActivateSet, enetActivateValue uint8_t(0), uint8_t(value ? 1 : 0), // usbHostPowerSet, usbHostPowerValue (set to work around firmware bug) uint8_t(1), uint8_t(value ? 1 : 0) // backupPowerSet, backupPowerValue }); case IO::BackupPowerGood: break; // Read-only, return ParameterOutOfRange case IO::Misc: case IO::EMisc: break; // Read-only for the moment, return ParameterOutOfRange }; report(APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::Error); return false; } std::optional Device::getAnalogIO(IO type, size_t number /* = 1 */) { if(number == 0) { // Start counting from 1 report(APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::Error); return false; } std::lock_guard lk(ioMutex); switch(type) { case IO::EthernetActivation: case IO::USBHostPower: case IO::BackupPowerEnabled: case IO::BackupPowerGood: break; case IO::Misc: { bool found = false; for(const auto& misc : getMiscIO()) { if(misc.number == number) { found = misc.supportsAnalogIn; break; } } if(!found) break; // ParameterOutOfRange if(number > miscAnalog.size()) break; // ParameterOutOfRange if(!miscAnalog[number - 1].has_value()) report(APIEvent::Type::ValueNotYetPresent, APIEvent::Severity::Error); return miscAnalog[number - 1]; } case IO::EMisc: { bool found = false; for(const auto& misc : getEMiscIO()) { if(misc.number == number) { found = misc.supportsAnalogIn; break; } } if(!found) break; // ParameterOutOfRange if(number > miscAnalog.size()) break; // ParameterOutOfRange // If there is ever a device with overlapping misc IOs and emisc IOs, // you will need to make a new member variable for the emisc IOs. if(!miscAnalog[number - 1].has_value()) report(APIEvent::Type::ValueNotYetPresent, APIEvent::Severity::Error); return miscAnalog[number - 1]; } }; report(APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::Error); return std::nullopt; } void Device::wiviThreadBody() { std::shared_ptr filter = std::make_shared(Message::Type::WiVICommandResponse); std::unique_lock lk(wiviMutex); EventManager::GetInstance().downgradeErrorsOnCurrentThread(); bool first = true; while(!stopWiVIThread) { if(first) // Skip the first wait first = false; else stopWiVIcv.wait_for(lk, std::chrono::seconds(3)); // Use the command GetAll to get a WiVI::Info structure from the device const auto generic = com->waitForMessageSync([this]() { return com->sendCommand(Command::WiVICommand, WiVI::CommandPacket::GetAll::Encode()); }, filter, std::chrono::milliseconds(1000)); if(!generic || generic->type != Message::Type::WiVICommandResponse) { report(APIEvent::Type::WiVIStackRefreshFailed, APIEvent::Severity::Error); continue; } const auto resp = std::static_pointer_cast(generic); if(!resp->success || !resp->info.has_value()) { report(APIEvent::Type::WiVIStackRefreshFailed, APIEvent::Severity::Error); continue; } // Now we know we have a WiVI::Info structure // Don't process captures unless there is a callback attached, // we don't want to clear any while nobody's listening. bool processCaptures = false; for(const auto& cb : newCaptureCallbacks) { if(cb) { processCaptures = true; break; } } if(processCaptures) { std::vector clearMasks; for (size_t i = 0; i < resp->info->captures.size(); i++) { const auto capture = resp->info->captures.at(i); if(capture.flags.uploadOverflow) report(APIEvent::Type::WiVIUploadStackOverflow, APIEvent::Severity::Error); const auto MaxUploads = sizeof(capture.uploadStack) / sizeof(capture.uploadStack[0]); auto uploadCount = capture.flags.uploadStackSize + 1u; if(uploadCount > MaxUploads) { report(APIEvent::Type::WiVIStackRefreshFailed, APIEvent::Severity::Error); uploadCount = MaxUploads; } for(size_t j = 0; j < uploadCount; j++) { const auto& upload = capture.uploadStack[j]; if(!upload.flags.pending) continue; // Not complete yet, don't notify // Schedule this upload to be cleared from the firmware's stack if(clearMasks.size() != resp->info->captures.size()) clearMasks.resize(resp->info->captures.size()); clearMasks[i] |= (1 << j); WiVIUpload wiviUpload {}; wiviUpload.captureIndex = capture.captureBlockIndex; wiviUpload.cellular = capture.flags.uploadOverCellular; wiviUpload.wifi = capture.flags.uploadOverWiFi; wiviUpload.isPrePost = capture.flags.isPrePost; wiviUpload.isPreTime = capture.flags.isPreTime; wiviUpload.preTriggerSize = capture.preTriggerSize; wiviUpload.priority = capture.flags.uploadPriority; wiviUpload.startSector = upload.startSector; wiviUpload.endSector = upload.endSector; // Notify the client for(const auto& cb : newCaptureCallbacks) { if(cb) { lk.unlock(); try { cb(wiviUpload); } catch(...) { report(APIEvent::Type::Unknown, APIEvent::Severity::Error); } lk.lock(); } } } } if(!clearMasks.empty()) { const auto clearMasksGenericResp = com->waitForMessageSync([this, &clearMasks]() { return com->sendCommand(Command::WiVICommand, WiVI::CommandPacket::ClearUploads::Encode(clearMasks)); }, filter, std::chrono::milliseconds(1000)); if(!clearMasksGenericResp || clearMasksGenericResp->type != Message::Type::WiVICommandResponse || !std::static_pointer_cast(clearMasksGenericResp)->success) { report(APIEvent::Type::WiVIStackRefreshFailed, APIEvent::Severity::Error); } } } // Process sleep requests if(resp->info->sleepRequest & 1 /* sleep requested by VSSAL */) { // Notify any callers we haven't notified yet for(auto& cb : sleepRequestedCallbacks) { if(!cb.second && cb.first) { cb.second = true; lk.unlock(); try { cb.first(resp->info->connectionTimeoutMinutes); } catch(...) { report(APIEvent::Type::Unknown, APIEvent::Severity::Error); } lk.lock(); } } } else { // If the sleepRequest becomes 1 again we will notify again for(auto& cb : sleepRequestedCallbacks) cb.second = false; } } } void Device::stopWiVIThreadIfNecessary(std::unique_lock lk) { // The callbacks will be empty std::functions if they are removed for(const auto& cb : newCaptureCallbacks) { if(cb) return; // We still need the WiVI Thread } for(const auto& cb : sleepRequestedCallbacks) { if(cb.first) return; // We still need the WiVI Thread } stopWiVIThread = true; lk.unlock(); stopWiVIcv.notify_all(); wiviThread.join(); wiviThread = std::thread(); } Lifetime Device::addNewCaptureCallback(NewCaptureCallback cb) { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return {}; } if(!supportsWiVI()) { report(APIEvent::Type::WiVINotSupported, APIEvent::Severity::Error); return {}; } std::lock_guard lk(wiviMutex); if(!wiviThread.joinable()) { // Start the thread stopWiVIThread = false; wiviThread = std::thread([this]() { wiviThreadBody(); }); } size_t idx = 0; for(; idx < newCaptureCallbacks.size(); idx++) { if(!newCaptureCallbacks[idx]) // Empty space (previously erased callback) break; } if(idx == newCaptureCallbacks.size()) // Create a new space newCaptureCallbacks.push_back(std::move(cb)); else newCaptureCallbacks[idx] = std::move(cb); // Cleanup function to remove this capture callback return Lifetime([this, idx]() { // TODO: Hold a weak ptr to the `this` instead of relying on the user to keep `this` valid std::unique_lock lk2(wiviMutex); newCaptureCallbacks[idx] = NewCaptureCallback(); stopWiVIThreadIfNecessary(std::move(lk2)); }); } Lifetime Device::addSleepRequestedCallback(SleepRequestedCallback cb) { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return {}; } if(!supportsWiVI()) { report(APIEvent::Type::WiVINotSupported, APIEvent::Severity::Error); return {}; } std::lock_guard lk(wiviMutex); if(!wiviThread.joinable()) { // Start the thread stopWiVIThread = false; wiviThread = std::thread([this]() { wiviThreadBody(); }); } size_t idx = 0; for(; idx < sleepRequestedCallbacks.size(); idx++) { if(!sleepRequestedCallbacks[idx].first) // Empty space (previously erased callback) break; } if(idx == sleepRequestedCallbacks.size()) // Create a new space sleepRequestedCallbacks.emplace_back(std::move(cb), false); else sleepRequestedCallbacks[idx] = { std::move(cb), false }; // Cleanup function to remove this sleep requested callback return Lifetime([this, idx]() { // TODO: Hold a weak ptr to the `this` instead of relying on the user to keep `this` valid std::unique_lock lk2(wiviMutex); sleepRequestedCallbacks[idx].first = SleepRequestedCallback(); stopWiVIThreadIfNecessary(std::move(lk2)); }); } std::optional Device::isSleepRequested() const { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return std::nullopt; } if(!supportsWiVI()) { report(APIEvent::Type::WiVINotSupported, APIEvent::Severity::Error); return std::nullopt; } static std::shared_ptr filter = std::make_shared(Message::Type::WiVICommandResponse); // Hold this lock so the WiVI stack doesn't issue a WiVICommand at the same time as us std::lock_guard lk(wiviMutex); const auto generic = com->waitForMessageSync([this]() { // VSSAL sets bit0 to indicate that it's waiting to sleep, then // it waits for Wireless neoVI to acknowledge by clearing it. // If we set bit1 at the same time we clear bit0, remote wakeup // will be suppressed (assuming the device supported it in the // first place) return com->sendCommand(Command::WiVICommand, WiVI::CommandPacket::GetSignal::Encode(WiVI::SignalType::SleepRequest)); }, filter, std::chrono::milliseconds(1000)); if(!generic || generic->type != Message::Type::WiVICommandResponse) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return std::nullopt; } const auto resp = std::static_pointer_cast(generic); if(!resp->success || !resp->value.has_value()) { report(APIEvent::Type::ValueNotYetPresent, APIEvent::Severity::Error); return std::nullopt; } return *resp->value; } bool Device::allowSleep(bool remoteWakeup) { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return false; } if(!supportsWiVI()) { report(APIEvent::Type::WiVINotSupported, APIEvent::Severity::Error); return false; } static std::shared_ptr filter = std::make_shared(Message::Type::WiVICommandResponse); // Hold this lock so the WiVI stack doesn't issue a WiVICommand at the same time as us std::lock_guard lk(wiviMutex); const auto generic = com->waitForMessageSync([this, remoteWakeup]() { // VSSAL sets bit0 to indicate that it's waiting to sleep, then // it waits for Wireless neoVI to acknowledge by clearing it. // If we set bit1 at the same time we clear bit0, remote wakeup // will be suppressed (assuming the device supported it in the // first place) return com->sendCommand(Command::WiVICommand, WiVI::CommandPacket::SetSignal::Encode( WiVI::SignalType::SleepRequest, remoteWakeup ? 0 : 2 )); }, filter, std::chrono::milliseconds(1000)); if(!generic || generic->type != Message::Type::WiVICommandResponse) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return false; } const auto resp = std::static_pointer_cast(generic); if(!resp->success) { report(APIEvent::Type::ValueNotYetPresent, APIEvent::Severity::Error); return false; } return true; } void Device::scriptStatusThreadBody() { std::unique_lock lk(scriptStatusMutex); EventManager::GetInstance().downgradeErrorsOnCurrentThread(); bool first = true; while(!stopScriptStatusThread) { if(first) // Skip the first wait first = false; else stopScriptStatusCv.wait_for(lk, std::chrono::seconds(10)); const auto resp = getScriptStatus(); if(!resp) continue; //If value changed/was inserted, notify callback if(updateScriptStatusValue(ScriptStatus::CoreMiniRunning, resp->isCoreminiRunning)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::CoreMiniRunning, resp->isCoreminiRunning); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::IsEncrypted, resp->isEncrypted)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::IsEncrypted, resp->isEncrypted); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::SectorOverflow, resp->sectorOverflows)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::SectorOverflow, resp->sectorOverflows); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::RemainingSectors, resp->numRemainingSectorBuffers)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::RemainingSectors, resp->numRemainingSectorBuffers); lk.lock(); } bool logging = false; if(updateScriptStatusValue(ScriptStatus::LastSector, resp->lastSector)) { logging = true; lk.unlock(); notifyScriptStatusCallback(ScriptStatus::LastSector, resp->lastSector); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::Logging, logging)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::Logging, logging); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::ReadBinSize, resp->readBinSize)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::ReadBinSize, resp->readBinSize); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::MinSector, resp->minSector)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::MinSector, resp->minSector); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::MaxSector, resp->maxSector)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::MaxSector, resp->maxSector); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::CurrentSector, resp->currentSector)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::CurrentSector, resp->currentSector); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::CoreMiniCreateTime, resp->coreminiCreateTime)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::CoreMiniCreateTime, resp->coreminiCreateTime); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::FileChecksum, resp->fileChecksum)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::FileChecksum, resp->fileChecksum); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::CoreMiniVersion, resp->coreminiVersion)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::CoreMiniVersion, resp->coreminiVersion); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::CoreMiniHeaderSize, resp->coreminiHeaderSize)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::CoreMiniHeaderSize, resp->coreminiHeaderSize); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::DiagnosticErrorCode, resp->diagnosticErrorCode)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::DiagnosticErrorCode, resp->diagnosticErrorCode); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::DiagnosticErrorCodeCount, resp->diagnosticErrorCodeCount)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::DiagnosticErrorCodeCount, resp->diagnosticErrorCodeCount); lk.lock(); } if(updateScriptStatusValue(ScriptStatus::MaxCoreMiniSize, resp->maxCoreminiSizeKB)) { lk.unlock(); notifyScriptStatusCallback(ScriptStatus::MaxCoreMiniSize, resp->maxCoreminiSizeKB); lk.lock(); } } } std::shared_ptr Device::getScriptStatus() const { static std::shared_ptr filter = std::make_shared(Message::Type::ScriptStatus); const auto generic = com->waitForMessageSync([this]() { return com->sendCommand(Command::ScriptStatus); }, filter, std::chrono::milliseconds(3000)); if(!generic || generic->type != Message::Type::ScriptStatus) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return nullptr; } return std::static_pointer_cast(generic); } bool Device::updateScriptStatusValue(ScriptStatus key, uint64_t value) { auto pair = scriptStatusValues.find(key); if(pair != scriptStatusValues.end()) { if(pair->second != value) { //Value changed scriptStatusValues[key] = value; return true; } //Value didn't change return false; } //Value was inserted scriptStatusValues.insert(std::make_pair(key, value)); return true; } void Device::notifyScriptStatusCallback(ScriptStatus key, uint64_t value) { auto callbackList = scriptStatusCallbacks.find(key); if(callbackList != scriptStatusCallbacks.end()) { for(const auto& callback : callbackList->second) { if(callback) { try { callback(value); } catch(...) { report(APIEvent::Type::Unknown, APIEvent::Severity::Error); } } } } } Lifetime Device::addScriptStatusCallback(ScriptStatus key, ScriptStatusCallback cb) { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return {}; } std::lock_guard lk(scriptStatusMutex); if(!scriptStatusThread.joinable()) { // Start the thread stopScriptStatusThread = false; scriptStatusThread = std::thread([this]() { scriptStatusThreadBody(); }); } size_t idx = 0; std::vector callbackList; auto callbackPair = scriptStatusCallbacks.find(key); if(callbackPair != scriptStatusCallbacks.end()) callbackList = callbackPair->second; if(idx == callbackList.size()) callbackList.push_back(std::move(cb)); else callbackList[idx] = std::move(cb); scriptStatusCallbacks.insert_or_assign(key, callbackList); return Lifetime([this, key, idx](){ std::unique_lock lk2(scriptStatusMutex); auto callbackList = scriptStatusCallbacks.find(key); if(callbackList != scriptStatusCallbacks.end()) callbackList->second[idx] = ScriptStatusCallback(); stopScriptStatusThreadIfNecessary(std::move(lk2)); }); } void Device::stopScriptStatusThreadIfNecessary(std::unique_lock lk) { for(const auto& callbackList : scriptStatusCallbacks) { for(const auto& callback : callbackList.second) { if(callback) return; } } stopScriptStatusThread = true; lk.unlock(); stopScriptStatusCv.notify_all(); scriptStatusThread.join(); scriptStatusThread = std::thread(); } Lifetime Device::suppressDisconnects() { std::lock_guard lk(heartbeatMutex); heartbeatSuppressedByUser++; return Lifetime([this] { std::lock_guard lk2(heartbeatMutex); heartbeatSuppressedByUser--; }); } void Device::addExtension(std::shared_ptr&& extension) { std::lock_guard lk(extensionsLock); extensions.push_back(extension); } void Device::forEachExtension(std::function&)> fn) { std::vector> extensionsCopy; { std::lock_guard lk(extensionsLock); extensionsCopy = extensions; } for(const auto& ext : extensionsCopy) { if(!fn(ext)) break; } } void Device::handleInternalMessage(std::shared_ptr message) { switch(message->type) { case Message::Type::ResetStatus: latestResetStatus = std::static_pointer_cast(message); break; case Message::Type::RawMessage: { auto rawMessage = std::static_pointer_cast(message); switch(rawMessage->network.getNetID()) { case Network::NetID::Device: { // Device is not guaranteed to be a CANMessage, it might be a RawMessage // if it couldn't be decoded to a CANMessage. We only care about the // CANMessage decoding right now. auto canmsg = std::dynamic_pointer_cast(message); if(canmsg) handleNeoVIMessage(std::move(canmsg)); break; } case Network::NetID::DeviceStatus: // Device Status format is unique per device, so the devices need to decode it themselves handleDeviceStatus(rawMessage); break; default: break; //std::cout << "HandleInternalMessage got a message from " << message->network << " and it was unhandled!" << std::endl; } break; } default: break; } forEachExtension([&](const std::shared_ptr& ext) { ext->handleMessage(message); return true; // false breaks out early }); } void Device::handleNeoVIMessage(std::shared_ptr message) { switch(message->arbid) { case 0x103: { // Report Message (neoVI FIRE 2) if(message->data.size() < 34) { report(APIEvent::Type::PacketDecodingError, APIEvent::Severity::EventWarning); return; } uint16_t emisc[2]; memcpy(emisc, message->data.data() + 24, sizeof(emisc)); std::lock_guard lk(ioMutex); miscAnalog[0] = (message->data[24] | (uint16_t(message->data[25]) << 8)) * 0.01015511; // In volts now miscAnalog[1] = (message->data[26] | (uint16_t(message->data[27]) << 8)) * 0.01015511; miscDigital[0] = message->data[28] & 0x01; miscDigital[1] = message->data[29] & 0x01; miscDigital[4] = message->data[30] & 0x01; miscDigital[5] = message->data[31] & 0x01; } } } bool Device::firmwareUpdateSupported() { bool ret = false; forEachExtension([&ret](const std::shared_ptr& ext) { if(ext->providesFirmware()) { ret = true; return false; } return true; // false breaks out early }); return ret; } APIEvent::Type Device::getCommunicationNotEstablishedError() { if(firmwareUpdateSupported()) { if(requiresVehiclePower()) return APIEvent::Type::NoSerialNumberFW12V; else return APIEvent::Type::NoSerialNumberFW; } else { if(requiresVehiclePower()) return APIEvent::Type::NoSerialNumber12V; else return APIEvent::Type::NoSerialNumber; } } void Device::updateLEDState() { std::vector args {(uint8_t) ledState}; com->sendCommand(Command::UpdateLEDState, args); } std::optional Device::sendEthPhyMsg(const EthPhyMessage& message, std::chrono::milliseconds timeout) { if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return std::nullopt; } if(!getEthPhyRegControlSupported()) { report(APIEvent::Type::EthPhyRegisterControlNotAvailable, APIEvent::Severity::Error); return std::nullopt; } if(!isOnline()) { report(APIEvent::Type::DeviceCurrentlyOffline, APIEvent::Severity::Error); return std::nullopt; } std::vector bytes; HardwareEthernetPhyRegisterPacket::EncodeFromMessage(message, bytes, report); std::shared_ptr response = com->waitForMessageSync( [this, bytes](){ return com->sendCommand(Command::PHYControlRegisters, bytes); }, std::make_shared(Network::NetID::EthPHYControl), timeout); if(!response) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return std::nullopt; } auto retMsg = std::static_pointer_cast(response); if(!retMsg) { return std::nullopt; } return std::make_optional(*retMsg); } std::optional Device::SetRootDirectoryEntryFlags(uint8_t mask, uint8_t values, uint32_t collectionEntryByteAddress) { if(!supportsWiVI()) { report(APIEvent::Type::WiVINotSupported, APIEvent::Severity::EventWarning); return std::nullopt; } if (mask & RootDirectoryEntryFlags::IsPrePost) { report(APIEvent::Type::RestrictedEntryFlag, APIEvent::Severity::EventWarning); mask &= ~RootDirectoryEntryFlags::IsPrePost; values &= ~RootDirectoryEntryFlags::IsPrePost; } if (mask & RootDirectoryEntryFlags::PrePostTriggered) { report(APIEvent::Type::RestrictedEntryFlag, APIEvent::Severity::EventWarning); mask &= ~RootDirectoryEntryFlags::PrePostTriggered; values &= ~RootDirectoryEntryFlags::PrePostTriggered; } auto timeout = std::chrono::milliseconds(2500); std::vector args( {(uint8_t)(collectionEntryByteAddress & 0xFF), (uint8_t)((collectionEntryByteAddress >> 8) & 0xFF), (uint8_t)((collectionEntryByteAddress >> 16) & 0xFF), (uint8_t)((collectionEntryByteAddress >> 24) & 0xFF), values, mask}); std::shared_ptr response = com->waitForMessageSync( [this, args](){ return com->sendCommand(ExtendedCommand::SetRootFSEntryFlags, args); }, std::make_shared(Message::Type::ExtendedResponse), timeout); if(!response) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return std::nullopt; } auto retMsg = std::static_pointer_cast(response); if(!retMsg) { // TODO fix this error report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return false; } bool success = retMsg->response == ExtendedResponse::OK; if(!success) { // TODO fix this error report(APIEvent::Type::Unknown, APIEvent::Severity::EventWarning); } // Valid device with a properly formed response, return success return success; } std::optional> Device::getRTC() { static const std::shared_ptr filter = std::make_shared(Network::NetID::RED_GET_RTC); std::shared_ptr generic = com->waitForMessageSync([this]() { return com->sendCommand(Command::GetRTC); }, filter, std::chrono::milliseconds(3000)); if(!generic) // Did not receive a message return std::nullopt; auto rawMes = std::dynamic_pointer_cast(generic); if(!rawMes) return std::nullopt; if(rawMes->data.size() != sizeof(RTCCTIME)) return std::nullopt; const auto* time = (RTCCTIME*)rawMes->data.data(); std::tm stdTime = {}; // std::tm has no member for the `FracSec` member of RTCCTIME struct stdTime.tm_sec = time->Sec; stdTime.tm_min = time->Min; stdTime.tm_hour = time->Hour; stdTime.tm_mday = time->Day; stdTime.tm_mon = time->Month - 1; // [0-11] stdTime.tm_year = time->Year + 100; // Number of years since 1900+100 stdTime.tm_wday = time->DOW; // RTCCTIME struct has no member for `tm_yday` #ifdef _MSC_VER #define timegm _mkgmtime #endif return std::chrono::system_clock::from_time_t(timegm(&stdTime)); } bool Device::setRTC(const std::chrono::time_point& time) { auto now = std::chrono::system_clock::to_time_t(time); const auto timeInfo = std::gmtime(&now); if(!timeInfo) return false; // Populate the RTCCTIME struct using the timeInfo and offsets // Create a vector of arguments to send as the payload to the communication command std::vector bytestream(sizeof(RTCCTIME)); auto rtcVals = (RTCCTIME*)bytestream.data(); rtcVals->FracSec = (uint8_t)0x00; rtcVals->Sec = (uint8_t)timeInfo->tm_sec; rtcVals->Min = (uint8_t)timeInfo->tm_min; rtcVals->Hour = (uint8_t)timeInfo->tm_hour; rtcVals->DOW = (uint8_t)timeInfo->tm_wday + 1; rtcVals->Day = (uint8_t)timeInfo->tm_mday; rtcVals->Month = (uint8_t)timeInfo->tm_mon + 1; // [0-11] rtcVals->Year = (uint8_t)timeInfo->tm_year % 100; // divide by 100 and take remainder to get last 2 digits of year const auto generic = com->waitForMessageSync([&]() { return com->sendCommand(Command::SetRTC, bytestream); }, std::make_shared(Command::SetRTC), std::chrono::milliseconds(100)); if(!generic) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return false; } auto m51msg = std::dynamic_pointer_cast(generic); if(!m51msg || m51msg->data.size() != 1) { report(APIEvent::Type::MessageFormattingError, APIEvent::Severity::Error); return false; } return m51msg->data.front(); } std::optional> Device::getSupportedFeatures() { auto timeout = std::chrono::milliseconds(100); std::shared_ptr msg = com->waitForMessageSync( [this](){ return com->sendCommand(ExtendedCommand::GetSupportedFeatures, {}); }, std::make_shared(Message::Type::SupportedFeatures), timeout); if(!msg) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return std::nullopt; } const auto& typedResponse = std::dynamic_pointer_cast(msg); if(!typedResponse) { report(APIEvent::Type::UnexpectedResponse, APIEvent::Severity::Error); return std::nullopt; } return std::move(typedResponse->features); } std::optional Device::getGenericBinarySize(uint16_t binaryIndex) { auto timeout = std::chrono::milliseconds(2000); if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return std::nullopt; } if(!isOnline()) { report(APIEvent::Type::DeviceCurrentlyOffline, APIEvent::Severity::Error); return std::nullopt; } std::vector args = GenericBinaryStatusPacket::EncodeArguments(binaryIndex); std::shared_ptr response = com->waitForMessageSync( [this, &args](){ return com->sendCommand(ExtendedCommand::GenericBinaryInfo, args); }, std::make_shared(Message::Type::GenericBinaryStatus), timeout ); if(!response) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return std::nullopt; } auto retMsg = std::static_pointer_cast(response); if(!retMsg) { return std::nullopt; } return retMsg->binarySize; } bool Device::readBinaryFile(std::ostream& stream, uint16_t binaryIndex) { auto timeout = std::chrono::milliseconds(100); auto size = getGenericBinarySize(binaryIndex); if(!size) { return false; } std::vector arguments(sizeof(ExtendedDataMessage::ExtendedDataHeader)); ExtendedDataMessage::ExtendedDataHeader& parameters = *reinterpret_cast(arguments.data()); auto filter = std::make_shared(Network::NetID::ExtendedData); for(size_t offset = 0; offset < *size; offset+=ExtendedDataMessage::MaxExtendedDataBufferSize) { parameters.subCommand = ExtendedDataSubCommand::GenericBinaryRead; parameters.userValue = static_cast(binaryIndex); parameters.offset = static_cast(offset); parameters.length = static_cast(std::min(ExtendedDataMessage::MaxExtendedDataBufferSize, *size - offset)); std::shared_ptr response = com->waitForMessageSync( [this, arguments](){ return com->sendCommand(Command::ExtendedData, arguments); }, filter, timeout ); if(!response) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return false; } auto retMsg = std::static_pointer_cast(response); if(!stream.write(reinterpret_cast(retMsg->data.data()), retMsg->data.size())) { return false; } } return true; } bool Device::subscribeLiveData(std::shared_ptr message) { if(!supportsLiveData()) { report(APIEvent::Type::LiveDataNotSupported, APIEvent::Severity::Error); return false; } if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return false; } if((message->args.size() > MAX_LIVE_DATA_ENTRIES) || message->args.empty()) { report(APIEvent::Type::LiveDataInvalidArgument, APIEvent::Severity::Error); return false; } std::vector bytes; if(!com->encoder->encode(*com->packetizer, bytes, message)) { report(APIEvent::Type::LiveDataEncoderError, APIEvent::Severity::Error); return false; } std::shared_ptr response = com->waitForMessageSync( [this, &bytes](){ return com->sendPacket(bytes); }, std::make_shared(Message::Type::LiveData)); if(response) { auto statusMsg = std::dynamic_pointer_cast(response); if(statusMsg && statusMsg->requestedCommand == message->cmd) { switch(statusMsg->status) { case LiveDataStatus::SUCCESS: return true; case LiveDataStatus::ERR_DUPLICATE: case LiveDataStatus::ERR_HANDLE: { report(APIEvent::Type::LiveDataInvalidHandle, APIEvent::Severity::Error); return false; } case LiveDataStatus::ERR_FULL: { report(APIEvent::Type::LiveDataMaxSignalsReached, APIEvent::Severity::Error); return false; } case LiveDataStatus::ERR_UNKNOWN_COMMAND: { report(APIEvent::Type::LiveDataCommandFailed, APIEvent::Severity::Error); return false; } default: break; } } } report(APIEvent::Type::LiveDataNoDeviceResponse, APIEvent::Severity::Error); return false; } bool Device::unsubscribeLiveData(const LiveDataHandle& handle) { if(!supportsLiveData()) { report(APIEvent::Type::LiveDataNotSupported, APIEvent::Severity::Error); return false; } if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return false; } if(!handle) { report(APIEvent::Type::RequiredParameterNull, APIEvent::Severity::Error); return false; } auto msg = std::make_shared(); msg->cmd = LiveDataCommand::UNSUBSCRIBE; msg->handle = handle; std::vector bytes; if(!com->encoder->encode(*com->packetizer, bytes, msg)) { report(APIEvent::Type::LiveDataEncoderError, APIEvent::Severity::Error); return false; } std::shared_ptr response = com->waitForMessageSync( [this, &bytes](){ return com->sendPacket(bytes); }, std::make_shared(Message::Type::LiveData)); if(!response) { report(APIEvent::Type::LiveDataNoDeviceResponse, APIEvent::Severity::Error); return false; } auto statusMsg = std::dynamic_pointer_cast(response); if(!statusMsg || statusMsg->requestedCommand != msg->cmd) { report(APIEvent::Type::MessageFormattingError, APIEvent::Severity::Error); return false; } if(statusMsg->status != LiveDataStatus::SUCCESS) { report(APIEvent::Type::LiveDataCommandFailed, APIEvent::Severity::Error); return false; } return true; } bool Device::clearAllLiveData() { if(!supportsLiveData()) { report(APIEvent::Type::LiveDataNotSupported, APIEvent::Severity::Error); return false; } if(!isOpen()) { report(APIEvent::Type::DeviceCurrentlyClosed, APIEvent::Severity::Error); return false; } auto msg = std::make_shared(); msg->cmd = LiveDataCommand::CLEAR_ALL; std::vector bytes; if(!com->encoder->encode(*com->packetizer, bytes, msg)) { report(APIEvent::Type::LiveDataEncoderError, APIEvent::Severity::Error); return false; } std::shared_ptr response = com->waitForMessageSync( [this, &bytes](){ return com->sendPacket(bytes); }, std::make_shared(Message::Type::LiveData)); if(!response) { report(APIEvent::Type::LiveDataNoDeviceResponse, APIEvent::Severity::Error); return false; } auto statusMsg = std::dynamic_pointer_cast(response); if(!statusMsg || statusMsg->requestedCommand != msg->cmd) { report(APIEvent::Type::MessageFormattingError, APIEvent::Severity::Error); return false; } if(statusMsg->status != LiveDataStatus::SUCCESS) { report(APIEvent::Type::LiveDataCommandFailed, APIEvent::Severity::Error); return false; } return true; } bool Device::readVSA(const VSAExtractionSettings& extractionSettings) { if(isOnline()) { goOffline(); } auto innerReadVSA = [&](uint64_t diskSize) -> bool { // Adjust driver to offset to start of VSA file const auto& offset = getVSAOffsetInLogicalDisk(); if(!offset) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } diskReadDriver->setVSAOffset(*offset); // Gather metadata about VSA file system VSAMetadata metadata; metadata.diskSize = diskSize; if(!probeVSA(metadata, extractionSettings)) { return false; } if(extractionSettings.filters.empty()) { // Full SD Dump if(!parseVSA(metadata, extractionSettings)) { return false; } } else { // Only read data specified by filters for(const auto& filter : extractionSettings.filters) { if(!parseVSA(metadata, extractionSettings, filter)) { return false; } } } return true; }; auto bufferSize = getVSADiskSize(); if(!bufferSize) { return false; } const auto wasScriptStarted = getScriptStatus()->isCoreminiRunning; if(extractionSettings.stopCoreMini && wasScriptStarted) { stopScript(); } const auto ret = innerReadVSA(*bufferSize); // Restart CoreMini script if stopped if(extractionSettings.stopCoreMini && wasScriptStarted) { startScript(); } return ret; } bool Device::probeVSA(VSAMetadata& metadata, const VSAExtractionSettings& extractionSettings) { auto cmTimestamp = getCoreMiniScriptTimestamp(); if(!metadata.coreMiniTimestamp) { return false; } metadata.coreMiniTimestamp = *cmTimestamp; const auto& isOverlapped = isVSAOverlapped(metadata); if(isOverlapped) { metadata.isOverlapped = *isOverlapped; } else { return false; } if(!findFirstVSARecord(metadata.firstRecordLocation, metadata.firstRecord, extractionSettings, metadata)) { return false; } if(!findLastVSARecord(metadata.lastRecordLocation, metadata.lastRecord, extractionSettings, metadata)) { return false; } if(metadata.isOverlapped) { // The last byte in the buffer should immediately precede the first if the buffer is overlapped metadata.bufferEnd = metadata.firstRecordLocation; } else { metadata.bufferEnd = metadata.lastRecordLocation; const auto& type = metadata.lastRecord->getType(); if(type == VSA::Type::AA0D || type == VSA::Type::AA0E || type == VSA::Type::AA0F) { // Add bytes based off of how many records should be in extended record sequence metadata.bufferEnd += std::dynamic_pointer_cast(metadata.lastRecord)->getRecordCount() * VSA::StandardRecordSize; } else if(type == VSA::Type::AA6A) { // Add a full sector for script status backup records metadata.bufferEnd += Disk::SectorSize; } else { // All other records add only one single record offset. metadata.bufferEnd += VSA::StandardRecordSize; } } return true; } bool Device::findFirstVSARecord(uint64_t& firstOffset, std::shared_ptr& firstRecord, const VSAExtractionSettings& extractionSettings, std::optional optMetadata) { // Grab important metadata features if metadata not defined VSAMetadata metadata; if(!optMetadata) { const auto& coreMiniTimestamp = getCoreMiniScriptTimestamp(); if(!coreMiniTimestamp) { return false; } metadata.coreMiniTimestamp = *coreMiniTimestamp; const auto& diskSize = getVSADiskSize(); if(!diskSize) { return false; } metadata.diskSize = *diskSize; const auto& isOverlapped = isVSAOverlapped(metadata); if(isOverlapped) { metadata.isOverlapped = *isOverlapped; } else { return false; } } else { metadata = *optMetadata; } if(!metadata.isOverlapped) { // Grab the first record in the buffer std::vector buffer; buffer.resize(Disk::SectorSize); const auto& bytesRead = readLogicalDisk(VSA::RecordStartOffset, buffer.data(), Disk::SectorSize); if(!bytesRead || *bytesRead < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } VSAParser parser(report); std::shared_ptr record; const auto& firstRecordStatus = parser.getRecordFromBytes(buffer.data(), Disk::SectorSize, record); if(firstRecordStatus == VSAParser::RecordParseStatus::Success) { firstOffset = VSA::RecordStartOffset; firstRecord = record; return true; } report(APIEvent::Type::VSAOtherError, APIEvent::Severity::Error); return false; } static constexpr size_t RegionCount = 2; // Number of regions to chunk remaining disk into during logarithmic search static constexpr size_t LinearSearchMaxSize = 0x00010000u; // Size of remaining disk space at which to begin iterative search (64 KB) // Initialize variables for disk search VSAParser parser(report); uint64_t first = VSA::RecordStartOffset; // First byte of vsa records to search uint64_t last = metadata.diskSize; // One beyond the last byte of vsa records to search uint64_t lastSector = last - Disk::SectorSize; std::vector buffer; buffer.resize(Disk::SectorSize); uint64_t start = first; // First byte of data to search for this iteration of algorithm uint64_t stop = last; // One beyond last byte of data to search for this iteration of algorithm // Repeatedly chunk data into regions and find largest negative difference in timestamp // (i.e., where the timestamp at the beginning of the region is much larger than at the end) // until data is under the LinearSearchMaxSize while(stop - start > LinearSearchMaxSize) { uint64_t regionSize = (stop - start) / RegionCount; regionSize -= regionSize % Disk::SectorSize; // Ensures regions are chunked on a sector boundary uint64_t largestNegative = 0; size_t largestNegativeIndex = 0; uint64_t smallestStartTimestamp = UINT64_MAX; size_t smallestStartTimestampIndex = 0; // Only necessary in cases where smallest timestamp falls on front edge of a region for(size_t i = 0; i < RegionCount; i++) { uint64_t regionStart = start + i * regionSize; // Start of last sector to read in the current region uint64_t regionLastSector = (i != RegionCount - 1) ? regionStart + regionSize - Disk::SectorSize : (stop - Disk::SectorSize) - ((stop - Disk::SectorSize) % Disk::SectorSize); const auto& timestampStart = getVSATimestampOrBefore(parser, buffer, regionStart, first, metadata); const auto& timestampStop = getVSATimestampOrAfter(parser, buffer, regionLastSector, lastSector, metadata); // Ensure valid timestamps were found if(!timestampStart || !timestampStop) { return false; } // Check if region has largest negative if(*timestampStart > *timestampStop && *timestampStart - *timestampStop > largestNegative) { if(*timestampStop > metadata.coreMiniTimestamp || extractionSettings.parseOldRecords) { largestNegative = *timestampStart - *timestampStop; largestNegativeIndex = i; } } // Track smallest start timestamp for edge case if(*timestampStart < smallestStartTimestamp) { if(*timestampStart >= metadata.coreMiniTimestamp || extractionSettings.parseOldRecords) { smallestStartTimestamp = *timestampStart; smallestStartTimestampIndex = i; } } } if(largestNegative == 0) { // We did not find a switch between large and small timestamps within a region. // Therefore the smallest timestamp is the first record within one of the regions. uint64_t location = start + smallestStartTimestampIndex * regionSize; const auto& bytesRead = readLogicalDisk(location, buffer.data(), Disk::SectorSize); if(!bytesRead || *bytesRead < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } std::shared_ptr record; parser.getRecordFromBytes(buffer.data(), Disk::SectorSize, record); if(record) { firstRecord = record; firstOffset = location; return true; } report(APIEvent::Type::VSAByteParseFailure, APIEvent::Severity::Error); return false; } // Set start and stop to be start and stop of largest negative region start += largestNegativeIndex * regionSize; stop = (largestNegativeIndex != RegionCount - 1) ? start + regionSize : stop; } // Initialize variables for iterative search auto smallestTimestampLocation = UINT64_MAX; auto smallestTimestamp = UINT64_MAX; size_t regionSize = static_cast(stop - start - ((stop - start) % Disk::SectorSize)); buffer.resize(regionSize); const auto& bytesRead = readLogicalDisk(start, buffer.data(), regionSize); if(!bytesRead || *bytesRead < regionSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } std::shared_ptr minRecord; // Iteratively find sector with smallest timestamp from largest negative region for(size_t offset = 0; offset + VSA::StandardRecordSize < regionSize; offset += VSA::StandardRecordSize) { std::shared_ptr record; auto recordParseStatus = parser.getRecordFromBytes(buffer.data() + offset, Disk::SectorSize, record); if(recordParseStatus == VSAParser::RecordParseStatus::ConsecutiveExtended) { // Backtrack to get timestamp if first record is consecutive extended record auto extendedRecord = std::dynamic_pointer_cast(record); auto pos = start + offset; if(!findFirstExtendedVSAFromConsecutive(extendedRecord, pos, parser, metadata)) { // Possibly erased by looped buffer continue; } uint64_t timestamp = extendedRecord->getTimestamp(); if(timestamp < smallestTimestamp) { smallestTimestampLocation = pos; smallestTimestamp = timestamp; minRecord = extendedRecord; } } else if(record) { // Update data tracking record with minimum timestamp uint64_t timestamp = record->getTimestamp(); if(timestamp < smallestTimestamp) { smallestTimestampLocation = offset + start; smallestTimestamp = timestamp; minRecord = record; } } } if(smallestTimestamp == UINT64_MAX || !minRecord) { report(APIEvent::Type::VSATimestampNotFound, APIEvent::Severity::Error); return false; } firstOffset = smallestTimestampLocation; firstRecord = minRecord; return true; } bool Device::findLastVSARecord(uint64_t& lastOffset, std::shared_ptr& lastRecord, const VSAExtractionSettings& extractionSettings, std::optional optMetadata) { static constexpr auto LinearSearchSize = 0x8000u; // Grab important metadata features if metadata not defined VSAMetadata metadata; if(optMetadata) { metadata = *optMetadata; } else { const auto& coreMiniTimestamp = getCoreMiniScriptTimestamp(); if(!coreMiniTimestamp) { return false; } metadata.coreMiniTimestamp = *coreMiniTimestamp; const auto& diskSize = getVSADiskSize(); if(!diskSize) { return false; } metadata.diskSize = *diskSize; const auto& isOverlapped = isVSAOverlapped(metadata); if(!isOverlapped) { return false; } metadata.isOverlapped = *isOverlapped; } // Find record prior to first (chronological) record if VSA buffer is overlapped VSAParser parser(report); if(metadata.isOverlapped) { uint64_t firstOffset; std::shared_ptr firstRecord; if(metadata.firstRecordLocation != UINT64_MAX && metadata.firstRecord) { firstOffset = metadata.firstRecordLocation; } else if(!findFirstVSARecord(firstOffset, firstRecord, extractionSettings, metadata)) { return false; } std::vector buffer; buffer.resize(Disk::SectorSize); // Read sector before first if buffer is looped const auto& bytesRead = vsaReadLogicalDisk(firstOffset - Disk::SectorSize, buffer.data(), Disk::SectorSize, metadata); if(!bytesRead || *bytesRead < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } uint16_t bufferOffset = Disk::SectorSize; do { bufferOffset -= VSA::StandardRecordSize; const auto& lastRecordStatus = parser.getRecordFromBytes(buffer.data() + bufferOffset, VSA::StandardRecordSize, lastRecord); if(lastRecordStatus == VSAParser::RecordParseStatus::Success) { lastOffset = firstOffset - Disk::SectorSize + bufferOffset; return true; } else if(lastRecordStatus != VSAParser::RecordParseStatus::NotARecordStart) { // Reverse search for record with valid timestamp // Necessary if previous record is a pad record or consecutive extended record auto pos = firstOffset - bufferOffset; if(findPreviousRecordWithTimestamp(lastRecord, pos, parser)) { lastOffset = pos; return true; } return true; } } while(bufferOffset > 0); lastRecord = nullptr; report(APIEvent::Type::VSAOtherError, APIEvent::Severity::Error); return false; } // Binary search for last record until region is less than LinearSearchSize std::vector buffer; buffer.resize(Disk::SectorSize); uint64_t left = VSA::RecordStartOffset; uint64_t right = metadata.diskSize; while(right - left > LinearSearchSize) { uint64_t pos = (left + right) / 2; pos -= pos % Disk::SectorSize; const auto& bytesRead = readLogicalDisk(pos, buffer.data(), Disk::SectorSize); if(!bytesRead || *bytesRead < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } std::shared_ptr testRecord; const auto& testStatus = parser.getRecordFromBytes(buffer.data(), Disk::SectorSize, testRecord); if(testStatus == VSAParser::RecordParseStatus::NotARecordStart) { right = pos; } else if(testStatus == VSAParser::RecordParseStatus::ConsecutiveExtended) { auto extendedRecord = std::dynamic_pointer_cast(testRecord); auto tempPos = pos; if(!findFirstExtendedVSAFromConsecutive(extendedRecord, tempPos, parser, metadata)) { tempPos = pos; if(!findPreviousRecordWithTimestamp(testRecord, tempPos, parser)) { return false; } if(testRecord->getTimestamp() > metadata.coreMiniTimestamp || extractionSettings.parseOldRecords) { left = tempPos; } else { right = tempPos; } } if(extendedRecord->getTimestamp() > metadata.coreMiniTimestamp || extractionSettings.parseOldRecords) { left = tempPos; } else { right = tempPos; } } else { if(testRecord->getTimestamp() > metadata.coreMiniTimestamp || extractionSettings.parseOldRecords) { left = pos; } else { right = pos; } } } buffer.resize(static_cast(LinearSearchSize)); const auto& bytesRead = readLogicalDisk(left, buffer.data(), LinearSearchSize); if(!bytesRead || *bytesRead < LinearSearchSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } std::shared_ptr previousRecord; auto previousParseStatus = parser.getRecordFromBytes(buffer.data(), LinearSearchSize, previousRecord); uint64_t previousParseLocation = left; uint64_t bufferOffset = VSA::StandardRecordSize; // Ensure we have a record with a valid timestamp as the previousParseResult while(previousParseStatus != VSAParser::RecordParseStatus::Success) { if(bufferOffset >= LinearSearchSize) { // Never found a record with a valid timestamp (unlikely) report(APIEvent::Type::VSAOtherError, APIEvent::Severity::Error); return false; } previousParseStatus = parser.getRecordFromBytes(buffer.data() + bufferOffset, static_cast(LinearSearchSize - bufferOffset), previousRecord); previousParseLocation = left + bufferOffset; bufferOffset += VSA::StandardRecordSize; } // Perform linear search for last record while(bufferOffset < LinearSearchSize) { std::shared_ptr record; auto parseStatus = parser.getRecordFromBytes(buffer.data() + bufferOffset, static_cast(LinearSearchSize - bufferOffset), record); if(parseStatus == VSAParser::RecordParseStatus::NotARecordStart) { // We found the end of the VSA buffer // Return the last valid record we found lastOffset = previousParseLocation; lastRecord = previousRecord; return true; } else if( parseStatus == VSAParser::RecordParseStatus::Success && record->getTimestamp() < metadata.coreMiniTimestamp && !extractionSettings.parseOldRecords ) { // We have entered outdated record data lastOffset = previousParseLocation; lastRecord = previousRecord; return true; } else if(parseStatus == VSAParser::RecordParseStatus::Success) { // Save new last-record data and update bufferOffset according to record size previousParseStatus = parseStatus; previousRecord = record; previousParseLocation = left + bufferOffset; bufferOffset += (record->getType() == VSA::Type::AA6A) ? Disk::SectorSize : VSA::StandardRecordSize; } else { bufferOffset += VSA::StandardRecordSize; } } report(APIEvent::Type::VSAOtherError, APIEvent::Severity::Error); return false; // Somehow we did not find any non-record data despite non-overlapped buffer } std::optional Device::isVSAOverlapped(std::optional optMetadata) { // Grab important metadata features if metadata not defined VSAMetadata metadata; if(!optMetadata) { const auto& diskSize = getVSADiskSize(); if(!diskSize) { return std::nullopt; } metadata.diskSize = *diskSize; const auto& coreMiniTimestamp = getCoreMiniScriptTimestamp(); if(!coreMiniTimestamp) { return std::nullopt; } metadata.coreMiniTimestamp = *coreMiniTimestamp; } else { metadata = *optMetadata; } // Read first sector VSAParser parser(report); std::vector buffer; buffer.resize(Disk::SectorSize); const auto& bytesReadFirst = readLogicalDisk(VSA::RecordStartOffset, buffer.data(), Disk::SectorSize); if(!bytesReadFirst || *bytesReadFirst < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return std::nullopt; } std::shared_ptr firstRecord; auto firstRecordStatus = parser.getRecordFromBytes(buffer.data(), Disk::SectorSize, firstRecord); if(firstRecordStatus == VSAParser::RecordParseStatus::NotARecordStart) { report(APIEvent::Type::VSABufferCorrupted, APIEvent::Severity::Error); return std::nullopt; // Beginning of buffer is not a record } else if(firstRecordStatus == VSAParser::RecordParseStatus::ConsecutiveExtended) { return true; // The only way to have a consecutive extended record at the beginning is if the buffer looped } // Read last sector uint64_t lastPos = metadata.diskSize - Disk::SectorSize; lastPos -= lastPos % Disk::SectorSize; const auto& bytesReadLast = readLogicalDisk(lastPos, buffer.data(), Disk::SectorSize); if(!bytesReadLast || *bytesReadLast < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return std::nullopt; } std::shared_ptr lastSectorRecord; auto lastSectorStatus = parser.getRecordFromBytes(buffer.data(), Disk::SectorSize, lastSectorRecord); if(lastSectorStatus == VSAParser::RecordParseStatus::ConsecutiveExtended) { // Find the beginning record of the extended record sequence auto extendedRecord = std::dynamic_pointer_cast(lastSectorRecord); if(!findFirstExtendedVSAFromConsecutive(extendedRecord, lastPos, parser, metadata)) { return std::nullopt; } return firstRecord->getTimestamp() >= extendedRecord->getTimestamp() && extendedRecord->getTimestamp() > metadata.coreMiniTimestamp; } else if(firstRecord && lastSectorRecord && firstRecord->isTimestampValid() && lastSectorRecord->isTimestampValid()) { // Handle situation where both first and last records have valid timestamps return firstRecord->getTimestamp() >= lastSectorRecord->getTimestamp() && lastSectorRecord->getTimestamp() > metadata.coreMiniTimestamp; } else if(lastSectorStatus == VSAParser::RecordParseStatus::NotARecordStart) { // The vsa record buffer is not full report(APIEvent::Type::VSAOtherError, APIEvent::Severity::Error); return false; } report(APIEvent::Type::VSABufferFormatError, APIEvent::Severity::Error); return std::nullopt; } bool Device::findFirstExtendedVSAFromConsecutive(std::shared_ptr& record, uint64_t& pos, VSAParser& parser, std::optional optMetadata) { static constexpr auto MaxReadAttempts = 10; VSAMetadata metadata; if(!optMetadata) { const auto& diskSize = getVSADiskSize(); if(!diskSize) { return false; } metadata.diskSize = *diskSize; } else { metadata = *optMetadata; } // Reverse iteratively search from given pos for first record in sequence const auto& index = record->getIndex(); const auto& seqNum = record->getSequenceNum(); pos -= (index - 1) * VSA::StandardRecordSize; std::vector buffer; buffer.resize(Disk::SectorSize); uint16_t readCount = 0; while(readCount < MaxReadAttempts) { const auto& bytesRead = vsaReadLogicalDisk(pos, buffer.data(), Disk::SectorSize, metadata); if(!bytesRead || *bytesRead < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } std::shared_ptr possibleFirstRecord; const auto& possibleFirstStatus = parser.getRecordFromBytes(buffer.data(), Disk::SectorSize, possibleFirstRecord); std::shared_ptr extendedVSA; if(possibleFirstStatus == VSAParser::RecordParseStatus::NotARecordStart) { // Most likely either null bytes or the middle of an AA6A record pos -= VSA::StandardRecordSize; readCount++; continue; } extendedVSA = std::dynamic_pointer_cast(possibleFirstRecord); if(!extendedVSA) { // Record is not an extended message record pos -= VSA::StandardRecordSize; readCount++; continue; } if(possibleFirstStatus == VSAParser::RecordParseStatus::Success && extendedVSA->getSequenceNum() == seqNum) { // Found the desired record record = extendedVSA; return true; } else if(possibleFirstStatus == VSAParser::RecordParseStatus::ConsecutiveExtended) { if(seqNum != extendedVSA->getSequenceNum()) { // Another extended record sequence is intermixed with the one we want pos -= VSA::StandardRecordSize; } else { // Traverse backwards the minimum amount the first record must be behind extendedVSA pos -= extendedVSA->getIndex() * VSA::StandardRecordSize; } } readCount++; } report(APIEvent::Type::VSAMaxReadAttemptsReached, APIEvent::Severity::Error); return false; } bool Device::findPreviousRecordWithTimestamp(std::shared_ptr& record, uint64_t& pos, VSAParser& parser) { static constexpr uint16_t MaxReadAttempts = 100; static constexpr uint64_t ReadSize = 0x1000; std::vector buffer; buffer.resize(ReadSize); uint16_t readCount = 0; while(readCount < MaxReadAttempts) { pos -= ReadSize; const auto& bytesRead = vsaReadLogicalDisk(pos, buffer.data(), ReadSize); if(!bytesRead || *bytesRead < ReadSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } // Reverse search through buffer for a record with a valid timestamp for (size_t offsetBufferEnd = VSA::StandardRecordSize; offsetBufferEnd < ReadSize; offsetBufferEnd += VSA::StandardRecordSize) { const auto& parseStatus = parser.getRecordFromBytes(buffer.data() + ReadSize - offsetBufferEnd, offsetBufferEnd, record); if(parseStatus == VSAParser::RecordParseStatus::Success) { pos -= offsetBufferEnd; return true; } } readCount++; } report(APIEvent::Type::VSAMaxReadAttemptsReached, APIEvent::Severity::Error); return false; // Exit if record not found within readCount number of reads } bool Device::findVSAOffsetFromTimepoint(ICSClock::time_point point, uint64_t& vsaOffset, std::shared_ptr& record, const VSAExtractionSettings& extractionSettings, std::optional optMetadata) { static constexpr uint64_t LinearSearchTickDifference = 100000ull; // The maximum number of ticks offset from point at which to do a linear search // Grab important metadata features if metadata not defined VSAMetadata metadata; if(!optMetadata) { if(!probeVSA(metadata, extractionSettings)) { return false; } } else { metadata = *optMetadata; } if(metadata.diskSize == 0) { // Disk size is unknown report(APIEvent::Type::RequiredParameterNull, APIEvent::Severity::Error); return false; } uint64_t firstOffset = metadata.firstRecordLocation; // Offset of the first record chronologically const auto& desiredTimestamp = VSA::getICSTimestampFromTimepoint(point); VSAParser parser(report); std::vector buffer; buffer.resize(Disk::SectorSize); if(desiredTimestamp <= metadata.firstRecord->getTimestamp()) { // Timestamp is less than first timestamp so we just return the first vsaOffset = metadata.firstRecordLocation; record = metadata.firstRecord; return true; } if(desiredTimestamp > metadata.lastRecord->getTimestamp()) { report(APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::Error); return false; } // Handle situation where first offset is not on sector boundary // Find smallest tick difference between record in sector and desired timestamp uint64_t bestTickDiff = UINT64_MAX; std::shared_ptr bestRecord = nullptr; uint64_t bestOffset = UINT64_MAX; auto findBestTickDiff = [&] (uint64_t readPos, uint64_t readSize) { for(uint64_t i = 0; i < Disk::SectorSize; i += VSA::StandardRecordSize) { std::shared_ptr testRecord; const auto& testRecordStatus = parser.getRecordFromBytes(buffer.data() + i, static_cast(readSize - i), testRecord); if(testRecordStatus == VSAParser::RecordParseStatus::Success) { const auto& testTimestamp = testRecord->getTimestamp(); uint64_t tickDiff = (testTimestamp > desiredTimestamp) ? testTimestamp - desiredTimestamp : desiredTimestamp - testTimestamp; if(tickDiff < bestTickDiff) { bestTickDiff = tickDiff; bestOffset = readPos + i; bestRecord = testRecord; } } } }; if(firstOffset % Disk::SectorSize != 0) { // First record is not located at the beginning of a sector // Find the best tick diff for the sector that the first record is in // and ignore it during the binary search to allow for binary searching sectors, not bytes. // Compare the bestTickDiff from the firstSector to results during the linear search to // find the true best tick diff. uint64_t readPos = firstOffset - (firstOffset % Disk::SectorSize); const auto& bytesRead = vsaReadLogicalDisk(readPos, buffer.data(), Disk::SectorSize, metadata); if(!bytesRead || *bytesRead < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } findBestTickDiff(readPos, Disk::SectorSize); // Change the first offset to eliminate the first sector from the binary search firstOffset = firstOffset + Disk::SectorSize - (firstOffset % Disk::SectorSize); } uint64_t leftIndex = 0; // Index of the leftmost sector (not the byte) uint64_t rightIndex = ((metadata.isOverlapped) ? metadata.diskSize : metadata.bufferEnd) - VSA::RecordStartOffset; rightIndex = (rightIndex / Disk::SectorSize); uint64_t midIndex; // Index of the middle sector (not the byte) // Indices indicate sector offset from first sector location in metadata while(leftIndex < rightIndex) { // Perform binary search for records with timestamp closest to the desired timestamp midIndex = (rightIndex + leftIndex) / 2; uint64_t readPos = firstOffset + midIndex * Disk::SectorSize; const auto& bytesRead = vsaReadLogicalDisk(readPos, buffer.data(), Disk::SectorSize, metadata); if(!bytesRead || *bytesRead < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } std::shared_ptr midRecord; auto midRecordStatus = parser.getRecordFromBytes(buffer.data(), Disk::SectorSize, midRecord); switch(midRecordStatus) { case VSAParser::RecordParseStatus::NotARecordStart: // This part of the buffer does not contain records rightIndex = midIndex - 1; continue; case VSAParser::RecordParseStatus::ConsecutiveExtended: // We dropped in the middle of an extended message record auto extendedRecord = std::dynamic_pointer_cast(midRecord); uint64_t pos = readPos; if(!findFirstExtendedVSAFromConsecutive(extendedRecord, pos, parser, metadata)) { pos = firstOffset + midIndex * Disk::SectorSize; if(!findPreviousRecordWithTimestamp(midRecord, pos, parser)) { return false; } midIndex = (pos - firstOffset) / Disk::SectorSize; break; } midIndex = (pos - firstOffset) / Disk::SectorSize; midRecord = extendedRecord; break; } if(midIndex <= leftIndex) { // Extended records cause problems with binary search // Just move on to linear search leftIndex = midIndex; break; } if(midIndex > rightIndex) { // Extended records cause problems with binary search // Just move on to linear search rightIndex = midIndex; break; } if(!midRecord || !midRecord->isTimestampValid()) { // Unhandled failure to get timestamp report(APIEvent::Type::VSATimestampNotFound, APIEvent::Severity::Error); return false; } if(midRecord->getTimestamp() == desiredTimestamp) { vsaOffset = firstOffset + midIndex * Disk::SectorSize; record = midRecord; return true; } if(midRecord->getTimestamp() < desiredTimestamp && midRecord->getTimestamp() > desiredTimestamp - LinearSearchTickDifference) { // The timestamp of this sector is within desired linear search range const uint64_t LinearReadByteAmount = 0x1000ull; // Number of bytes to read for linear search uint64_t pos = firstOffset + midIndex * Disk::SectorSize; const auto& recordBufferSize = metadata.diskSize - VSA::RecordStartOffset; pos -= ((pos - VSA::RecordStartOffset) / recordBufferSize) * recordBufferSize; // Move pos to within physical space of record buffer uint64_t lastTickDiff = UINT64_MAX; buffer.resize(LinearReadByteAmount); // Begin linear search for record with closest timestamp to desired while(lastTickDiff < LinearSearchTickDifference || lastTickDiff == UINT64_MAX) { const auto& bytesReadLinear = vsaReadLogicalDisk(pos, buffer.data(), LinearReadByteAmount, metadata); if(!bytesReadLinear || *bytesReadLinear < LinearReadByteAmount) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } for(uint64_t i = 0; i < LinearReadByteAmount; i += VSA::StandardRecordSize) { std::shared_ptr testRecord; const auto& testRecordStatus = parser.getRecordFromBytes(buffer.data() + i, static_cast(LinearReadByteAmount - i), testRecord); if(testRecordStatus == VSAParser::RecordParseStatus::Success) { const auto& testTimestamp = testRecord->getTimestamp(); uint64_t tickDiff = (testTimestamp > desiredTimestamp) ? testTimestamp - desiredTimestamp : desiredTimestamp - testTimestamp; if(tickDiff < bestTickDiff) { bestTickDiff = tickDiff; bestRecord = testRecord; bestOffset = pos + i; } lastTickDiff = tickDiff; if(testRecord->getType() == VSA::Type::AA6A) { // Increment to skip the whole record if it is a full sector length record i += Disk::SectorSize - VSA::StandardRecordSize; } } else if(testRecordStatus == VSAParser::RecordParseStatus::NotARecordStart) { // We have reached an area with no records break; } else { lastTickDiff = UINT64_MAX; } } pos += LinearReadByteAmount; if(!metadata.isOverlapped && pos > metadata.bufferEnd) { // Ran out of valid records to search break; } if(metadata.isOverlapped && pos >= metadata.firstRecordLocation && pos - LinearReadByteAmount < metadata.firstRecordLocation) { // Ran out of valid records to search break; } } // Return the location with the closest timestamp to the desired point vsaOffset = bestOffset; record = bestRecord; return true; } else if(midRecord->getTimestamp() < desiredTimestamp) { // Keep right partition leftIndex = midIndex; } else { // Keep left partition rightIndex = midIndex; } } // Nothing found within desired linear search tick range // Check for closest record to desired timestamp if(leftIndex <= rightIndex) { buffer.clear(); uint64_t pos = firstOffset + leftIndex * Disk::SectorSize; uint64_t bufferSize = (rightIndex - leftIndex + 1) * Disk::SectorSize; buffer.resize(static_cast(bufferSize)); const auto& bytesRead = vsaReadLogicalDisk(pos, buffer.data(), Disk::SectorSize, metadata); if(!bytesRead || *bytesRead < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } findBestTickDiff(pos, bufferSize); vsaOffset = bestOffset; record = bestRecord; return true; } report(APIEvent::Type::VSAOtherError, APIEvent::Severity::Error); return false; } std::optional Device::getVSATimestampOrBefore(VSAParser& parser, std::vector& buffer, uint64_t pos, uint64_t minPos, std::optional optMetadata) { VSAMetadata metadata; if(!optMetadata) { const auto& diskSize = getVSADiskSize(); if(!diskSize) { return std::nullopt; } metadata.diskSize = *diskSize; } else { metadata = *optMetadata; } while(pos >= minPos) { const auto& bytesRead = readLogicalDisk(pos, buffer.data(), Disk::SectorSize); if(!bytesRead || *bytesRead < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return std::nullopt; } std::shared_ptr record; auto parseStatus = parser.getRecordFromBytes(buffer.data(), Disk::SectorSize, record); // Handle situations where we were dropped in the middle of an extended record if(parseStatus == VSAParser::RecordParseStatus::ConsecutiveExtended) { auto extendedRecord = std::dynamic_pointer_cast(record); auto tempPos = pos; // If the first-extended search fails, we still have the original value of the search start location if(!findFirstExtendedVSAFromConsecutive(extendedRecord, tempPos, parser, metadata)) { // Unoptimized/Unloopable search if(!findPreviousRecordWithTimestamp(record, pos, parser)) { return std::nullopt; } return record->getTimestamp(); } return extendedRecord->getTimestamp(); } // Handle other situations without valid timestamp if(parseStatus != VSAParser::RecordParseStatus::Success) { pos -= VSA::StandardRecordSize; } else { return record->getTimestamp(); } } report(APIEvent::Type::VSATimestampNotFound, APIEvent::Severity::Error); return std::nullopt; } std::optional Device::getVSATimestampOrAfter(VSAParser& parser, std::vector& buffer, uint64_t pos, uint64_t maxPos, std::optional optMetadata) { VSAMetadata metadata; if(!optMetadata) { const auto& diskSize = getVSADiskSize(); if(!diskSize) { return std::nullopt; } metadata.diskSize = *diskSize; } else { metadata = *optMetadata; } while(pos <= maxPos) { const auto& bytesRead = readLogicalDisk(pos, buffer.data(), Disk::SectorSize); if(!bytesRead || *bytesRead < Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return std::nullopt; } std::shared_ptr record; auto parseStatus = parser.getRecordFromBytes(buffer.data(), Disk::SectorSize, record); if(parseStatus == VSAParser::RecordParseStatus::ConsecutiveExtended) { auto extendedRecord = std::dynamic_pointer_cast(record); auto tempPos = pos; // If the first-extended search fails, we still have the original value of the search start location if(!findFirstExtendedVSAFromConsecutive(extendedRecord, tempPos, parser, metadata)) { pos += VSA::StandardRecordSize; continue; } return extendedRecord->getTimestamp(); } // Other situations with invalid timestamp if(parseStatus != VSAParser::RecordParseStatus::Success) { pos += VSA::StandardRecordSize; } else { return record->getTimestamp(); } } report(APIEvent::Type::VSATimestampNotFound, APIEvent::Severity::Error); return std::nullopt; } bool Device::parseVSA( VSAMetadata& metadata, const VSAExtractionSettings& extractionSettings, const VSAMessageReadFilter& filter ) { static constexpr uint64_t MaxReadAmountPerIteration = 0x10000; // Read 512 KB per iteration static constexpr uint64_t MaxReadFailuresAllowed = 10; if(filter.readRange.first > filter.readRange.second) { // First timestamp occurs after second timestamp in filter // Do not fail to allow for other filters to do work report(APIEvent::Type::ParameterOutOfRange, APIEvent::Severity::EventWarning); return true; } // Location of first time_point from the filter uint64_t readOffset; std::shared_ptr recordAtOffset; if(!findVSAOffsetFromTimepoint(filter.readRange.first, readOffset, recordAtOffset, extractionSettings, metadata)) { report(APIEvent::Type::VSATimestampNotFound, APIEvent::Severity::Error); return false; } if(readOffset >= metadata.diskSize) { readOffset -= metadata.diskSize - VSA::RecordStartOffset; } std::vector buffer; bool success = true; bool moreToRead = true; VSAParser::Settings parserSettings = VSAParser::Settings::messageRecords(); VSAParser parser(report, parserSettings); parser.setMessageFilter(filter); while(moreToRead) { uint64_t amount; if(!metadata.isOverlapped) { moreToRead = readOffset + MaxReadAmountPerIteration < metadata.bufferEnd; amount = std::min(MaxReadAmountPerIteration, metadata.bufferEnd - readOffset); } else { uint64_t readEnd = readOffset + MaxReadAmountPerIteration; if(readEnd > metadata.diskSize) { // Make sure read end is within the buffer for a possible looped read readEnd -= metadata.diskSize - VSA::RecordStartOffset; } moreToRead = !(readOffset < metadata.bufferEnd && readEnd >= metadata.bufferEnd); amount = moreToRead ? MaxReadAmountPerIteration : metadata.bufferEnd - readOffset; } if(amount < VSA::StandardRecordSize) { break; } buffer.resize(static_cast(amount)); uint64_t readAttempt = 1; while(true) { const auto& bytesRead = vsaReadLogicalDisk(readOffset, buffer.data(), amount, metadata); if(bytesRead && *bytesRead == amount) { break; } if(readAttempt >= MaxReadFailuresAllowed) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return false; } report(APIEvent::Type::FailedToRead, APIEvent::Severity::EventWarning); ++readAttempt; } success = parser.parseBytes(buffer.data(), amount); if(!success) { report(APIEvent::Type::VSAByteParseFailure, APIEvent::Severity::Error); return false; } if(parser.empty()) { // No message records or all extended message records were not terminated in block readOffset += amount; continue; } const auto& lastRecord = parser.back(); if(lastRecord->getTimestampICSClock() >= filter.readRange.second) { // Reached the desired end timestamp moreToRead = false; } if(!dispatchVSAMessages(parser)) { // Piecemeal dispatch messages so we don't have exceptionally large vectors return false; } readOffset += amount; } return success; } std::optional Device::vsaReadLogicalDisk(uint64_t pos, uint8_t* into, uint64_t amount, std::optional metadata) { uint64_t diskSize; if(metadata) { diskSize = metadata->diskSize; } else { const auto& testDiskSize = getVSADiskSize(); if(!testDiskSize) { return std::nullopt; } diskSize = *testDiskSize; } // Set the position to be within the ring buffer if(pos < VSA::RecordStartOffset) { pos = diskSize - (VSA::RecordStartOffset - pos); } else if(pos >= diskSize) { const auto& bufferSize = diskSize - VSA::RecordStartOffset; const auto& timesLooped = (pos - VSA::RecordStartOffset) / bufferSize; pos -= bufferSize * timesLooped; } if(amount > diskSize - VSA::RecordStartOffset) { // Given read amount is too large amount = diskSize - VSA::RecordStartOffset; // Do full disk dump } if(pos + amount < diskSize) { // Read doesn't need to loop return readLogicalDisk(pos, into, amount); } uint64_t firstReadAmount = diskSize - pos; if(!readLogicalDisk(pos, into, firstReadAmount)) { return std::nullopt; } return readLogicalDisk(VSA::RecordStartOffset, into + firstReadAmount, amount - firstReadAmount); } bool Device::dispatchVSAMessages(VSAParser& parser) { std::vector> packets; if(!parser.extractMessagePackets(packets)) { report(APIEvent::Type::VSAByteParseFailure, APIEvent::Severity::Error); return false; } for(const auto& packet : packets) { std::shared_ptr msg; if(!com->decoder->decode(msg, packet)) { return false; } com->dispatchMessage(msg); } return true; } std::optional Device::getCoreMiniScriptTimestamp() { static constexpr auto CoreMiniTimestampLocation = 48; static constexpr auto CoreMiniTimestampSize = 8; uint8_t buffer[CoreMiniTimestampSize]; const auto& numBytes = readLogicalDisk(CoreMiniTimestampLocation, buffer, CoreMiniTimestampSize); if(!numBytes || *numBytes < CoreMiniTimestampSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return std::nullopt; } return *reinterpret_cast(buffer); } std::optional Device::getVSADiskSize() { uint64_t diskSize; auto scriptStatus = getScriptStatus(); if(!scriptStatus) { return std::nullopt; } if(!scriptStatus->isCoreminiRunning) { startScript(); scriptStatus = getScriptStatus(); if(!scriptStatus) { return std::nullopt; } diskSize = (scriptStatus->maxSector + 1) * Disk::SectorSize; stopScript(); } else { diskSize = (scriptStatus->maxSector + 1) * Disk::SectorSize; } if(diskSize == Disk::SectorSize) { report(APIEvent::Type::FailedToRead, APIEvent::Severity::Error); return std::nullopt; } return diskSize; } bool Device::requestTC10Wake(Network::NetID network) { if(!supportsTC10()) { report(APIEvent::Type::NotSupported, APIEvent::Severity::Error); return false; } std::vector args(sizeof(network)); *(Network::NetID*)args.data() = network; auto msg = com->waitForMessageSync([&] { return com->sendCommand(ExtendedCommand::RequestTC10Wake, args); }, std::make_shared(Message::Type::ExtendedResponse), std::chrono::milliseconds(1000)); if(!msg) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return false; } auto resp = std::static_pointer_cast(msg); if(!resp) { report(APIEvent::Type::UnexpectedResponse, APIEvent::Severity::Error); return false; } return resp->response == ExtendedResponse::OK; } bool Device::requestTC10Sleep(Network::NetID network) { if(!supportsTC10()) { report(APIEvent::Type::NotSupported, APIEvent::Severity::Error); return false; } std::vector args(sizeof(network)); *(Network::NetID*)args.data() = network; auto msg = com->waitForMessageSync([&] { return com->sendCommand(ExtendedCommand::RequestTC10Sleep, args); }, std::make_shared(Message::Type::ExtendedResponse), std::chrono::milliseconds(1000)); if(!msg) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return false; } auto typed = std::static_pointer_cast(msg); if(!typed) { report(APIEvent::Type::UnexpectedResponse, APIEvent::Severity::Error); return false; } return typed->response == ExtendedResponse::OK; } std::optional Device::getTC10Status(Network::NetID network) { if(!supportsTC10()) { report(APIEvent::Type::NotSupported, APIEvent::Severity::Error); return std::nullopt; } std::vector args(sizeof(network)); *(Network::NetID*)args.data() = network; auto msg = com->waitForMessageSync([&] { return com->sendCommand(ExtendedCommand::GetTC10Status, args); }, std::make_shared(Message::Type::TC10Status), std::chrono::milliseconds(1000)); if(!msg) { report(APIEvent::Type::NoDeviceResponse, APIEvent::Severity::Error); return std::nullopt; } auto typed = std::static_pointer_cast(msg); if(!typed) { report(APIEvent::Type::UnexpectedResponse, APIEvent::Severity::Error); return std::nullopt; } return *typed; }