diff --git a/HISTORY.md b/HISTORY.md index 44547d6..b3149b3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -170,8 +170,33 @@ Records key decisions, structural changes, and completed development stages. - Updated CMake source lists, include directories, and dependent includes for the new layout. +### Stage 3 - USB timeout and error recovery + +- Reject short USB writes and responses that do not contain the minimum data + required by the protocol operation. +- Stop each capture-state transaction at its first failed transfer instead of + issuing the remaining USB operations with invalid state. +- Retry transient timeouts and I/O errors with bounded attempts and delays. +- Stop acquisition immediately when libusb reports that the device was lost. +- Join an acquisition worker before releasing a lost device's USB resources. +- Keep a connected device available for another Start attempt after repeated + recoverable transfer errors. +- Report recovery, terminal I/O errors, and device loss through application + states in the status line. +- Removed the temporary poll/error counters and per-cycle stderr diagnostics. +- Verified on physical hardware that unplugging the device during acquisition + stops acquisition and closes the connection. +- Added periodic presence checks outside acquisition so an unplugged connected + device is detected and a returned device appears without a manual rescan. +- Removed the redundant manual rescan control and kept the device-disconnected + status visible until the device returns. +- Verified on physical hardware that idle disconnection is detected, the + disconnected status remains visible, and active acquisition behavior is + unchanged. + ### Next USB tasks - Decode capture-state responses and acquired sample packets. - Add buffering between USB reads and waveform processing. -- Add automatic recovery for transfer errors and unexpected disconnections. +- Add deterministic fault-injection tests for timeout and transfer-error + recovery, then verify recovery with a connected physical device. diff --git a/README.md b/README.md index 49332c8..19c2563 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,20 @@ path for the Hantek DSO-2250: - discovery and connection through libusb; - FX2 firmware upload and operational-device re-enumeration; - a Start/Stop-controlled acquisition thread that polls the bulk endpoints; -- poll and transfer-error counters in the status line. +- bounded recovery from USB timeouts and I/O errors; +- safe acquisition shutdown and status reporting when the device is lost. The application starts in Live mode when a supported device is present and in Demo mode otherwise. Connecting prepares the device; endpoint traffic begins -only after pressing Start and stops after pressing Stop. +only after pressing Start and stops after pressing Stop. Transient USB errors +are retried with a bounded delay. Repeated errors stop acquisition while +keeping an available device connected for another Start attempt. Disconnecting +the device during acquisition stops the worker before USB resources are +released and reports the device loss in the status line. Outside acquisition, +the application periodically checks device presence: unplugging a connected +device closes the stale connection, and plugging it back in updates the status +automatically. The disconnect status remains visible while the device is +absent. Reconnecting remains an explicit action. ## Planned Stack diff --git a/app/main.cpp b/app/main.cpp index 6e3bdd4..097ae82 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -76,7 +76,10 @@ #include "usb_device.h" using oscilloscope::capture::SAcquisitionLoop; +using oscilloscope::capture::EAcquisitionOperation; +using oscilloscope::capture::EAcquisitionState; using oscilloscope::usb::EScanStatus; +using oscilloscope::usb::EUsbTransferStatus; using oscilloscope::usb::SUsbConnection; using oscilloscope::usb::SUsbConnectionResult; using oscilloscope::usb::SUsbDeviceInfo; @@ -84,6 +87,9 @@ using oscilloscope::usb::SUsbScanResult; /***************************** Private variables *****************************/ +/** @brief Interval between USB presence checks outside acquisition */ +static const uint32_t USB_PRESENCE_INTERVAL_MS = 1000U; + #ifdef __GNUC__ // GCC/MinGW only const char version_info[] __attribute__((section(".version"), used)) = "FileDescription: Oscilloscope application\n" @@ -117,6 +123,10 @@ static std::string formatUsbConnectionError( const char *operation, const SUsbConnectionResult &result ); +static std::string formatAcquisitionError( + EAcquisitionOperation operation, + EUsbTransferStatus transferStatus +); static void updateDemoMode( bool *demoMode, SUsbScanResult *usbScanResult, @@ -193,6 +203,7 @@ int main (void) { bool running = true; bool acquisitionRunning = false; + bool deviceWasDisconnected = false; bool channelEnabled[] = {true, true}; int timebase = 6; int voltsPerDivision[] = {2, 2}; @@ -208,6 +219,8 @@ int main (void) { usbScanResult, usbConnection ); + uint32_t nextUsbPresenceCheck = + SDL_GetTicks() + USB_PRESENCE_INTERVAL_MS; const char* timebases[] = { "4 ns/div", "20 ns/div", "100 ns/div", "1 us/div", "10 us/div", "100 us/div", "1 ms/div", "10 ms/div", "100 ms/div", "1 s/div" @@ -230,6 +243,68 @@ int main (void) { ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); + const EAcquisitionState acquisitionState = + acquisitionLoop.status.state.load(); + if ( + acquisitionRunning && + ((acquisitionState == EAcquisitionState::eDeviceLost) || + (acquisitionState == EAcquisitionState::eFailed)) + ) { + const std::string acquisitionError = formatAcquisitionError( + acquisitionLoop.status.failedOperation.load(), + acquisitionLoop.status.lastTransferStatus.load() + ); + + oscilloscope::capture::joinFinishedAcquisitionLoop( + &acquisitionLoop + ); + acquisitionRunning = false; + if (acquisitionState == EAcquisitionState::eDeviceLost) { + oscilloscope::usb::disconnectFromDevice(&usbConnection); + connectedDevice = {0U, 0U, 0U, 0U, NULL}; + deviceWasDisconnected = true; + deviceStatus = "Device disconnected: " + acquisitionError; + } + else { + deviceStatus = "Acquisition stopped: " + acquisitionError; + } + } + + const uint32_t currentTicks = SDL_GetTicks(); + if ( + !demoMode && + !acquisitionRunning && + (static_cast(currentTicks - nextUsbPresenceCheck) >= 0) + ) { + usbScanResult = oscilloscope::usb::enumerateSupportedDevices(); + nextUsbPresenceCheck = + currentTicks + USB_PRESENCE_INTERVAL_MS; + + if ( + usbConnection.isConnected && + (usbScanResult.status == EScanStatus::eSuccess) && + !isUsbDevicePresent(usbScanResult, connectedDevice) + ) { + oscilloscope::usb::disconnectFromDevice(&usbConnection); + connectedDevice = {0U, 0U, 0U, 0U, NULL}; + deviceWasDisconnected = true; + deviceStatus = "Device disconnected"; + } + else if ( + !usbConnection.isConnected && + (!usbScanResult.devices.empty() || + !deviceWasDisconnected) + ) { + if (!usbScanResult.devices.empty()) { + deviceWasDisconnected = false; + } + deviceStatus = formatUsbConnectionStatus( + usbScanResult, + usbConnection + ); + } + } + if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Exit")) { @@ -309,43 +384,12 @@ int main (void) { &acquisitionLoop, usbConnection ); - } - acquisitionRunning = true; - } - } - ImGui::EndDisabled(); - ImGui::BeginDisabled(usbConnection.isConnected); - if (ImGui::Button("Rescan devices", ImVec2(-1.0f, 32.0f))) { - usbScanResult = oscilloscope::usb::enumerateSupportedDevices(); - - if ( - usbConnection.isConnected && - !isUsbDevicePresent(usbScanResult, connectedDevice) - ) { - oscilloscope::capture::stopAcquisitionLoop(&acquisitionLoop); - acquisitionRunning = false; - const SUsbConnectionResult disconnectResult = - oscilloscope::usb::disconnectFromDevice(&usbConnection); - - connectedDevice = {0U, 0U, 0U, 0U, NULL}; - if (disconnectResult.errorMessage.empty()) { deviceStatus = formatUsbConnectionStatus( usbScanResult, usbConnection ); } - else { - deviceStatus = formatUsbConnectionError( - "Disconnect", - disconnectResult - ); - } - } - else { - deviceStatus = formatUsbConnectionStatus( - usbScanResult, - usbConnection - ); + acquisitionRunning = true; } } ImGui::EndDisabled(); @@ -358,6 +402,7 @@ int main (void) { oscilloscope::usb::disconnectFromDevice(&usbConnection); connectedDevice = {0U, 0U, 0U, 0U, NULL}; + deviceWasDisconnected = false; if (disconnectResult.errorMessage.empty()) { deviceStatus = formatUsbConnectionStatus( usbScanResult, @@ -387,11 +432,25 @@ int main (void) { ); if (connectResult.errorMessage.empty()) { - connectedDevice = usbScanResult.devices.front(); - deviceStatus = formatUsbConnectionStatus( - usbScanResult, - usbConnection - ); + if ( + oscilloscope::usb::getConnectedDeviceInfo( + usbConnection, + &connectedDevice + ) + ) { + deviceWasDisconnected = false; + deviceStatus = formatUsbConnectionStatus( + usbScanResult, + usbConnection + ); + } + else { + oscilloscope::usb::disconnectFromDevice( + &usbConnection + ); + deviceStatus = + "Connect error: Cannot identify USB device"; + } } else { deviceStatus = formatUsbConnectionError( @@ -436,14 +495,16 @@ int main (void) { ImGui::SetCursorScreenPos(statusPosition); ImGui::Text( - "%s | %s | %s | CH1 %s | CH2 %s | polls %lu err %lu", - acquisitionRunning ? "Acquiring" : "Stopped", + "%s | %s | %s | CH1 %s | CH2 %s", + acquisitionRunning + ? (acquisitionLoop.status.state.load() == + EAcquisitionState::eRecovering + ? "Recovering USB connection" : "Acquiring") + : "Stopped", demoMode ? "Demo mode" : "Live mode", deviceStatus.c_str(), channelEnabled[0] ? "on" : "off", - channelEnabled[1] ? "on" : "off", - acquisitionLoop.status.pollCount.load(), - acquisitionLoop.status.errorCount.load() + channelEnabled[1] ? "on" : "off" ); ImGui::End(); @@ -531,6 +592,53 @@ static std::string formatUsbConnectionError( return status; } +static std::string formatAcquisitionError( + const EAcquisitionOperation operation, + const EUsbTransferStatus transferStatus +) { + std::string status; + + switch (transferStatus) { + case EUsbTransferStatus::eTimeout: + status = "USB timeout"; + break; + case EUsbTransferStatus::eNoDevice: + status = "USB device lost"; + break; + case EUsbTransferStatus::eShortTransfer: + status = "Incomplete USB response"; + break; + case EUsbTransferStatus::eError: + status = "USB I/O error"; + break; + case EUsbTransferStatus::eSuccess: + default: + status = "USB acquisition error"; + break; + } + + switch (operation) { + case EAcquisitionOperation::eBeginCommand: + status += " while beginning command"; + break; + case EAcquisitionOperation::eSpeedBeforeCommand: + case EAcquisitionOperation::eSpeedBeforeResponse: + status += " while checking connection speed"; + break; + case EAcquisitionOperation::eCaptureStateCommand: + status += " while sending capture-state command"; + break; + case EAcquisitionOperation::eCaptureStateResponse: + status += " while reading capture state"; + break; + case EAcquisitionOperation::eNone: + default: + break; + } + + return status; +} + static void updateDemoMode( bool *demoMode, SUsbScanResult *usbScanResult, diff --git a/capture/acquisition_loop.cpp b/capture/acquisition_loop.cpp index 572e015..dbb5350 100644 --- a/capture/acquisition_loop.cpp +++ b/capture/acquisition_loop.cpp @@ -21,7 +21,6 @@ */ /******************************* Included files *******************************/ -#include #include #include "acquisition_loop.h" @@ -47,6 +46,10 @@ static const uint8_t CMD_GET_CAPTURE_STATE = 6U; /** @brief Delay between poll cycles; also the effective host-activity rate */ static const unsigned int POLL_INTERVAL_MS = 100U; +static const unsigned int RECOVERY_INTERVAL_MS = 250U; +static const unsigned int TRANSFER_TIMEOUT_MS = 250U; +static const unsigned int TRANSFER_ATTEMPTS = 1U; +static const unsigned int MAX_CONSECUTIVE_FAILURES = 3U; /***************************** Private prototypes *****************************/ @@ -60,6 +63,19 @@ static void pollCaptureState( usb::SUsbConnection connection ); +/** + * @brief Executes one complete capture-state polling transaction + * @param[in] connection USB connection to poll + * @param[out] response Buffer receiving the capture-state response + * @param[out] failedOperation First operation that failed + * @returns Result of the first failed operation or the final successful read + */ +static usb::SUsbTransferResult executePollingTransaction( + const usb::SUsbConnection &connection, + uint8_t *response, + EAcquisitionOperation *failedOperation +); + /********************* Application Programming Interface **********************/ /** @fn startAcquisitionLoop */ @@ -69,13 +85,37 @@ void startAcquisitionLoop( ) { if (loop != NULL) { loop->stopRequested.store(false); - loop->status.pollCount.store(0UL); - loop->status.errorCount.store(0UL); loop->status.lastCaptureState.store(-1); + loop->status.state.store(EAcquisitionState::eRunning); + loop->status.failedOperation.store(EAcquisitionOperation::eNone); + loop->status.lastTransferStatus.store( + usb::EUsbTransferStatus::eSuccess + ); loop->workerThread = std::thread(pollCaptureState, loop, connection); } } +/** @fn joinFinishedAcquisitionLoop */ +bool joinFinishedAcquisitionLoop(SAcquisitionLoop *loop) { + bool joined = false; + EAcquisitionState state = EAcquisitionState::eStopped; + + if (loop != NULL) { + state = loop->status.state.load(); + if ( + (state == EAcquisitionState::eDeviceLost) || + (state == EAcquisitionState::eFailed) + ) { + if (loop->workerThread.joinable()) { + loop->workerThread.join(); + } + joined = true; + } + } + + return joined; +} + /** @fn stopAcquisitionLoop */ void stopAcquisitionLoop(SAcquisitionLoop *loop) { if (loop != NULL) { @@ -83,6 +123,7 @@ void stopAcquisitionLoop(SAcquisitionLoop *loop) { if (loop->workerThread.joinable()) { loop->workerThread.join(); } + loop->status.state.store(EAcquisitionState::eStopped); } } @@ -92,77 +133,137 @@ void stopAcquisitionLoop(SAcquisitionLoop *loop) { static void pollCaptureState( SAcquisitionLoop *loop, usb::SUsbConnection connection +) { + uint8_t response[EP_BULK_IN_MAX_PACKET_LEN]; + usb::SUsbTransferResult transferResult = { + usb::EUsbTransferStatus::eSuccess, 0, "" + }; + EAcquisitionOperation failedOperation = EAcquisitionOperation::eNone; + unsigned int consecutiveFailures = 0U; + unsigned int delayMs = POLL_INTERVAL_MS; + + while (!loop->stopRequested.load()) { + transferResult = executePollingTransaction( + connection, response, &failedOperation + ); + + if (transferResult.status == usb::EUsbTransferStatus::eSuccess) { + loop->status.lastCaptureState.store(static_cast(response[0])); + loop->status.state.store(EAcquisitionState::eRunning); + loop->status.failedOperation.store(EAcquisitionOperation::eNone); + loop->status.lastTransferStatus.store( + usb::EUsbTransferStatus::eSuccess + ); + consecutiveFailures = 0U; + delayMs = POLL_INTERVAL_MS; + } + else { + loop->status.failedOperation.store(failedOperation); + loop->status.lastTransferStatus.store(transferResult.status); + ++consecutiveFailures; + + if (transferResult.status == usb::EUsbTransferStatus::eNoDevice) { + loop->status.state.store(EAcquisitionState::eDeviceLost); + break; + } + else if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { + loop->status.state.store(EAcquisitionState::eFailed); + break; + } + else { + loop->status.state.store(EAcquisitionState::eRecovering); + delayMs = RECOVERY_INTERVAL_MS; + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); + } +} + +/*----------------------------------------------------------------------------*/ + +/** @fn executePollingTransaction */ +static usb::SUsbTransferResult executePollingTransaction( + const usb::SUsbConnection &connection, + uint8_t *response, + EAcquisitionOperation *failedOperation ) { uint8_t beginCommandPayload[10] = {0x0FU, 0x03U, 0x03U, 0x03U, 0U, 0U, 0U, 0U, 0U, 0U}; uint8_t speedBuffer[10]; uint8_t captureStateCommand[2] = {CMD_GET_CAPTURE_STATE, 0U}; - uint8_t response[EP_BULK_IN_MAX_PACKET_LEN]; - usb::SUsbTransferResult beginResult = { - usb::EUsbTransferStatus::eError, 0, "" - }; - usb::SUsbTransferResult speedResult = { - usb::EUsbTransferStatus::eError, 0, "" - }; - usb::SUsbTransferResult writeResult = {usb::EUsbTransferStatus::eError, 0, ""}; - usb::SUsbTransferResult readSpeedResult = { - usb::EUsbTransferStatus::eError, 0, "" + usb::SUsbTransferResult result = { + usb::EUsbTransferStatus::eSuccess, 0, "" }; - usb::SUsbTransferResult readResult = {usb::EUsbTransferStatus::eError, 0, ""}; - while (!loop->stopRequested.load()) { - beginResult = usb::controlWrite( + *failedOperation = EAcquisitionOperation::eBeginCommand; + result = usb::controlWrite( + connection, + CONTROL_BEGINCOMMAND, + beginCommandPayload, + sizeof(beginCommandPayload), + 0U, + 0U, + TRANSFER_TIMEOUT_MS, + TRANSFER_ATTEMPTS + ); + + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = EAcquisitionOperation::eSpeedBeforeCommand; + result = usb::controlRead( connection, - CONTROL_BEGINCOMMAND, - beginCommandPayload, - sizeof(beginCommandPayload) + CONTROL_GETSPEED, + speedBuffer, + sizeof(speedBuffer), + 0U, + 0U, + TRANSFER_TIMEOUT_MS, + TRANSFER_ATTEMPTS, + 1U ); - speedResult = usb::controlRead( - connection, CONTROL_GETSPEED, speedBuffer, sizeof(speedBuffer) - ); - writeResult = usb::bulkWrite( + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = EAcquisitionOperation::eCaptureStateCommand; + result = usb::bulkWrite( connection, EP_BULK_OUT, captureStateCommand, - sizeof(captureStateCommand) + sizeof(captureStateCommand), + TRANSFER_TIMEOUT_MS, + TRANSFER_ATTEMPTS ); - readSpeedResult = usb::controlRead( - connection, CONTROL_GETSPEED, speedBuffer, sizeof(speedBuffer) - ); - readResult = usb::bulkRead( - connection, EP_BULK_IN, response, EP_BULK_IN_MAX_PACKET_LEN + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = EAcquisitionOperation::eSpeedBeforeResponse; + result = usb::controlRead( + connection, + CONTROL_GETSPEED, + speedBuffer, + sizeof(speedBuffer), + 0U, + 0U, + TRANSFER_TIMEOUT_MS, + TRANSFER_ATTEMPTS, + 1U ); - - if ( - (beginResult.status == usb::EUsbTransferStatus::eSuccess) && - (speedResult.status == usb::EUsbTransferStatus::eSuccess) && - (writeResult.status == usb::EUsbTransferStatus::eSuccess) && - (readSpeedResult.status == usb::EUsbTransferStatus::eSuccess) && - (readResult.status == usb::EUsbTransferStatus::eSuccess) - ) { - loop->status.pollCount.fetch_add(1UL); - loop->status.lastCaptureState.store( - static_cast(response[0]) - ); - } - else { - loop->status.errorCount.fetch_add(1UL); - fprintf( - stderr, - "acquisition poll failed: begin=%s speed=%s write=%s " - "read-speed=%s read=%s\n", - beginResult.errorMessage.c_str(), - speedResult.errorMessage.c_str(), - writeResult.errorMessage.c_str(), - readSpeedResult.errorMessage.c_str(), - readResult.errorMessage.c_str() - ); - } - - std::this_thread::sleep_for( - std::chrono::milliseconds(POLL_INTERVAL_MS) + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = EAcquisitionOperation::eCaptureStateResponse; + result = usb::bulkRead( + connection, + EP_BULK_IN, + response, + EP_BULK_IN_MAX_PACKET_LEN, + TRANSFER_TIMEOUT_MS, + TRANSFER_ATTEMPTS, + 4 ); } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = EAcquisitionOperation::eNone; + } + + return result; } } // namespace capture diff --git a/capture/acquisition_loop.h b/capture/acquisition_loop.h index e207db2..41e79c7 100644 --- a/capture/acquisition_loop.h +++ b/capture/acquisition_loop.h @@ -34,11 +34,37 @@ namespace oscilloscope { namespace capture { -/** @brief Poll counters and last observed state, safe to read from any thread */ +/** @brief Describes the current state of the acquisition worker */ +enum class EAcquisitionState { + eStopped, /**< The worker is not running */ + eRunning, /**< Polling is proceeding normally */ + eRecovering, /**< A transient USB error is being retried */ + eDeviceLost, /**< The USB device was disconnected */ + eFailed /**< The recovery limit was exhausted */ +}; + +/** @brief Identifies the transfer that failed in a polling transaction */ +enum class EAcquisitionOperation { + eNone, /**< No transfer has failed */ + eBeginCommand, /**< Begin-command control write */ + eSpeedBeforeCommand, /**< Speed control read before command write */ + eCaptureStateCommand, /**< Capture-state bulk command write */ + eSpeedBeforeResponse, /**< Speed control read before response read */ + eCaptureStateResponse /**< Capture-state bulk response read */ +}; + +/** @brief Acquisition state safe to read from any thread */ struct SAcquisitionStatus { - std::atomic pollCount{0UL}; /**< Successful poll cycles */ - std::atomic errorCount{0UL}; /**< Failed poll cycles */ - std::atomic lastCaptureState{-1}; /**< Last raw capture state */ + std::atomic lastCaptureState{-1}; /**< Last raw capture state */ + std::atomic state{ + EAcquisitionState::eStopped + }; /**< Current worker state */ + std::atomic failedOperation{ + EAcquisitionOperation::eNone + }; /**< Most recent failed operation */ + std::atomic lastTransferStatus{ + usb::EUsbTransferStatus::eSuccess + }; /**< Most recent failed transfer status */ }; /** @brief Owns the background polling thread and its shared status */ @@ -69,6 +95,13 @@ void startAcquisitionLoop( */ void stopAcquisitionLoop(SAcquisitionLoop *loop); +/** + * @brief Joins a worker that ended because of a terminal USB error + * @param[in,out] loop Loop control block containing the finished worker + * @returns True when a terminal worker was joined + */ +bool joinFinishedAcquisitionLoop(SAcquisitionLoop *loop); + } // namespace capture } // namespace oscilloscope /******************************************************************************/ diff --git a/docs/Doxyfile b/docs/Doxyfile index 4822b48..99c62a7 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -911,7 +911,8 @@ WARN_LOGFILE = INPUT = ./mainpage.md \ ../app \ ../usb/inc \ - ../usb/src + ../usb/src \ + ../capture # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/tools/dsoextractfw.c b/tools/dsoextractfw.c index 853d222..8d52ad9 100644 --- a/tools/dsoextractfw.c +++ b/tools/dsoextractfw.c @@ -6,7 +6,6 @@ * @date @showdate "%Y-%m-%d" * @brief Extracts Hantek FX2 firmware from an installed Windows driver * @par - * Adapted from the GPL-2.0-or-later extractor in OldQtCode/dsoextractfw. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/usb/inc/usb_device.h b/usb/inc/usb_device.h index 435fb0b..f9c1a60 100644 --- a/usb/inc/usb_device.h +++ b/usb/inc/usb_device.h @@ -87,10 +87,11 @@ struct SUsbConnectionResult { /** @brief Describes the outcome of a bulk endpoint transfer */ enum class EUsbTransferStatus { - eSuccess, /**< The full requested length was transferred */ - eTimeout, /**< All retry attempts timed out */ - eNoDevice, /**< The device was disconnected mid-transfer */ - eError /**< Some other libusb error occurred */ + eSuccess, /**< The full requested length was transferred */ + eTimeout, /**< All retry attempts timed out */ + eNoDevice, /**< The device was disconnected mid-transfer */ + eShortTransfer, /**< Fewer bytes than requested were transferred */ + eError /**< Some other libusb error occurred */ }; /** @brief Holds the result of a bulk endpoint transfer */ @@ -128,6 +129,17 @@ SUsbConnectionResult connectToDevice( */ SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection); +/** + * @brief Reads the identity of the device behind an active connection + * @param[in] connection Active USB connection + * @param[out] deviceInfo Actual device identity after any re-enumeration + * @returns True when the connected device identity was read successfully + */ +bool getConnectedDeviceInfo( + const SUsbConnection &connection, + SUsbDeviceInfo *deviceInfo +); + /** * @brief Writes to a bulk OUT endpoint, retrying on timeout * @param[in] connection Active connection to write through @@ -155,6 +167,7 @@ SUsbTransferResult bulkWrite( * @param[in] length Number of bytes to read * @param[in] timeoutMs Per-attempt timeout in milliseconds * @param[in] attempts Number of attempts before giving up on timeout + * @param[in] minimumLength Minimum valid response length in bytes * @returns Transfer status, bytes transferred, and error message */ SUsbTransferResult bulkRead( @@ -163,7 +176,8 @@ SUsbTransferResult bulkRead( uint8_t *buffer, int length, unsigned int timeoutMs = 500U, - unsigned int attempts = 3U + unsigned int attempts = 3U, + int minimumLength = 1 ); /** @@ -199,6 +213,7 @@ SUsbTransferResult controlWrite( * @param[in] index wIndex field * @param[in] timeoutMs Per-attempt timeout in milliseconds * @param[in] attempts Number of attempts before giving up on timeout + * @param[in] minimumLength Minimum valid response length in bytes * @returns Transfer status, bytes transferred, and error message */ SUsbTransferResult controlRead( @@ -209,7 +224,8 @@ SUsbTransferResult controlRead( uint16_t value = 0U, uint16_t index = 0U, unsigned int timeoutMs = 500U, - unsigned int attempts = 3U + unsigned int attempts = 3U, + uint16_t minimumLength = 1U ); } // namespace usb diff --git a/usb/src/usb_device.cpp b/usb/src/usb_device.cpp index 3ad8ce9..e388b69 100644 --- a/usb/src/usb_device.cpp +++ b/usb/src/usb_device.cpp @@ -143,6 +143,21 @@ static EUsbTransferStatus classifyBulkTransferResult(int transferResult); */ static EUsbTransferStatus classifyControlTransferResult(int transferResult); +/** + * @brief Builds a transfer result and validates its minimum byte count + * @param[in] status Status classified from the libusb return value + * @param[in] transferResult Raw libusb return value for error reporting + * @param[in] transferredBytes Number of bytes transferred + * @param[in] minimumLength Minimum valid transfer length + * @returns Complete transfer result for the public USB API + */ +static SUsbTransferResult makeTransferResult( + EUsbTransferStatus status, + int transferResult, + int transferredBytes, + int minimumLength +); + /********************* Application Programming Interface **********************/ /** @fn enumerateSupportedDevices */ @@ -420,6 +435,45 @@ SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection) { return result; } +/** @fn getConnectedDeviceInfo */ +bool getConnectedDeviceInfo( + const SUsbConnection &connection, + SUsbDeviceInfo *deviceInfo +) { + bool result = false; + libusb_device *device = NULL; + libusb_device_descriptor descriptor; + const SSupportedDevice *supportedDevice = NULL; + int descriptorResult = LIBUSB_ERROR_INVALID_PARAM; + + if ( + connection.isConnected && + (connection.handle != NULL) && + (deviceInfo != NULL) + ) { + device = libusb_get_device(connection.handle); + descriptorResult = libusb_get_device_descriptor(device, &descriptor); + if (descriptorResult == LIBUSB_SUCCESS) { + supportedDevice = findSupportedDevice( + descriptor.idVendor, + descriptor.idProduct + ); + if (supportedDevice != NULL) { + *deviceInfo = { + descriptor.idVendor, + descriptor.idProduct, + libusb_get_bus_number(device), + libusb_get_device_address(device), + supportedDevice->modelName + }; + result = true; + } + } + } + + return result; +} + /** @fn bulkWrite */ SUsbTransferResult bulkWrite( const SUsbConnection &connection, @@ -449,11 +503,12 @@ SUsbTransferResult bulkWrite( ); } - result.transferredBytes = transferredBytes; - result.status = classifyBulkTransferResult(transferResult); - if (result.status != EUsbTransferStatus::eSuccess) { - result.errorMessage = libusb_error_name(transferResult); - } + result = makeTransferResult( + classifyBulkTransferResult(transferResult), + transferResult, + transferredBytes, + length + ); return result; } @@ -465,7 +520,8 @@ SUsbTransferResult bulkRead( uint8_t *buffer, const int length, const unsigned int timeoutMs, - const unsigned int attempts + const unsigned int attempts, + const int minimumLength ) { SUsbTransferResult result = {EUsbTransferStatus::eError, 0, ""}; int transferResult = LIBUSB_ERROR_TIMEOUT; @@ -487,11 +543,12 @@ SUsbTransferResult bulkRead( ); } - result.transferredBytes = transferredBytes; - result.status = classifyBulkTransferResult(transferResult); - if (result.status != EUsbTransferStatus::eSuccess) { - result.errorMessage = libusb_error_name(transferResult); - } + result = makeTransferResult( + classifyBulkTransferResult(transferResult), + transferResult, + transferredBytes, + minimumLength + ); return result; } @@ -529,11 +586,12 @@ SUsbTransferResult controlWrite( ); } - result.transferredBytes = (transferResult >= 0) ? transferResult : 0; - result.status = classifyControlTransferResult(transferResult); - if (result.status != EUsbTransferStatus::eSuccess) { - result.errorMessage = libusb_error_name(transferResult); - } + result = makeTransferResult( + classifyControlTransferResult(transferResult), + transferResult, + (transferResult >= 0) ? transferResult : 0, + static_cast(length) + ); return result; } @@ -547,7 +605,8 @@ SUsbTransferResult controlRead( const uint16_t value, const uint16_t index, const unsigned int timeoutMs, - const unsigned int attempts + const unsigned int attempts, + const uint16_t minimumLength ) { SUsbTransferResult result = {EUsbTransferStatus::eError, 0, ""}; int transferResult = LIBUSB_ERROR_TIMEOUT; @@ -571,11 +630,12 @@ SUsbTransferResult controlRead( ); } - result.transferredBytes = (transferResult >= 0) ? transferResult : 0; - result.status = classifyControlTransferResult(transferResult); - if (result.status != EUsbTransferStatus::eSuccess) { - result.errorMessage = libusb_error_name(transferResult); - } + result = makeTransferResult( + classifyControlTransferResult(transferResult), + transferResult, + (transferResult >= 0) ? transferResult : 0, + static_cast(minimumLength) + ); return result; } @@ -751,6 +811,34 @@ static EUsbTransferStatus classifyControlTransferResult( return result; } +/*----------------------------------------------------------------------------*/ + +/** @fn makeTransferResult */ +static SUsbTransferResult makeTransferResult( + const EUsbTransferStatus status, + const int transferResult, + const int transferredBytes, + const int minimumLength +) { + SUsbTransferResult result = {status, transferredBytes, ""}; + + if ( + (result.status == EUsbTransferStatus::eSuccess) && + (result.transferredBytes < minimumLength) + ) { + result.status = EUsbTransferStatus::eShortTransfer; + } + + if (result.status == EUsbTransferStatus::eShortTransfer) { + result.errorMessage = "short transfer"; + } + else if (result.status != EUsbTransferStatus::eSuccess) { + result.errorMessage = libusb_error_name(transferResult); + } + + return result; +} + } // namespace usb } // namespace oscilloscope /******************************************************************************/