Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
192 changes: 150 additions & 42 deletions app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,20 @@
#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;
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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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};
Expand All @@ -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"
Expand All @@ -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<int32_t>(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")) {
Expand Down Expand Up @@ -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();
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading