From 0dec8f8e4635cef2db6a77971cf83ea106ac7f1b Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 15:46:52 +0300 Subject: [PATCH 01/13] Added short USB transfer validation --- usb/inc/usb_device.h | 17 ++++++++----- usb/src/usb_device.cpp | 58 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/usb/inc/usb_device.h b/usb/inc/usb_device.h index 435fb0b..af0af42 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 */ @@ -155,6 +156,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 +165,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 +202,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 +213,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..8c86155 100644 --- a/usb/src/usb_device.cpp +++ b/usb/src/usb_device.cpp @@ -451,8 +451,19 @@ SUsbTransferResult bulkWrite( result.transferredBytes = transferredBytes; result.status = classifyBulkTransferResult(transferResult); + if ( + (result.status == EUsbTransferStatus::eSuccess) && + (transferredBytes != length) + ) { + result.status = EUsbTransferStatus::eShortTransfer; + } if (result.status != EUsbTransferStatus::eSuccess) { - result.errorMessage = libusb_error_name(transferResult); + if (result.status == EUsbTransferStatus::eShortTransfer) { + result.errorMessage = "short transfer"; + } + else { + result.errorMessage = libusb_error_name(transferResult); + } } return result; @@ -465,7 +476,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; @@ -489,8 +501,19 @@ SUsbTransferResult bulkRead( result.transferredBytes = transferredBytes; result.status = classifyBulkTransferResult(transferResult); + if ( + (result.status == EUsbTransferStatus::eSuccess) && + (transferredBytes < minimumLength) + ) { + result.status = EUsbTransferStatus::eShortTransfer; + } if (result.status != EUsbTransferStatus::eSuccess) { - result.errorMessage = libusb_error_name(transferResult); + if (result.status == EUsbTransferStatus::eShortTransfer) { + result.errorMessage = "short transfer"; + } + else { + result.errorMessage = libusb_error_name(transferResult); + } } return result; @@ -531,8 +554,19 @@ SUsbTransferResult controlWrite( result.transferredBytes = (transferResult >= 0) ? transferResult : 0; result.status = classifyControlTransferResult(transferResult); + if ( + (result.status == EUsbTransferStatus::eSuccess) && + (result.transferredBytes != static_cast(length)) + ) { + result.status = EUsbTransferStatus::eShortTransfer; + } if (result.status != EUsbTransferStatus::eSuccess) { - result.errorMessage = libusb_error_name(transferResult); + if (result.status == EUsbTransferStatus::eShortTransfer) { + result.errorMessage = "short transfer"; + } + else { + result.errorMessage = libusb_error_name(transferResult); + } } return result; @@ -547,7 +581,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; @@ -573,8 +608,19 @@ SUsbTransferResult controlRead( result.transferredBytes = (transferResult >= 0) ? transferResult : 0; result.status = classifyControlTransferResult(transferResult); + if ( + (result.status == EUsbTransferStatus::eSuccess) && + (result.transferredBytes < static_cast(minimumLength)) + ) { + result.status = EUsbTransferStatus::eShortTransfer; + } if (result.status != EUsbTransferStatus::eSuccess) { - result.errorMessage = libusb_error_name(transferResult); + if (result.status == EUsbTransferStatus::eShortTransfer) { + result.errorMessage = "short transfer"; + } + else { + result.errorMessage = libusb_error_name(transferResult); + } } return result; From 390ee837d8e5b5ace4d485d8da8d1f05cc1b947b Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 15:47:26 +0300 Subject: [PATCH 02/13] Added USB acquisition recovery states --- app/main.cpp | 92 ++++++++++++++- capture/acquisition_loop.cpp | 217 +++++++++++++++++++++++++---------- capture/acquisition_loop.h | 41 ++++++- 3 files changed, 283 insertions(+), 67 deletions(-) diff --git a/app/main.cpp b/app/main.cpp index 6e3bdd4..38cfd44 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; @@ -117,6 +120,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, @@ -230,6 +237,32 @@ 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}; + deviceStatus = "Device disconnected: " + acquisitionError; + } + else { + deviceStatus = "Acquisition stopped: " + acquisitionError; + } + } + if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Exit")) { @@ -436,14 +469,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 +566,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 /******************************************************************************/ From 543b1c451fdf540a35f10e2e9a95a14759b11caa Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 15:49:33 +0300 Subject: [PATCH 03/13] Reset USB status after acquisition restart --- app/main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/main.cpp b/app/main.cpp index 38cfd44..57e99eb 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -342,6 +342,10 @@ int main (void) { &acquisitionLoop, usbConnection ); + deviceStatus = formatUsbConnectionStatus( + usbScanResult, + usbConnection + ); } acquisitionRunning = true; } From 4044e64329418739a3e402a5db05b646b5a935be Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 15:51:28 +0300 Subject: [PATCH 04/13] Deduplicated USB transfer results --- usb/src/usb_device.cpp | 131 +++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 64 deletions(-) diff --git a/usb/src/usb_device.cpp b/usb/src/usb_device.cpp index 8c86155..b0cf133 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 */ @@ -449,22 +464,12 @@ SUsbTransferResult bulkWrite( ); } - result.transferredBytes = transferredBytes; - result.status = classifyBulkTransferResult(transferResult); - if ( - (result.status == EUsbTransferStatus::eSuccess) && - (transferredBytes != length) - ) { - result.status = EUsbTransferStatus::eShortTransfer; - } - if (result.status != EUsbTransferStatus::eSuccess) { - if (result.status == EUsbTransferStatus::eShortTransfer) { - result.errorMessage = "short transfer"; - } - else { - result.errorMessage = libusb_error_name(transferResult); - } - } + result = makeTransferResult( + classifyBulkTransferResult(transferResult), + transferResult, + transferredBytes, + length + ); return result; } @@ -499,22 +504,12 @@ SUsbTransferResult bulkRead( ); } - result.transferredBytes = transferredBytes; - result.status = classifyBulkTransferResult(transferResult); - if ( - (result.status == EUsbTransferStatus::eSuccess) && - (transferredBytes < minimumLength) - ) { - result.status = EUsbTransferStatus::eShortTransfer; - } - if (result.status != EUsbTransferStatus::eSuccess) { - if (result.status == EUsbTransferStatus::eShortTransfer) { - result.errorMessage = "short transfer"; - } - else { - result.errorMessage = libusb_error_name(transferResult); - } - } + result = makeTransferResult( + classifyBulkTransferResult(transferResult), + transferResult, + transferredBytes, + minimumLength + ); return result; } @@ -552,22 +547,12 @@ SUsbTransferResult controlWrite( ); } - result.transferredBytes = (transferResult >= 0) ? transferResult : 0; - result.status = classifyControlTransferResult(transferResult); - if ( - (result.status == EUsbTransferStatus::eSuccess) && - (result.transferredBytes != static_cast(length)) - ) { - result.status = EUsbTransferStatus::eShortTransfer; - } - if (result.status != EUsbTransferStatus::eSuccess) { - if (result.status == EUsbTransferStatus::eShortTransfer) { - result.errorMessage = "short transfer"; - } - else { - result.errorMessage = libusb_error_name(transferResult); - } - } + result = makeTransferResult( + classifyControlTransferResult(transferResult), + transferResult, + (transferResult >= 0) ? transferResult : 0, + static_cast(length) + ); return result; } @@ -606,22 +591,12 @@ SUsbTransferResult controlRead( ); } - result.transferredBytes = (transferResult >= 0) ? transferResult : 0; - result.status = classifyControlTransferResult(transferResult); - if ( - (result.status == EUsbTransferStatus::eSuccess) && - (result.transferredBytes < static_cast(minimumLength)) - ) { - result.status = EUsbTransferStatus::eShortTransfer; - } - if (result.status != EUsbTransferStatus::eSuccess) { - if (result.status == EUsbTransferStatus::eShortTransfer) { - result.errorMessage = "short transfer"; - } - else { - result.errorMessage = libusb_error_name(transferResult); - } - } + result = makeTransferResult( + classifyControlTransferResult(transferResult), + transferResult, + (transferResult >= 0) ? transferResult : 0, + static_cast(minimumLength) + ); return result; } @@ -797,6 +772,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 /******************************************************************************/ From 26f6c2b0bde58f116d214467d75fd419030e6da5 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 15:55:52 +0300 Subject: [PATCH 05/13] Documented USB error recovery --- HISTORY.md | 18 +++++++++++++++++- README.md | 9 +++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 44547d6..391be77 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -170,8 +170,24 @@ 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. + ### 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. +- Verify transfer-error recovery and unexpected disconnection handling with + physical hardware. diff --git a/README.md b/README.md index 49332c8..a27350d 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,16 @@ 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. ## Planned Stack From c27f2639cfff6a1ffa37fa3a1d9703ea43285a55 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 16:26:14 +0300 Subject: [PATCH 06/13] Added connected USB device identity --- usb/inc/usb_device.h | 11 +++++++++++ usb/src/usb_device.cpp | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/usb/inc/usb_device.h b/usb/inc/usb_device.h index af0af42..f9c1a60 100644 --- a/usb/inc/usb_device.h +++ b/usb/inc/usb_device.h @@ -129,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 diff --git a/usb/src/usb_device.cpp b/usb/src/usb_device.cpp index b0cf133..e388b69 100644 --- a/usb/src/usb_device.cpp +++ b/usb/src/usb_device.cpp @@ -435,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, From 623a8bc30223b1e40edf92d05c32e4876c81e378 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 16:26:54 +0300 Subject: [PATCH 07/13] Added idle USB presence monitoring --- app/main.cpp | 54 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/app/main.cpp b/app/main.cpp index 57e99eb..6ecfb4d 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -87,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" @@ -215,6 +218,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" @@ -263,6 +268,32 @@ int main (void) { } } + 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 && + !isUsbDevicePresent(usbScanResult, connectedDevice) + ) { + oscilloscope::usb::disconnectFromDevice(&usbConnection); + connectedDevice = {0U, 0U, 0U, 0U, NULL}; + deviceStatus = "Device disconnected"; + } + else if (!usbConnection.isConnected) { + deviceStatus = formatUsbConnectionStatus( + usbScanResult, + usbConnection + ); + } + } + if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Exit")) { @@ -424,11 +455,24 @@ int main (void) { ); if (connectResult.errorMessage.empty()) { - connectedDevice = usbScanResult.devices.front(); - deviceStatus = formatUsbConnectionStatus( - usbScanResult, - usbConnection - ); + if ( + oscilloscope::usb::getConnectedDeviceInfo( + usbConnection, + &connectedDevice + ) + ) { + deviceStatus = formatUsbConnectionStatus( + usbScanResult, + usbConnection + ); + } + else { + oscilloscope::usb::disconnectFromDevice( + &usbConnection + ); + deviceStatus = + "Connect error: Cannot identify USB device"; + } } else { deviceStatus = formatUsbConnectionError( From e74a234e801a17eb12db40d7e727cb9244ee96df Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 16:27:32 +0300 Subject: [PATCH 08/13] Documented USB presence monitoring --- HISTORY.md | 8 ++++++-- README.md | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 391be77..f03fcdf 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -184,10 +184,14 @@ Records key decisions, structural changes, and completed development stages. - 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. ### Next USB tasks - Decode capture-state responses and acquired sample packets. - Add buffering between USB reads and waveform processing. -- Verify transfer-error recovery and unexpected disconnection handling with - physical hardware. +- Verify idle disconnection detection, automatic return detection, and + transfer-error recovery with physical hardware. diff --git a/README.md b/README.md index a27350d..456a715 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,10 @@ 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. +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 +without requiring a manual rescan. Reconnecting remains an explicit action. ## Planned Stack From 9e63bf982020e7400ce883f8bbb81d5b718dc468 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 16:35:07 +0300 Subject: [PATCH 09/13] Removed redundant USB rescan control --- app/main.cpp | 50 ++++++++++++++------------------------------------ 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/app/main.cpp b/app/main.cpp index 6ecfb4d..097ae82 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -203,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}; @@ -261,6 +262,7 @@ int main (void) { if (acquisitionState == EAcquisitionState::eDeviceLost) { oscilloscope::usb::disconnectFromDevice(&usbConnection); connectedDevice = {0U, 0U, 0U, 0U, NULL}; + deviceWasDisconnected = true; deviceStatus = "Device disconnected: " + acquisitionError; } else { @@ -280,13 +282,22 @@ int main (void) { 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) { + else if ( + !usbConnection.isConnected && + (!usbScanResult.devices.empty() || + !deviceWasDisconnected) + ) { + if (!usbScanResult.devices.empty()) { + deviceWasDisconnected = false; + } deviceStatus = formatUsbConnectionStatus( usbScanResult, usbConnection @@ -382,41 +393,6 @@ int main (void) { } } 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 - ); - } - } - ImGui::EndDisabled(); if (usbConnection.isConnected) { if (ImGui::Button("Disconnect", ImVec2(-1.0f, 32.0f))) { @@ -426,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, @@ -461,6 +438,7 @@ int main (void) { &connectedDevice ) ) { + deviceWasDisconnected = false; deviceStatus = formatUsbConnectionStatus( usbScanResult, usbConnection From 43b50880142af729d04065d0d78b2e0aae400b8e Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 16:35:21 +0300 Subject: [PATCH 10/13] Documented automatic USB scanning --- HISTORY.md | 2 ++ README.md | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index f03fcdf..04fc4ae 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -188,6 +188,8 @@ Records key decisions, structural changes, and completed development stages. 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. ### Next USB tasks diff --git a/README.md b/README.md index 456a715..19c2563 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ 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 -without requiring a manual rescan. Reconnecting remains an explicit action. +automatically. The disconnect status remains visible while the device is +absent. Reconnecting remains an explicit action. ## Planned Stack From 92057ff4630bec7d830f70895c2ed17299921316 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 16:47:55 +0300 Subject: [PATCH 11/13] Updated Doxyfile --- docs/Doxyfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Doxyfile b/docs/Doxyfile index 4822b48..2855057 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -911,7 +911,7 @@ WARN_LOGFILE = INPUT = ./mainpage.md \ ../app \ ../usb/inc \ - ../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 From 8eed158169dd4f11270419e25b4efa30814f4f56 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 16:48:29 +0300 Subject: [PATCH 12/13] Verified idle USB disconnection --- HISTORY.md | 7 +++++-- tools/dsoextractfw.c | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 04fc4ae..b3149b3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -190,10 +190,13 @@ Records key decisions, structural changes, and completed development stages. 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. -- Verify idle disconnection detection, automatic return detection, and - transfer-error recovery with physical hardware. +- Add deterministic fault-injection tests for timeout and transfer-error + recovery, then verify recovery with a connected physical device. 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 From e8b822987854c9ee620964de162317eea3e6c117 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 17:24:56 +0300 Subject: [PATCH 13/13] Fixed PR-remark --- docs/Doxyfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Doxyfile b/docs/Doxyfile index 2855057..99c62a7 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -911,6 +911,7 @@ WARN_LOGFILE = INPUT = ./mainpage.md \ ../app \ ../usb/inc \ + ../usb/src \ ../capture # This tag can be used to specify the character encoding of the source files