From 139d6b6de0037fcd9e0878f703a528381d53e052 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Wed, 2 Sep 2026 18:02:09 +0300 Subject: [PATCH 1/7] Implemented FX2 firmware upload and bulk transfer helpers - Added usb/firmware_loader for fxload-based FX2 firmware/loader upload - Wired firmware upload and post-reenumeration rescan into connectToDevice - Selected the working alternate interface setting after claiming it - Added bulkWrite/bulkRead helpers with timeout retries - Added a udev rule for automatic fxload-based firmware upload - Documented FX2 firmware findings in HISTORY.md --- .gitignore | 3 + CMakeLists.txt | 2 + HISTORY.md | 29 ++++ usb/81-hantek-dso.rules | 20 +++ usb/firmware_loader.cpp | 127 +++++++++++++++++ usb/firmware_loader.h | 77 +++++++++++ usb/usb_device.cpp | 299 ++++++++++++++++++++++++++++++++++++++-- usb/usb_device.h | 60 +++++++- 8 files changed, 606 insertions(+), 11 deletions(-) create mode 100644 usb/81-hantek-dso.rules create mode 100644 usb/firmware_loader.cpp create mode 100644 usb/firmware_loader.h diff --git a/.gitignore b/.gitignore index 83009db..6b085b1 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ OldQtCode/ WorkingDocs **/imgui.ini +# Extracted proprietary FX2 firmware +firmware/ + diff --git a/CMakeLists.txt b/CMakeLists.txt index 5556e8f..79018f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O2") set(SOURCES_LIST app/main.cpp usb/usb_device.cpp + usb/firmware_loader.cpp ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_tables.cpp @@ -43,6 +44,7 @@ set(SOURCES_LIST set(HEADERS_LIST usb/usb_device.h + usb/firmware_loader.h ) if(CMAKE_BUILD_TYPE MATCHES "Debug") diff --git a/HISTORY.md b/HISTORY.md index 624903c..4455619 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -88,8 +88,37 @@ Records key decisions, structural changes, and completed development stages. - Added a Linux udev rule for non-root DSO-2250 access through libusb. - Verified the connect/disconnect lifecycle with a connected Hantek DSO-2250. +## 2026-09-02 + +### Research - FX2 firmware upload requirement + +- Investigated the old code for a host "presence ping" that would explain the + DSO-2250 status LED behavior (red blink on USB link, green blink on host + activity, long red+green during data bursts). +- Found no dedicated ping command: `HantekDSOAThread::run()` in + `OldQtCode/src/hantekdsoathread.cpp` polls `dsoGetCaptureState` in a loop + with a `msleep(timeBase)` delay; the repeated bulk transaction itself is + what the firmware reports as host activity. +- Found that the DSO-2250 uses a Cypress EZ-USB FX2 chip with RAM-resident + firmware. `OldQtCode/dsoextractfw/HantekDSO.rules` uploads + `DSO2250_firmware.hex` and `DSO2250_loader.hex` through `fxload` on every + USB "add" event; `OldQtCode/dsoextractfw/dsoextractfw.c` extracts those hex + files from the official Windows driver (`1.SYS`). +- Until firmware is uploaded, the device stays in a bare bootloader state: + no status LED activity and no working bulk endpoints. This explains the + LED blinking seen on Windows (official driver uploads firmware + automatically) versus no blinking on Linux with the current codebase (no + firmware upload step exists yet, and no `.hex` firmware files are present + in this repository). +- The firmware `.hex` files are not included in this repository (proprietary, + extracted from the official Windows driver); they must be supplied + separately before the USB layer can work end-to-end. + ### Next USB tasks +- Add a firmware upload step (fxload-equivalent control/bulk transfer) before + claiming the interface, sourcing `.hex` files from an external, user- + provided location. - Read endpoint data chunks in a dedicated acquisition path. - Add buffering between USB reads and waveform processing. - Handle timeouts, I/O errors, disconnects, and recovery states. diff --git a/usb/81-hantek-dso.rules b/usb/81-hantek-dso.rules new file mode 100644 index 0000000..172baa3 --- /dev/null +++ b/usb/81-hantek-dso.rules @@ -0,0 +1,20 @@ +# Hantek DSO-2090 +SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/2090/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO2090_firmware.hex -s /usr/share/HantekDSO/DSO2090_loader.hex -D $env{DEVNAME}" + +# Hantek DSO-2100 +SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/2100/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO2100_firmware.hex -s /usr/share/HantekDSO/DSO2100_loader.hex -D $env{DEVNAME}" + +# Hantek DSO-2100 +SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="547/1006/*", RUN+="/usr/sbin/fxload -t an21 -I /usr/share/HantekDSO/DSO2100_firmware.hex -s /usr/share/HantekDSO/DSO2100_loader.hex -D $env{DEVNAME}" + +# Hantek DSO-2150 +SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/2150/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO2150_firmware.hex -s /usr/share/HantekDSO/DSO2150_loader.hex -D $env{DEVNAME}" + +# Hantek DSO-2250 +SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/2250/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO2250_firmware.hex -s /usr/share/HantekDSO/DSO2250_loader.hex -D $env{DEVNAME}" + +# Hantek DSO-5200 +SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/5200/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO5200_firmware.hex -s /usr/share/HantekDSO/DSO5200_loader.hex -D $env{DEVNAME}" + +# Hantek DSO-5200A +SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/520a/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO520A_firmware.hex -s /usr/share/HantekDSO/DSO520A_loader.hex -D $env{DEVNAME}" diff --git a/usb/firmware_loader.cpp b/usb/firmware_loader.cpp new file mode 100644 index 0000000..d4589c2 --- /dev/null +++ b/usb/firmware_loader.cpp @@ -0,0 +1,127 @@ +/** + * @file firmware_loader.cpp + * @version 0.2.2 + * @authors Anton Chernov + * @date 2026-09-02 + * @date @showdate "%Y-%m-%d" + * @par + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/******************************* Included files *******************************/ +#include +#include +#include +#include + +#include "firmware_loader.h" + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace usb { + +/***************************** Private prototypes *****************************/ + +/** + * @brief Checks whether a regular file exists at the given path + * @param[in] path File path to check + * @returns True when the file exists + */ +static bool fileExists(const std::string &path); + +/********************* Application Programming Interface **********************/ + +/** @fn loadFx2Firmware */ +SFirmwareLoadResult loadFx2Firmware( + const uint8_t busNumber, + const uint8_t deviceAddress, + const SFirmwarePaths &paths +) { + SFirmwareLoadResult result = {EFirmwareLoadStatus::eLoaded, ""}; + char devicePath[32]; + pid_t childPid = -1; + int childStatus = 0; + + if (!fileExists(paths.firmwareHexPath) || !fileExists(paths.loaderHexPath)) { + result.status = EFirmwareLoadStatus::eHexFileNotFound; + result.errorMessage = + "Firmware or loader .hex file not found: " + + paths.firmwareHexPath + ", " + paths.loaderHexPath; + } + else { + std::snprintf( + devicePath, + sizeof(devicePath), + "/dev/bus/usb/%03u/%03u", + busNumber, + deviceAddress + ); + + childPid = fork(); + + if (childPid < 0) { + result.status = EFirmwareLoadStatus::eUploadFailed; + result.errorMessage = "fork() failed while launching fxload"; + } + else if (childPid == 0) { + execlp( + "fxload", "fxload", + "-t", "fx2", + "-I", paths.firmwareHexPath.c_str(), + "-s", paths.loaderHexPath.c_str(), + "-D", devicePath, + (char*)NULL + ); + /* Fall back to the common sbin location: GUI launchers often + * run with a PATH that omits /usr/sbin. */ + execl( + "/usr/sbin/fxload", "fxload", + "-t", "fx2", + "-I", paths.firmwareHexPath.c_str(), + "-s", paths.loaderHexPath.c_str(), + "-D", devicePath, + (char*)NULL + ); + _exit(127); /* both exec attempts failed */ + } + else if (waitpid(childPid, &childStatus, 0) < 0) { + result.status = EFirmwareLoadStatus::eUploadFailed; + result.errorMessage = "waitpid() failed while running fxload"; + } + else if (!WIFEXITED(childStatus) || (WEXITSTATUS(childStatus) == 127)) { + result.status = EFirmwareLoadStatus::eFxloadNotFound; + result.errorMessage = "fxload utility was not found in PATH"; + } + else if (WEXITSTATUS(childStatus) != 0) { + result.status = EFirmwareLoadStatus::eUploadFailed; + result.errorMessage = "fxload exited with a non-zero status"; + } + } + + return result; +} + +/****************************** Private functions *****************************/ + +/** @fn fileExists */ +static bool fileExists(const std::string &path) { + struct stat statBuffer; + return stat(path.c_str(), &statBuffer) == 0; +} + +} // namespace usb +} // namespace oscilloscope +/******************************************************************************/ diff --git a/usb/firmware_loader.h b/usb/firmware_loader.h new file mode 100644 index 0000000..14189f9 --- /dev/null +++ b/usb/firmware_loader.h @@ -0,0 +1,77 @@ +/** + * @file firmware_loader.h + * @version 0.2.2 + * @authors Anton Chernov + * @date 2026-09-02 + * @date @showdate "%Y-%m-%d" + * @par + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FIRMWARE_LOADER_H_ +#define FIRMWARE_LOADER_H_ + +/******************************* Included files *******************************/ +#include +#include + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace usb { + +/** @brief Describes the outcome of an FX2 firmware upload */ +enum class EFirmwareLoadStatus { + eLoaded, /**< fxload uploaded the firmware successfully */ + eHexFileNotFound, /**< The firmware or loader .hex file is missing */ + eFxloadNotFound, /**< The fxload utility is not available in PATH */ + eUploadFailed /**< fxload ran but reported a failure */ +}; + +/** @brief Locates the loader and firmware .hex files for one device model */ +struct SFirmwarePaths { + std::string loaderHexPath; /**< Path to the second-stage loader .hex */ + std::string firmwareHexPath; /**< Path to the main firmware .hex */ +}; + +/** @brief Holds the result of a firmware upload attempt */ +struct SFirmwareLoadResult { + EFirmwareLoadStatus status; /**< Upload outcome */ + std::string errorMessage; /**< Details when applicable */ +}; + +/********************* Application Programming Interface **********************/ + +/** + * @brief Uploads RAM-resident FX2 firmware to a bare/bootloader-mode device + * @param[in] busNumber USB bus number of the target device + * @param[in] deviceAddress USB device address of the target device + * @param[in] paths Loader and firmware .hex file locations + * @returns Upload status and error message when applicable + * @note The device performs a soft USB re-enumeration once firmware starts + * running; the caller must re-scan for the device afterward, since its + * bus address may change. + */ +SFirmwareLoadResult loadFx2Firmware( + uint8_t busNumber, + uint8_t deviceAddress, + const SFirmwarePaths &paths +); + +} // namespace usb +} // namespace oscilloscope + +#endif //! FIRMWARE_LOADER_H_ +/******************************************************************************/ diff --git a/usb/usb_device.cpp b/usb/usb_device.cpp index f205f79..3746813 100644 --- a/usb/usb_device.cpp +++ b/usb/usb_device.cpp @@ -21,8 +21,14 @@ */ /******************************* Included files *******************************/ +#include + +#include +#include + #include +#include "firmware_loader.h" #include "usb_device.h" /********************************* Definitions ********************************/ @@ -35,14 +41,21 @@ struct SSupportedDevice { uint16_t vendorId; uint16_t productId; uint8_t interfaceNumber; + uint8_t alternateSetting; const char *modelName; + const char *firmwareBaseName; }; +/** @brief Microseconds to wait for the FX2 chip to re-enumerate after upload */ +static const unsigned int FIRMWARE_REENUMERATION_DELAY_US = 1500000U; + /****************************** Module variables ******************************/ /** @brief Maps supported USB VID/PID pairs to model display names */ static const SSupportedDevice supported_devices[] = { - {0x04B4U, 0x2250U, 0U, "Hantek DSO-2250"} + /* Alt setting 0 exposes no endpoints; alt setting 1 has the bulk pair + * (EP2 OUT / EP6 IN) that the FX2 firmware actually uses. */ + {0x04B4U, 0x2250U, 0U, 1U, "Hantek DSO-2250", "DSO2250"} }; /***************************** Private prototypes *****************************/ @@ -70,6 +83,48 @@ static libusb_device* findDeviceByInfo( const ssize_t deviceCount ); +/** + * @brief Finds the first connected device matching a VID/PID pair + * @param[in] deviceList libusb device list to search + * @param[in] vendorId USB vendor identifier to match + * @param[in] productId USB product identifier to match + * @param[in] deviceCount Number of entries in `deviceList` + * @returns Matching libusb device, or NULL when not found + */ +static libusb_device* findDeviceByVidPid( + libusb_device **deviceList, + uint16_t vendorId, + uint16_t productId, + const ssize_t deviceCount +); + +/** + * @brief Checks whether a regular file exists at the given path + * @param[in] path File path to check + * @returns True when the file exists + */ +static bool firmwareFileExists(const std::string &path); + +/** + * @brief Builds the loader/firmware .hex paths for a supported model + * @param[in] supportedDevice Matching model entry + * @returns Loader and firmware .hex file locations + * @note The directory defaults to `firmware/` relative to the working + * directory, and can be overridden with `OSCILLOSCOPE_FIRMWARE_DIR`. + * When no override is set, a few common build/run working directories + * are tried automatically. + */ +static SFirmwarePaths resolveFirmwarePaths( + const SSupportedDevice &supportedDevice +); + +/** + * @brief Maps a libusb bulk transfer result code to a transfer status + * @param[in] transferResult Return value of `libusb_bulk_transfer` + * @returns Classified transfer status + */ +static EBulkTransferStatus classifyBulkTransferResult(int transferResult); + /********************* Application Programming Interface **********************/ /** @fn enumerateSupportedDevices */ @@ -137,6 +192,7 @@ SUsbConnectionResult connectToDevice( libusb_device *device = NULL; libusb_device_handle *handle = NULL; const SSupportedDevice *supportedDevice = NULL; + SFirmwareLoadResult firmwareResult = {EFirmwareLoadStatus::eLoaded, ""}; ssize_t deviceCount = 0; int initializationResult = LIBUSB_SUCCESS; int openResult = LIBUSB_SUCCESS; @@ -172,6 +228,43 @@ SUsbConnectionResult connectToDevice( result.errorMessage = "Requested USB device was not found"; } else { + firmwareResult = loadFx2Firmware( + deviceInfo.busNumber, + deviceInfo.deviceAddress, + resolveFirmwarePaths(*supportedDevice) + ); + + if (firmwareResult.status != EFirmwareLoadStatus::eLoaded) { + result.status = EConnectionStatus::eFirmwareLoadFailed; + result.errorMessage = firmwareResult.errorMessage; + } + else { + /* The FX2 chip re-enumerates once firmware starts + * running, so the device pointer above is stale and + * its bus address may have changed. */ + usleep(FIRMWARE_REENUMERATION_DELAY_US); + libusb_free_device_list(deviceList, 1); + deviceList = NULL; + deviceCount = libusb_get_device_list(context, &deviceList); + device = findDeviceByVidPid( + deviceList, + deviceInfo.vendorId, + deviceInfo.productId, + deviceCount + ); + + if ((deviceCount < 0) || (device == NULL)) { + result.status = EConnectionStatus::eDeviceNotFound; + result.errorMessage = + "Device did not reappear after firmware upload"; + } + } + } + + if ( + (result.status != EConnectionStatus::eDeviceNotFound) && + (result.status != EConnectionStatus::eFirmwareLoadFailed) + ) { openResult = libusb_open(device, &handle); if (openResult != LIBUSB_SUCCESS) { @@ -189,14 +282,32 @@ SUsbConnectionResult connectToDevice( result.errorMessage = libusb_error_name(claimResult); } else { - connection->context = context; - connection->handle = handle; - connection->interfaceNumber = - supportedDevice->interfaceNumber; - connection->isConnected = true; - result.status = EConnectionStatus::eConnected; - context = NULL; - handle = NULL; + const int setInterfaceResult = + libusb_set_interface_alt_setting( + handle, + supportedDevice->interfaceNumber, + supportedDevice->alternateSetting + ); + + if (setInterfaceResult != LIBUSB_SUCCESS) { + result.status = EConnectionStatus::eSetInterfaceFailed; + result.errorMessage = + libusb_error_name(setInterfaceResult); + libusb_release_interface( + handle, + supportedDevice->interfaceNumber + ); + } + else { + connection->context = context; + connection->handle = handle; + connection->interfaceNumber = + supportedDevice->interfaceNumber; + connection->isConnected = true; + result.status = EConnectionStatus::eConnected; + context = NULL; + handle = NULL; + } } } } @@ -258,6 +369,82 @@ SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection) { return result; } +/** @fn bulkWrite */ +SBulkTransferResult bulkWrite( + const SUsbConnection &connection, + const uint8_t endpointAddress, + const uint8_t *data, + const int length, + const unsigned int timeoutMs, + const unsigned int attempts +) { + SBulkTransferResult result = {EBulkTransferStatus::eError, 0, ""}; + int transferResult = LIBUSB_ERROR_TIMEOUT; + int transferredBytes = 0; + unsigned int attempt; + + for ( + attempt = 0U; + (attempt < attempts) && (transferResult == LIBUSB_ERROR_TIMEOUT); + ++attempt + ) { + transferResult = libusb_bulk_transfer( + connection.handle, + endpointAddress, + const_cast(data), + length, + &transferredBytes, + timeoutMs + ); + } + + result.transferredBytes = transferredBytes; + result.status = classifyBulkTransferResult(transferResult); + if (result.status != EBulkTransferStatus::eSuccess) { + result.errorMessage = libusb_error_name(transferResult); + } + + return result; +} + +/** @fn bulkRead */ +SBulkTransferResult bulkRead( + const SUsbConnection &connection, + const uint8_t endpointAddress, + uint8_t *buffer, + const int length, + const unsigned int timeoutMs, + const unsigned int attempts +) { + SBulkTransferResult result = {EBulkTransferStatus::eError, 0, ""}; + int transferResult = LIBUSB_ERROR_TIMEOUT; + int transferredBytes = 0; + unsigned int attempt; + + for ( + attempt = 0U; + (attempt < attempts) && (transferResult == LIBUSB_ERROR_TIMEOUT); + ++attempt + ) { + transferResult = libusb_bulk_transfer( + connection.handle, + endpointAddress, + buffer, + length, + &transferredBytes, + timeoutMs + ); + } + + result.transferredBytes = transferredBytes; + result.status = classifyBulkTransferResult(transferResult); + if (result.status != EBulkTransferStatus::eSuccess) { + result.errorMessage = libusb_error_name(transferResult); + } + + return result; +} + /****************************** Private functions *****************************/ /** @fn findSupportedDevice */ @@ -316,6 +503,100 @@ static libusb_device* findDeviceByInfo( return result; } +/** @fn findDeviceByVidPid */ +static libusb_device* findDeviceByVidPid( + libusb_device **deviceList, + const uint16_t vendorId, + const uint16_t productId, + const ssize_t deviceCount +) { + libusb_device *result = NULL; + libusb_device *candidate = NULL; + libusb_device_descriptor descriptor; + int descriptorResult = LIBUSB_ERROR_OTHER; + + for (ssize_t index = 0; index < deviceCount; ++index) { + candidate = deviceList[index]; + descriptorResult = libusb_get_device_descriptor( + candidate, + &descriptor + ); + + if (descriptorResult == LIBUSB_SUCCESS) { + if ( + (descriptor.idVendor == vendorId) && + (descriptor.idProduct == productId) + ) { + result = candidate; + break; + } + } + } + + return result; +} + +/** @fn resolveFirmwarePaths */ +static SFirmwarePaths resolveFirmwarePaths( + const SSupportedDevice &supportedDevice +) { + static const char* const candidateDirs[] = { + "firmware", "../firmware", "../../firmware" + }; + SFirmwarePaths paths; + const char *firmwareDir = getenv("OSCILLOSCOPE_FIRMWARE_DIR"); + const size_t candidateCount = + sizeof(candidateDirs) / sizeof(candidateDirs[0]); + size_t index; + + if (firmwareDir == NULL) { + firmwareDir = candidateDirs[0]; + + for (index = 0U; index < candidateCount; ++index) { + const std::string candidateLoaderPath = + std::string(candidateDirs[index]) + "/" + + supportedDevice.firmwareBaseName + "_loader.hex"; + + if (firmwareFileExists(candidateLoaderPath)) { + firmwareDir = candidateDirs[index]; + break; + } + } + } + + paths.loaderHexPath = + std::string(firmwareDir) + "/" + + supportedDevice.firmwareBaseName + "_loader.hex"; + paths.firmwareHexPath = + std::string(firmwareDir) + "/" + + supportedDevice.firmwareBaseName + "_firmware.hex"; + + return paths; +} + +/** @fn firmwareFileExists */ +static bool firmwareFileExists(const std::string &path) { + struct stat statBuffer; + return stat(path.c_str(), &statBuffer) == 0; +} + +/** @fn classifyBulkTransferResult */ +static EBulkTransferStatus classifyBulkTransferResult(const int transferResult) { + EBulkTransferStatus result = EBulkTransferStatus::eError; + + if (transferResult == LIBUSB_SUCCESS) { + result = EBulkTransferStatus::eSuccess; + } + else if (transferResult == LIBUSB_ERROR_TIMEOUT) { + result = EBulkTransferStatus::eTimeout; + } + else if (transferResult == LIBUSB_ERROR_NO_DEVICE) { + result = EBulkTransferStatus::eNoDevice; + } + + return result; +} + } // namespace usb } // namespace oscilloscope /******************************************************************************/ diff --git a/usb/usb_device.h b/usb/usb_device.h index 1a43d05..27b06c5 100644 --- a/usb/usb_device.h +++ b/usb/usb_device.h @@ -48,8 +48,10 @@ enum class EConnectionStatus { eDisconnected, /**< Device released and closed */ eInitializationFailed, /**< libusb context initialization failed */ eDeviceNotFound, /**< Matching device was not found */ + eFirmwareLoadFailed, /**< FX2 firmware upload failed */ eOpenFailed, /**< libusb_open failed */ eClaimInterfaceFailed, /**< libusb_claim_interface failed */ + eSetInterfaceFailed, /**< libusb_set_interface_alt_setting failed */ eReleaseFailed /**< libusb_release_interface failed */ }; @@ -83,6 +85,21 @@ struct SUsbConnectionResult { std::string errorMessage; /**< libusb error string when applicable */ }; +/** @brief Describes the outcome of a bulk endpoint transfer */ +enum class EBulkTransferStatus { + 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 */ +}; + +/** @brief Holds the result of a bulk endpoint transfer */ +struct SBulkTransferResult { + EBulkTransferStatus status; /**< Transfer outcome */ + int transferredBytes; /**< Bytes actually transferred */ + std::string errorMessage; /**< libusb error string when applicable */ +}; + /********************* Application Programming Interface *********************/ /** @@ -96,6 +113,8 @@ SUsbScanResult enumerateSupportedDevices(); * @param[in] deviceInfo Device descriptor returned by a scan result * @param[out] connection Connection state container that is filled in * @returns Connection status and error message when applicable + * @note Uploads the FX2 firmware first when the device is still in its bare + * bootloader state; see `usb/firmware_loader.h`. */ SUsbConnectionResult connectToDevice( const SUsbDeviceInfo &deviceInfo, @@ -109,8 +128,45 @@ SUsbConnectionResult connectToDevice( */ SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection); +/** + * @brief Writes to a bulk OUT endpoint, retrying on timeout + * @param[in] connection Active connection to write through + * @param[in] endpointAddress Bulk OUT endpoint address (e.g. 0x02) + * @param[in] data Buffer to send + * @param[in] length Number of bytes to send + * @param[in] timeoutMs Per-attempt timeout in milliseconds + * @param[in] attempts Number of attempts before giving up on timeout + * @returns Transfer status, bytes transferred, and error message + */ +SBulkTransferResult bulkWrite( + const SUsbConnection &connection, + uint8_t endpointAddress, + const uint8_t *data, + int length, + unsigned int timeoutMs = 500U, + unsigned int attempts = 3U +); + +/** + * @brief Reads from a bulk IN endpoint, retrying on timeout + * @param[in] connection Active connection to read from + * @param[in] endpointAddress Bulk IN endpoint address (e.g. 0x86) + * @param[out] buffer Buffer to receive into + * @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 + * @returns Transfer status, bytes transferred, and error message + */ +SBulkTransferResult bulkRead( + const SUsbConnection &connection, + uint8_t endpointAddress, + uint8_t *buffer, + int length, + unsigned int timeoutMs = 500U, + unsigned int attempts = 3U +); + } // namespace usb } // namespace oscilloscope - -#endif // USB_DEVICE_H_ /******************************************************************************/ +#endif // USB_DEVICE_H_ From a23e023de2b2eb82ba19e1706673825da556c0b6 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 12:40:33 +0300 Subject: [PATCH 2/7] Implemented endpoint read loop --- CMakeLists.txt | 4 + HISTORY.md | 34 ++++++ README.md | 47 +++++--- app/main.cpp | 47 +++++++- capture/acquisition_loop.cpp | 170 +++++++++++++++++++++++++++ capture/acquisition_loop.h | 75 ++++++++++++ docs/mainpage.md | 30 ++++- usb/80-hantek-dso-2250.rules | 1 + usb/81-hantek-dso.rules | 20 ---- usb/usb_device.cpp | 216 ++++++++++++++++++++++++++++++----- usb/usb_device.h | 56 ++++++++- 11 files changed, 621 insertions(+), 79 deletions(-) create mode 100644 capture/acquisition_loop.cpp create mode 100644 capture/acquisition_loop.h delete mode 100644 usb/81-hantek-dso.rules diff --git a/CMakeLists.txt b/CMakeLists.txt index 79018f2..90b2b1e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ FetchContent_MakeAvailable(imgui) find_package(OpenGL REQUIRED) find_package(SDL2 REQUIRED) find_package(PkgConfig REQUIRED) +find_package(Threads REQUIRED) pkg_check_modules(LIBUSB REQUIRED IMPORTED_TARGET libusb-1.0) set(APP_NAME run) @@ -34,6 +35,7 @@ set(SOURCES_LIST app/main.cpp usb/usb_device.cpp usb/firmware_loader.cpp + capture/acquisition_loop.cpp ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_tables.cpp @@ -45,6 +47,7 @@ set(SOURCES_LIST set(HEADERS_LIST usb/usb_device.h usb/firmware_loader.h + capture/acquisition_loop.h ) if(CMAKE_BUILD_TYPE MATCHES "Debug") @@ -71,5 +74,6 @@ target_link_libraries(${APP_NAME} PRIVATE SDL2::SDL2 OpenGL::GL PkgConfig::LIBUSB + Threads::Threads ) diff --git a/HISTORY.md b/HISTORY.md index 4455619..9ba2d0b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -122,3 +122,37 @@ Records key decisions, structural changes, and completed development stages. - Read endpoint data chunks in a dedicated acquisition path. - Add buffering between USB reads and waveform processing. - Handle timeouts, I/O errors, disconnects, and recovery states. + +## 2026-09-04 + +### Stage 3 - Endpoint read loop complete + +- Corrected the extracted DSO-2250 loader and firmware images and verified + their Intel HEX checksums; the proprietary files remain excluded from Git. +- Added support for the operational `04b5:2250` identity alongside the + `04b4:2250` bootloader identity. +- Wait for FX2 re-enumeration with bounded polling after firmware upload and + reconnect to the operational device on interface 0, alternate setting 0. +- Extended the supplied udev rule to grant access to both USB identities. +- Added vendor control-transfer helpers and the DSO-2250 initialization + sequence required before endpoint polling. +- Implemented a dedicated acquisition thread that sends the capture-state + request to bulk endpoint `0x02` and reads 512-byte responses from endpoint + `0x86`. +- Added successful-poll, transfer-error, and last-capture-state tracking with + thread-safe counters. +- Made endpoint activity follow the Start/Stop lifecycle instead of beginning + at Connect; Stop, Disconnect, mode changes, application shutdown, and device + loss stop and join the acquisition thread before releasing USB resources. +- Disabled Start until a live device is connected and disabled device rescans + while a connection is active. +- Select Live mode at startup when a supported device is present, otherwise + retain Demo mode. + +### Hardware verification + +- Verified firmware re-enumeration from `04b4:2250` to `04b5:2250` on a + physical Hantek DSO-2250. +- Verified successful `B3`, `B2`, bulk OUT, and 512-byte bulk IN transfers. +- Confirmed the instrument LED lifecycle: red after Connect, green during + Start/acquisition, red after Stop, and off after Disconnect. diff --git a/README.md b/README.md index e1a05b5..9c43bf0 100644 --- a/README.md +++ b/README.md @@ -11,20 +11,25 @@ is developed independently and has no Qt dependency. ## Status -Current version: `0.2.0`. +Current version: `0.2.2`. -The current iteration provides the first UI shell: +The current iteration provides a working USB connection and endpoint polling +path for the Hantek DSO-2250: - CMake build configuration for C++14; - `Debug` and `Release` build targets; - a Makefile and Bash build script for Linux; - an SDL2 window with an OpenGL 3 context; - Dear ImGui integration, a menu, control panel, status line, and display grid; -- a pinned Dear ImGui source dependency (`v1.90.9`) fetched by CMake. +- a pinned Dear ImGui source dependency (`v1.90.9`) fetched by CMake; +- 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. -The shell provides presentation-only controls for acquisition state, demo mode, -timebase, and the two channel scales. USB acquisition and waveform processing -are planned for later iterations. +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. ## Planned Stack @@ -63,12 +68,13 @@ The current application requires: - SDL2 development files; - OpenGL development files. - libusb-1.0 development files. +- `fxload` for uploading the RAM-resident FX2 firmware. On Debian, Ubuntu, and Linux Mint: ```bash sudo apt-get update -sudo apt-get install build-essential cmake make pkg-config libsdl2-dev libgl1-mesa-dev libusb-1.0-0-dev +sudo apt-get install build-essential cmake make pkg-config libsdl2-dev libgl1-mesa-dev libusb-1.0-0-dev fxload ``` Dear ImGui is downloaded automatically by CMake during configuration. @@ -76,8 +82,9 @@ Dear ImGui is downloaded automatically by CMake during configuration. ### Hantek DSO-2250 USB Access Linux requires a udev rule for a regular desktop user to open the Hantek device -through libusb. Install the supplied rule, reload udev rules, then reconnect the -oscilloscope: +through libusb. The rule covers both the `04b4:2250` bootloader identity and the +`04b5:2250` operational identity. Install the supplied rule, reload udev rules, +then reconnect the oscilloscope: ```bash sudo cp usb/80-hantek-dso-2250.rules /etc/udev/rules.d/ @@ -85,6 +92,20 @@ sudo udevadm control --reload-rules sudo udevadm trigger ``` +### Hantek DSO-2250 Firmware + +The FX2 firmware is proprietary and is not stored in this repository. Supply +the extracted files at these default paths: + +```text +firmware/DSO2250_loader.hex +firmware/DSO2250_firmware.hex +``` + +Set `OSCILLOSCOPE_FIRMWARE_DIR` to use another directory. On Connect, the +application uploads the firmware to a `04b4:2250` device with `fxload`, waits +for it to re-enumerate as `04b5:2250`, and then opens the operational device. + ## Build ### Makefile @@ -171,10 +192,10 @@ CI can update only the version metadata by passing `--skip-build`. ## Next Steps -1. Add a demo waveform to the display grid. -2. Implement a two-channel model, timebase, and basic controls. -3. Add libusb support and a safe acquisition thread. -4. Handle device disconnection and fallback to demo mode. +1. Decode capture-state responses and acquired sample packets. +2. Add buffering between USB acquisition and waveform processing. +3. Render live and demo waveforms on the display grid. +4. Implement the two-channel model, timebase, and instrument controls. The full goals, constraints, and architecture are documented in `WorkingDocs/TECHNICAL_SPECIFICATION.md`. diff --git a/app/main.cpp b/app/main.cpp index a9aee54..4ed6147 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -72,8 +72,10 @@ #include #include +#include "capture/acquisition_loop.h" #include "usb/usb_device.h" +using oscilloscope::capture::SAcquisitionLoop; using oscilloscope::usb::EScanStatus; using oscilloscope::usb::SUsbConnection; using oscilloscope::usb::SUsbConnectionResult; @@ -119,6 +121,7 @@ static void updateDemoMode( bool *demoMode, SUsbScanResult *usbScanResult, SUsbConnection *connection, + SAcquisitionLoop *acquisitionLoop, SUsbDeviceInfo *connectedDevice, std::string *deviceStatus ); @@ -172,13 +175,16 @@ int main (void) { bool running = true; bool acquisitionRunning = false; - bool demoMode = true; bool channelEnabled[] = {true, true}; int timebase = 6; int voltsPerDivision[] = {2, 2}; SUsbScanResult usbScanResult = oscilloscope::usb::enumerateSupportedDevices(); + bool demoMode = + (usbScanResult.status != EScanStatus::eSuccess) || + usbScanResult.devices.empty(); SUsbConnection usbConnection = {NULL, NULL, 0U, false}; + SAcquisitionLoop acquisitionLoop; SUsbDeviceInfo connectedDevice = {0U, 0U, 0U, 0U, NULL}; std::string deviceStatus = formatUsbConnectionStatus( usbScanResult, @@ -221,9 +227,11 @@ int main (void) { &demoMode, &usbScanResult, &usbConnection, + &acquisitionLoop, &connectedDevice, &deviceStatus ); + acquisitionRunning = false; } ImGui::EndMenu(); } @@ -264,12 +272,31 @@ int main (void) { ImGui::SameLine(); ImGui::BeginChild("Controls", ImVec2(260.0f, 0.0f), true); + ImGui::BeginDisabled(!(demoMode || usbConnection.isConnected)); if ( ImGui::Button( acquisitionRunning ? "Stop" : "Start", ImVec2(-1.0f, 32.0f)) ) { - acquisitionRunning = !acquisitionRunning; + if (acquisitionRunning) { + if (!demoMode) { + oscilloscope::capture::stopAcquisitionLoop( + &acquisitionLoop + ); + } + acquisitionRunning = false; + } + else { + if (!demoMode) { + oscilloscope::capture::startAcquisitionLoop( + &acquisitionLoop, + usbConnection + ); + } + acquisitionRunning = true; + } } + ImGui::EndDisabled(); + ImGui::BeginDisabled(usbConnection.isConnected); if (ImGui::Button("Rescan devices", ImVec2(-1.0f, 32.0f))) { usbScanResult = oscilloscope::usb::enumerateSupportedDevices(); @@ -277,6 +304,8 @@ int main (void) { usbConnection.isConnected && !isUsbDevicePresent(usbScanResult, connectedDevice) ) { + oscilloscope::capture::stopAcquisitionLoop(&acquisitionLoop); + acquisitionRunning = false; const SUsbConnectionResult disconnectResult = oscilloscope::usb::disconnectFromDevice(&usbConnection); @@ -301,9 +330,12 @@ int main (void) { ); } } + ImGui::EndDisabled(); if (usbConnection.isConnected) { if (ImGui::Button("Disconnect", ImVec2(-1.0f, 32.0f))) { + oscilloscope::capture::stopAcquisitionLoop(&acquisitionLoop); + acquisitionRunning = false; const SUsbConnectionResult disconnectResult = oscilloscope::usb::disconnectFromDevice(&usbConnection); @@ -376,20 +408,24 @@ int main (void) { &demoMode, &usbScanResult, &usbConnection, + &acquisitionLoop, &connectedDevice, &deviceStatus ); + acquisitionRunning = false; } ImGui::EndChild(); ImGui::SetCursorScreenPos(statusPosition); ImGui::Text( - "%s | %s | %s | CH1 %s | CH2 %s", + "%s | %s | %s | CH1 %s | CH2 %s | polls %lu err %lu", acquisitionRunning ? "Acquiring" : "Stopped", demoMode ? "Demo mode" : "Live mode", deviceStatus.c_str(), channelEnabled[0] ? "on" : "off", - channelEnabled[1] ? "on" : "off" + channelEnabled[1] ? "on" : "off", + acquisitionLoop.status.pollCount.load(), + acquisitionLoop.status.errorCount.load() ); ImGui::End(); @@ -408,6 +444,7 @@ int main (void) { ImGui_ImplSDL2_Shutdown(); ImGui::DestroyContext(); if (usbConnection.isConnected) { + oscilloscope::capture::stopAcquisitionLoop(&acquisitionLoop); oscilloscope::usb::disconnectFromDevice(&usbConnection); } SDL_GL_DeleteContext(glContext); @@ -480,6 +517,7 @@ static void updateDemoMode( bool *demoMode, SUsbScanResult *usbScanResult, SUsbConnection *connection, + SAcquisitionLoop *acquisitionLoop, SUsbDeviceInfo *connectedDevice, std::string *deviceStatus ) { @@ -489,6 +527,7 @@ static void updateDemoMode( if (*demoMode) { if (connection->isConnected) { + oscilloscope::capture::stopAcquisitionLoop(acquisitionLoop); const SUsbConnectionResult disconnectResult = oscilloscope::usb::disconnectFromDevice(connection); diff --git a/capture/acquisition_loop.cpp b/capture/acquisition_loop.cpp new file mode 100644 index 0000000..f260b68 --- /dev/null +++ b/capture/acquisition_loop.cpp @@ -0,0 +1,170 @@ +/** + * @file acquisition_loop.cpp + * @version 0.2.2 + * @authors Anton Chernov + * @date 2026-09-02 + * @date @showdate "%Y-%m-%d" + * @par + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/******************************* Included files *******************************/ +#include +#include + +#include "acquisition_loop.h" + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace capture { + +/****************************** Module variables ******************************/ + +/** @brief Hantek DSO-2250 bulk endpoint addresses */ +static const uint8_t EP_BULK_OUT = 0x02U; +static const uint8_t EP_BULK_IN = 0x86U; +static const uint16_t EP_BULK_IN_MAX_PACKET_LEN = 512U; + +/** @brief Vendor control requests used by the polling cycle */ +static const uint8_t CONTROL_GETSPEED = 0xB2U; +static const uint8_t CONTROL_BEGINCOMMAND = 0xB3U; + +/** @brief Protocol command byte for the "get capture state" request */ +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; + +/***************************** Private prototypes *****************************/ + +/** + * @brief Background thread entry point that polls the capture state + * @param[in,out] loop Loop control block to update + * @param[in] connection USB connection to poll + */ +static void pollCaptureState( + SAcquisitionLoop *loop, + usb::SUsbConnection connection +); + +/********************* Application Programming Interface **********************/ + +/** @fn startAcquisitionLoop */ +void startAcquisitionLoop( + SAcquisitionLoop *loop, + usb::SUsbConnection connection +) { + if (loop != NULL) { + loop->stopRequested.store(false); + loop->status.pollCount.store(0UL); + loop->status.errorCount.store(0UL); + loop->status.lastCaptureState.store(-1); + loop->workerThread = std::thread(pollCaptureState, loop, connection); + } +} + +/** @fn stopAcquisitionLoop */ +void stopAcquisitionLoop(SAcquisitionLoop *loop) { + if (loop != NULL) { + loop->stopRequested.store(true); + if (loop->workerThread.joinable()) { + loop->workerThread.join(); + } + } +} + +/****************************** Private functions *****************************/ + +/** @fn pollCaptureState */ +static void pollCaptureState( + SAcquisitionLoop *loop, + usb::SUsbConnection connection +) { + 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 readResult = {usb::EUsbTransferStatus::eError, 0, ""}; + + while (!loop->stopRequested.load()) { + beginResult = usb::controlWrite( + connection, + CONTROL_BEGINCOMMAND, + beginCommandPayload, + sizeof(beginCommandPayload) + ); + speedResult = usb::controlRead( + connection, CONTROL_GETSPEED, speedBuffer, sizeof(speedBuffer) + ); + writeResult = usb::bulkWrite( + connection, + EP_BULK_OUT, + captureStateCommand, + sizeof(captureStateCommand) + ); + readSpeedResult = usb::controlRead( + connection, CONTROL_GETSPEED, speedBuffer, sizeof(speedBuffer) + ); + readResult = usb::bulkRead( + connection, EP_BULK_IN, response, EP_BULK_IN_MAX_PACKET_LEN + ); + + 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) + ); + } +} + +} // namespace capture +} // namespace oscilloscope +/******************************************************************************/ diff --git a/capture/acquisition_loop.h b/capture/acquisition_loop.h new file mode 100644 index 0000000..869420a --- /dev/null +++ b/capture/acquisition_loop.h @@ -0,0 +1,75 @@ +/** + * @file acquisition_loop.h + * @version 0.2.2 + * @authors Anton Chernov + * @date 2026-09-02 + * @date @showdate "%Y-%m-%d" + * @par + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ACQUISITION_LOOP_H_ +#define ACQUISITION_LOOP_H_ + +/******************************* Included files ******************************/ +#include +#include + +#include "usb/usb_device.h" + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace capture { + +/** @brief Poll counters and last observed 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 */ +}; + +/** @brief Owns the background polling thread and its shared status */ +struct SAcquisitionLoop { + std::thread workerThread; /**< Background polling thread */ + std::atomic stopRequested{false}; /**< Set to request a stop */ + SAcquisitionStatus status; /**< Shared poll status */ +}; + +/********************* Application Programming Interface *********************/ + +/** + * @brief Starts the background capture-state polling thread + * @param[in,out] loop Loop control block to start; must not already be running + * @param[in] connection Active USB connection to poll (copied for the thread) + * @note This is the host activity that keeps the device's status LED + * blinking; there is no separate "ping" command in the protocol. + */ +void startAcquisitionLoop( + SAcquisitionLoop *loop, + usb::SUsbConnection connection +); + +/** + * @brief Requests the polling thread to stop and joins it + * @param[in,out] loop Loop control block to stop + * @note Must be called before disconnecting the underlying USB connection. + */ +void stopAcquisitionLoop(SAcquisitionLoop *loop); + +} // namespace capture +} // namespace oscilloscope +/******************************************************************************/ +#endif // ACQUISITION_LOOP_H_ diff --git a/docs/mainpage.md b/docs/mainpage.md index e8d9b17..64aef88 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -22,8 +22,13 @@ of the new implementation. - display grid, control panel, menu bar, and status line; - Demo and Live modes; - supported-device enumeration through libusb; -- detection of all connected Hantek DSO-2250 instruments (`0x04B4:0x2250`); -- manual USB rescan and automatic scan when entering Live mode. +- detection of the Hantek DSO-2250 bootloader (`0x04B4:0x2250`) and + operational device (`0x04B5:0x2250`); +- FX2 firmware upload through `fxload` and bounded re-enumeration polling; +- USB interface claiming and clean connect/disconnect handling; +- a Start/Stop-controlled acquisition thread; +- capture-state polling through vendor control requests and bulk endpoints; +- successful-poll and transfer-error counters in the status line. ## Architecture @@ -36,13 +41,25 @@ of the new implementation. | `render/` | Waveform rendering | | `ui/` | Reusable interface components | -## USB device discovery +## USB device operation The USB module enumerates the full libusb device list and compares every descriptor against its central table of supported VID/PID pairs. Each match is reported with its model name, VID/PID, bus number, and address, allowing multiple identical instruments to be distinguished. +An uninitialized DSO-2250 appears as `04b4:2250`. The application uploads the +externally supplied FX2 firmware, waits for the instrument to re-enumerate as +`04b5:2250`, and opens its bulk endpoints on interface 0, alternate setting 0. +The firmware files are proprietary and intentionally excluded from the +repository. + +Pressing Connect prepares the USB device without starting acquisition. Pressing +Start launches a background loop that sends the capture-state command through +bulk endpoint `0x02` and reads a 512-byte response from endpoint `0x86`, with +the required `B3` and `B2` vendor requests. Stop terminates and joins the loop +before the connection can be released. + See `oscilloscope::usb::enumerateSupportedDevices()` for the public discovery API. @@ -54,7 +71,8 @@ complete installation, build, and run instructions. ## Planned work -The next USB tasks are device connection, interface claiming, endpoint reads, -buffering, and recovery from I/O errors or disconnects. Waveform processing, -instrument controls, and persistent configuration follow in later milestones. +The next tasks are decoding capture responses and sample packets, buffering +data between acquisition and rendering, and displaying live waveforms. +Instrument controls, broader recovery behavior, and persistent configuration +follow in later milestones. diff --git a/usb/80-hantek-dso-2250.rules b/usb/80-hantek-dso-2250.rules index 162ffd0..dbecca2 100644 --- a/usb/80-hantek-dso-2250.rules +++ b/usb/80-hantek-dso-2250.rules @@ -1,2 +1,3 @@ # Permit the active local user to access the Hantek DSO-2250 via libusb. SUBSYSTEM=="usb", ATTR{idVendor}=="04b4", ATTR{idProduct}=="2250", TAG+="uaccess" +SUBSYSTEM=="usb", ATTR{idVendor}=="04b5", ATTR{idProduct}=="2250", TAG+="uaccess" diff --git a/usb/81-hantek-dso.rules b/usb/81-hantek-dso.rules deleted file mode 100644 index 172baa3..0000000 --- a/usb/81-hantek-dso.rules +++ /dev/null @@ -1,20 +0,0 @@ -# Hantek DSO-2090 -SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/2090/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO2090_firmware.hex -s /usr/share/HantekDSO/DSO2090_loader.hex -D $env{DEVNAME}" - -# Hantek DSO-2100 -SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/2100/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO2100_firmware.hex -s /usr/share/HantekDSO/DSO2100_loader.hex -D $env{DEVNAME}" - -# Hantek DSO-2100 -SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="547/1006/*", RUN+="/usr/sbin/fxload -t an21 -I /usr/share/HantekDSO/DSO2100_firmware.hex -s /usr/share/HantekDSO/DSO2100_loader.hex -D $env{DEVNAME}" - -# Hantek DSO-2150 -SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/2150/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO2150_firmware.hex -s /usr/share/HantekDSO/DSO2150_loader.hex -D $env{DEVNAME}" - -# Hantek DSO-2250 -SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/2250/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO2250_firmware.hex -s /usr/share/HantekDSO/DSO2250_loader.hex -D $env{DEVNAME}" - -# Hantek DSO-5200 -SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/5200/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO5200_firmware.hex -s /usr/share/HantekDSO/DSO5200_loader.hex -D $env{DEVNAME}" - -# Hantek DSO-5200A -SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", ENV{PRODUCT}=="4b4/520a/*", RUN+="/usr/sbin/fxload -t fx2 -I /usr/share/HantekDSO/DSO520A_firmware.hex -s /usr/share/HantekDSO/DSO520A_loader.hex -D $env{DEVNAME}" diff --git a/usb/usb_device.cpp b/usb/usb_device.cpp index 3746813..c57c6a8 100644 --- a/usb/usb_device.cpp +++ b/usb/usb_device.cpp @@ -40,22 +40,32 @@ namespace usb { struct SSupportedDevice { uint16_t vendorId; uint16_t productId; + uint16_t operationalVendorId; uint8_t interfaceNumber; uint8_t alternateSetting; + bool requiresFirmware; const char *modelName; const char *firmwareBaseName; }; -/** @brief Microseconds to wait for the FX2 chip to re-enumerate after upload */ -static const unsigned int FIRMWARE_REENUMERATION_DELAY_US = 1500000U; +/** @brief Delay between scans while waiting for FX2 re-enumeration */ +static const unsigned int FIRMWARE_REENUMERATION_POLL_DELAY_US = 100000U; +/** @brief Maximum scans while waiting for the operational USB device */ +static const unsigned int FIRMWARE_REENUMERATION_ATTEMPTS = 50U; /****************************** Module variables ******************************/ /** @brief Maps supported USB VID/PID pairs to model display names */ static const SSupportedDevice supported_devices[] = { - /* Alt setting 0 exposes no endpoints; alt setting 1 has the bulk pair - * (EP2 OUT / EP6 IN) that the FX2 firmware actually uses. */ - {0x04B4U, 0x2250U, 0U, 1U, "Hantek DSO-2250", "DSO2250"} + /* The bootloader exposes the bulk pair on alt setting 1. */ + { + 0x04B4U, 0x2250U, 0x04B5U, 0U, 1U, true, + "Hantek DSO-2250 Bootloader", "DSO2250" + }, + { + 0x04B5U, 0x2250U, 0x04B5U, 0U, 0U, false, + "Hantek DSO-2250", "DSO2250" + } }; /***************************** Private prototypes *****************************/ @@ -123,7 +133,15 @@ static SFirmwarePaths resolveFirmwarePaths( * @param[in] transferResult Return value of `libusb_bulk_transfer` * @returns Classified transfer status */ -static EBulkTransferStatus classifyBulkTransferResult(int transferResult); +static EUsbTransferStatus classifyBulkTransferResult(int transferResult); + +/** + * @brief Maps a libusb control transfer result code to a transfer status + * @param[in] transferResult Return value of `libusb_control_transfer` + * (transferred byte count on success, negative error otherwise) + * @returns Classified transfer status + */ +static EUsbTransferStatus classifyControlTransferResult(int transferResult); /********************* Application Programming Interface **********************/ @@ -197,6 +215,7 @@ SUsbConnectionResult connectToDevice( int initializationResult = LIBUSB_SUCCESS; int openResult = LIBUSB_SUCCESS; int claimResult = LIBUSB_SUCCESS; + unsigned int reenumerationAttempt = 0U; if (connection == NULL) { result.status = EConnectionStatus::eDeviceNotFound; @@ -228,30 +247,62 @@ SUsbConnectionResult connectToDevice( result.errorMessage = "Requested USB device was not found"; } else { - firmwareResult = loadFx2Firmware( - deviceInfo.busNumber, - deviceInfo.deviceAddress, - resolveFirmwarePaths(*supportedDevice) - ); + if (supportedDevice->requiresFirmware) { + firmwareResult = loadFx2Firmware( + deviceInfo.busNumber, + deviceInfo.deviceAddress, + resolveFirmwarePaths(*supportedDevice) + ); + } if (firmwareResult.status != EFirmwareLoadStatus::eLoaded) { result.status = EConnectionStatus::eFirmwareLoadFailed; result.errorMessage = firmwareResult.errorMessage; } - else { + else if (supportedDevice->requiresFirmware) { /* The FX2 chip re-enumerates once firmware starts * running, so the device pointer above is stale and * its bus address may have changed. */ - usleep(FIRMWARE_REENUMERATION_DELAY_US); libusb_free_device_list(deviceList, 1); deviceList = NULL; - deviceCount = libusb_get_device_list(context, &deviceList); - device = findDeviceByVidPid( - deviceList, - deviceInfo.vendorId, - deviceInfo.productId, - deviceCount - ); + device = NULL; + + for ( + reenumerationAttempt = 0U; + (reenumerationAttempt < + FIRMWARE_REENUMERATION_ATTEMPTS) && + (device == NULL); + ++reenumerationAttempt + ) { + usleep(FIRMWARE_REENUMERATION_POLL_DELAY_US); + deviceCount = libusb_get_device_list( + context, + &deviceList + ); + + if (deviceCount >= 0) { + device = findDeviceByVidPid( + deviceList, + supportedDevice->operationalVendorId, + deviceInfo.productId, + deviceCount + ); + + if (device != NULL) { + supportedDevice = findSupportedDevice( + supportedDevice->operationalVendorId, + deviceInfo.productId + ); + } + } + + if (device == NULL) { + if (deviceList != NULL) { + libusb_free_device_list(deviceList, 1); + deviceList = NULL; + } + } + } if ((deviceCount < 0) || (device == NULL)) { result.status = EConnectionStatus::eDeviceNotFound; @@ -370,7 +421,7 @@ SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection) { } /** @fn bulkWrite */ -SBulkTransferResult bulkWrite( +SUsbTransferResult bulkWrite( const SUsbConnection &connection, const uint8_t endpointAddress, const uint8_t *data, @@ -378,7 +429,7 @@ SBulkTransferResult bulkWrite( const unsigned int timeoutMs, const unsigned int attempts ) { - SBulkTransferResult result = {EBulkTransferStatus::eError, 0, ""}; + SUsbTransferResult result = {EUsbTransferStatus::eError, 0, ""}; int transferResult = LIBUSB_ERROR_TIMEOUT; int transferredBytes = 0; unsigned int attempt; @@ -400,7 +451,7 @@ SBulkTransferResult bulkWrite( result.transferredBytes = transferredBytes; result.status = classifyBulkTransferResult(transferResult); - if (result.status != EBulkTransferStatus::eSuccess) { + if (result.status != EUsbTransferStatus::eSuccess) { result.errorMessage = libusb_error_name(transferResult); } @@ -408,7 +459,7 @@ SBulkTransferResult bulkWrite( } /** @fn bulkRead */ -SBulkTransferResult bulkRead( +SUsbTransferResult bulkRead( const SUsbConnection &connection, const uint8_t endpointAddress, uint8_t *buffer, @@ -416,7 +467,7 @@ SBulkTransferResult bulkRead( const unsigned int timeoutMs, const unsigned int attempts ) { - SBulkTransferResult result = {EBulkTransferStatus::eError, 0, ""}; + SUsbTransferResult result = {EUsbTransferStatus::eError, 0, ""}; int transferResult = LIBUSB_ERROR_TIMEOUT; int transferredBytes = 0; unsigned int attempt; @@ -438,7 +489,91 @@ SBulkTransferResult bulkRead( result.transferredBytes = transferredBytes; result.status = classifyBulkTransferResult(transferResult); - if (result.status != EBulkTransferStatus::eSuccess) { + if (result.status != EUsbTransferStatus::eSuccess) { + result.errorMessage = libusb_error_name(transferResult); + } + + return result; +} + +/** @fn controlWrite */ +SUsbTransferResult controlWrite( + const SUsbConnection &connection, + const uint8_t request, + const uint8_t *data, + const uint16_t length, + const uint16_t value, + const uint16_t index, + const unsigned int timeoutMs, + const unsigned int attempts +) { + SUsbTransferResult result = {EUsbTransferStatus::eError, 0, ""}; + int transferResult = LIBUSB_ERROR_TIMEOUT; + unsigned int attempt; + + for ( + attempt = 0U; + (attempt < attempts) && (transferResult == LIBUSB_ERROR_TIMEOUT); + ++attempt + ) { + transferResult = libusb_control_transfer( + connection.handle, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, + request, + value, + index, + const_cast(data), + length, + timeoutMs + ); + } + + result.transferredBytes = (transferResult >= 0) ? transferResult : 0; + result.status = classifyControlTransferResult(transferResult); + if (result.status != EUsbTransferStatus::eSuccess) { + result.errorMessage = libusb_error_name(transferResult); + } + + return result; +} + +/** @fn controlRead */ +SUsbTransferResult controlRead( + const SUsbConnection &connection, + const uint8_t request, + uint8_t *buffer, + const uint16_t length, + const uint16_t value, + const uint16_t index, + const unsigned int timeoutMs, + const unsigned int attempts +) { + SUsbTransferResult result = {EUsbTransferStatus::eError, 0, ""}; + int transferResult = LIBUSB_ERROR_TIMEOUT; + unsigned int attempt; + + for ( + attempt = 0U; + (attempt < attempts) && (transferResult == LIBUSB_ERROR_TIMEOUT); + ++attempt + ) { + transferResult = libusb_control_transfer( + connection.handle, + LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | + LIBUSB_RECIPIENT_DEVICE, + request, + value, + index, + buffer, + length, + timeoutMs + ); + } + + result.transferredBytes = (transferResult >= 0) ? transferResult : 0; + result.status = classifyControlTransferResult(transferResult); + if (result.status != EUsbTransferStatus::eSuccess) { result.errorMessage = libusb_error_name(transferResult); } @@ -581,17 +716,36 @@ static bool firmwareFileExists(const std::string &path) { } /** @fn classifyBulkTransferResult */ -static EBulkTransferStatus classifyBulkTransferResult(const int transferResult) { - EBulkTransferStatus result = EBulkTransferStatus::eError; +static EUsbTransferStatus classifyBulkTransferResult(const int transferResult) { + EUsbTransferStatus result = EUsbTransferStatus::eError; if (transferResult == LIBUSB_SUCCESS) { - result = EBulkTransferStatus::eSuccess; + result = EUsbTransferStatus::eSuccess; + } + else if (transferResult == LIBUSB_ERROR_TIMEOUT) { + result = EUsbTransferStatus::eTimeout; + } + else if (transferResult == LIBUSB_ERROR_NO_DEVICE) { + result = EUsbTransferStatus::eNoDevice; + } + + return result; +} + +/** @fn classifyControlTransferResult */ +static EUsbTransferStatus classifyControlTransferResult( + const int transferResult +) { + EUsbTransferStatus result = EUsbTransferStatus::eError; + + if (transferResult >= 0) { + result = EUsbTransferStatus::eSuccess; } else if (transferResult == LIBUSB_ERROR_TIMEOUT) { - result = EBulkTransferStatus::eTimeout; + result = EUsbTransferStatus::eTimeout; } else if (transferResult == LIBUSB_ERROR_NO_DEVICE) { - result = EBulkTransferStatus::eNoDevice; + result = EUsbTransferStatus::eNoDevice; } return result; diff --git a/usb/usb_device.h b/usb/usb_device.h index 27b06c5..d076974 100644 --- a/usb/usb_device.h +++ b/usb/usb_device.h @@ -86,7 +86,7 @@ struct SUsbConnectionResult { }; /** @brief Describes the outcome of a bulk endpoint transfer */ -enum class EBulkTransferStatus { +enum class EUsbTransferStatus { eSuccess, /**< The full requested length was transferred */ eTimeout, /**< All retry attempts timed out */ eNoDevice, /**< The device was disconnected mid-transfer */ @@ -94,8 +94,8 @@ enum class EBulkTransferStatus { }; /** @brief Holds the result of a bulk endpoint transfer */ -struct SBulkTransferResult { - EBulkTransferStatus status; /**< Transfer outcome */ +struct SUsbTransferResult { + EUsbTransferStatus status; /**< Transfer outcome */ int transferredBytes; /**< Bytes actually transferred */ std::string errorMessage; /**< libusb error string when applicable */ }; @@ -138,7 +138,7 @@ SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection); * @param[in] attempts Number of attempts before giving up on timeout * @returns Transfer status, bytes transferred, and error message */ -SBulkTransferResult bulkWrite( +SUsbTransferResult bulkWrite( const SUsbConnection &connection, uint8_t endpointAddress, const uint8_t *data, @@ -157,7 +157,7 @@ SBulkTransferResult bulkWrite( * @param[in] attempts Number of attempts before giving up on timeout * @returns Transfer status, bytes transferred, and error message */ -SBulkTransferResult bulkRead( +SUsbTransferResult bulkRead( const SUsbConnection &connection, uint8_t endpointAddress, uint8_t *buffer, @@ -166,6 +166,52 @@ SBulkTransferResult bulkRead( unsigned int attempts = 3U ); +/** + * @brief Sends a vendor control OUT transfer, retrying on timeout + * @param[in] connection Active connection to write through + * @param[in] request Vendor bRequest code + * @param[in] data Buffer to send + * @param[in] length Number of bytes to send + * @param[in] value wValue field + * @param[in] index wIndex field + * @param[in] timeoutMs Per-attempt timeout in milliseconds + * @param[in] attempts Number of attempts before giving up on timeout + * @returns Transfer status, bytes transferred, and error message + */ +SUsbTransferResult controlWrite( + const SUsbConnection &connection, + uint8_t request, + const uint8_t *data, + uint16_t length, + uint16_t value = 0U, + uint16_t index = 0U, + unsigned int timeoutMs = 500U, + unsigned int attempts = 3U +); + +/** + * @brief Reads a vendor control IN transfer, retrying on timeout + * @param[in] connection Active connection to read from + * @param[in] request Vendor bRequest code + * @param[out] buffer Buffer to receive into + * @param[in] length Number of bytes to read + * @param[in] value wValue field + * @param[in] index wIndex field + * @param[in] timeoutMs Per-attempt timeout in milliseconds + * @param[in] attempts Number of attempts before giving up on timeout + * @returns Transfer status, bytes transferred, and error message + */ +SUsbTransferResult controlRead( + const SUsbConnection &connection, + uint8_t request, + uint8_t *buffer, + uint16_t length, + uint16_t value = 0U, + uint16_t index = 0U, + unsigned int timeoutMs = 500U, + unsigned int attempts = 3U +); + } // namespace usb } // namespace oscilloscope /******************************************************************************/ From 2ff8c05fd3066fb10534cd2e10c5f16a82e8e956 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 13:16:41 +0300 Subject: [PATCH 3/7] Added repaired firmware extractor --- .gitignore | 5 +- HISTORY.md | 23 +- README.md | 26 ++- docs/mainpage.md | 6 +- firmware/.gitkeep | 0 tools/dsoextractfw.c | 498 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 547 insertions(+), 11 deletions(-) create mode 100644 firmware/.gitkeep create mode 100644 tools/dsoextractfw.c diff --git a/.gitignore b/.gitignore index 6b085b1..b766ea8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ OldQtCode/ WorkingDocs **/imgui.ini -# Extracted proprietary FX2 firmware -firmware/ +# Extracted FX2 firmware (redistribution rights are not established) +firmware/* +!firmware/.gitkeep diff --git a/HISTORY.md b/HISTORY.md index 9ba2d0b..c7216d3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -110,9 +110,10 @@ Records key decisions, structural changes, and completed development stages. automatically) versus no blinking on Linux with the current codebase (no firmware upload step exists yet, and no `.hex` firmware files are present in this repository). -- The firmware `.hex` files are not included in this repository (proprietary, - extracted from the official Windows driver); they must be supplied - separately before the USB layer can work end-to-end. +- The firmware `.hex` files are not included because the official installer + prohibits unauthorized redistribution; they must be extracted from a + user's copy of the official Windows driver before the USB layer can work + end-to-end. ### Next USB tasks @@ -128,7 +129,8 @@ Records key decisions, structural changes, and completed development stages. ### Stage 3 - Endpoint read loop complete - Corrected the extracted DSO-2250 loader and firmware images and verified - their Intel HEX checksums; the proprietary files remain excluded from Git. + their Intel HEX checksums; the files remain excluded from Git because + redistribution rights have not been established. - Added support for the operational `04b5:2250` identity alongside the `04b4:2250` bootloader identity. - Wait for FX2 re-enumeration with bounded polling after firmware upload and @@ -156,3 +158,16 @@ Records key decisions, structural changes, and completed development stages. - Verified successful `B3`, `B2`, bulk OUT, and 512-byte bulk IN transfers. - Confirmed the instrument LED lifecycle: red after Connect, green during Start/acquisition, red after Stop, and off after Disconnect. + +### Firmware extractor repair + +- Added a maintained extraction utility under `tools/` for users who possess + the official `Dso2250x861.sys` Windows driver. +- Fixed the historical extractor's unhandled padding byte, which inserted an + extra zero and dropped the final data byte in every Intel HEX record. +- Skip empty 22-byte separator records instead of emitting invalid + `:0000000000` lines. +- Fixed loader range calculation and added validation for record layout, EOF + records, and generated checksums. +- Verified extraction from the official `Dso2250x861.sys` driver end to end; + both generated HEX files match the hardware-tested files byte for byte. diff --git a/README.md b/README.md index 9c43bf0..e293a93 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Oscilloscope/ ├── core/ Shared types and application logic. ├── docs/ Project documentation. ├── render/ Oscilloscope waveform rendering. +├── tools/ Optional firmware extraction utilities. ├── ui/ User interface. ├── usb/ USB device communication. ├── WorkingDocs/ Technical specification and device documentation. @@ -69,12 +70,14 @@ The current application requires: - OpenGL development files. - libusb-1.0 development files. - `fxload` for uploading the RAM-resident FX2 firmware. +- binutils development files for building the optional firmware extractor. On Debian, Ubuntu, and Linux Mint: ```bash sudo apt-get update -sudo apt-get install build-essential cmake make pkg-config libsdl2-dev libgl1-mesa-dev libusb-1.0-0-dev fxload +sudo apt-get install build-essential cmake make pkg-config libsdl2-dev \ + libgl1-mesa-dev libusb-1.0-0-dev fxload binutils-dev ``` Dear ImGui is downloaded automatically by CMake during configuration. @@ -94,8 +97,10 @@ sudo udevadm trigger ### Hantek DSO-2250 Firmware -The FX2 firmware is proprietary and is not stored in this repository. Supply -the extracted files at these default paths: +The official installer states that unauthorized reproduction or distribution +of the program, or any portion of it, is prohibited. Because separate firmware +redistribution rights have not been established, the extracted firmware is not +stored in this repository. Supply the files at these default paths: ```text firmware/DSO2250_loader.hex @@ -106,6 +111,21 @@ Set `OSCILLOSCOPE_FIRMWARE_DIR` to use another directory. On Connect, the application uploads the firmware to a `04b4:2250` device with `fxload`, waits for it to re-enumerate as `04b5:2250`, and then opens the operational device. +The repository provides a repaired extractor for users who have the official +32-bit Windows driver. Locate `Dso2250x861.sys` in the extracted driver package, +then run: + +```bash +mkdir -p firmware Output +cc -std=c11 -Wall -Wextra -Wpedantic tools/dsoextractfw.c \ + -o Output/dsoextractfw -lbfd +./Output/dsoextractfw /path/to/Dso2250x861.sys firmware +``` + +The utility validates record sizes, Intel HEX record types, and EOF records, +and calculates checksums while writing `DSO2250_firmware.hex` and +`DSO2250_loader.hex`. + ## Build ### Makefile diff --git a/docs/mainpage.md b/docs/mainpage.md index 64aef88..3e8d418 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -51,8 +51,10 @@ multiple identical instruments to be distinguished. An uninitialized DSO-2250 appears as `04b4:2250`. The application uploads the externally supplied FX2 firmware, waits for the instrument to re-enumerate as `04b5:2250`, and opens its bulk endpoints on interface 0, alternate setting 0. -The firmware files are proprietary and intentionally excluded from the -repository. +The official installer prohibits unauthorized redistribution, so the firmware +is excluded until separate redistribution rights can be confirmed. A repaired +extractor in `tools/dsoextractfw.c` allows owners to generate the required HEX +files from their copy of the official `Dso2250x861.sys` driver. Pressing Connect prepares the USB device without starting acquisition. Pressing Start launches a background loop that sends the capture-state command through diff --git a/firmware/.gitkeep b/firmware/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tools/dsoextractfw.c b/tools/dsoextractfw.c new file mode 100644 index 0000000..853d222 --- /dev/null +++ b/tools/dsoextractfw.c @@ -0,0 +1,498 @@ +/** + * @file dsoextractfw.c + * @version 0.2.2 + * @authors Oleg Khudyakov, Anton Chernov + * @date 2026-09-04 + * @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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +/******************************* Included files *******************************/ +#include +#include +#include +#include +#include +#include + +#include + +/********************************* Definitions ********************************/ + +#define RECORD_SIZE 22U +#define MAX_DATA_SIZE 16U +#define RECORD_COUNT_OFFSET 0U +#define RECORD_ADDRESS_OFFSET 2U +#define RECORD_TYPE_OFFSET 4U +#define RECORD_DATA_OFFSET 5U +#define RECORD_TYPE_DATA 0U +#define RECORD_TYPE_EOF 1U + +typedef struct { + bfd_size_type firmwareOffset; + bfd_size_type loaderOffset; +} SFirmwareOffsets_t; + +/***************************** Private prototypes *****************************/ + +static int extractFirmware(const char *driverPath, const char *outputDir); +static bfd* openDriver(const char *driverPath); +static int findFirmwareOffsets( + bfd *driver, + asection *dataSection, + SFirmwareOffsets_t *offsets +); +static int normalizeSymbolOffset( + const asymbol *symbol, + const asection *section, + bfd_size_type *offset +); +static int writeHexFile( + const char *path, + const uint8_t *records, + bfd_size_type length +); +static int writeHexRecord(FILE *output, const uint8_t *record); +static int isEmptyRecord(const uint8_t *record); +static int buildOutputPath( + char *path, + size_t pathLength, + const char *outputDir, + const char *driverPath, + const char *suffix +); +static size_t getModelNameLength(const char *baseName, size_t stemLength); + +/********************* Application Programming Interface *********************/ + +/** @fn main */ +int main(int argc, char **argv) { + const char *outputDir = "."; + int result = EXIT_FAILURE; + + if ((argc < 2) || (argc > 3)) { + fprintf(stderr, "Usage: %s DRIVER.SYS [OUTPUT_DIR]\n", argv[0]); + } + else { + if (argc == 3) { + outputDir = argv[2]; + } + result = extractFirmware(argv[1], outputDir); + } + + return result; +} + +/****************************** Private functions *****************************/ + +/** @fn extractFirmware */ +static int extractFirmware(const char *driverPath, const char *outputDir) { + bfd *driver = NULL; + asection *dataSection = NULL; + uint8_t *data = NULL; + SFirmwareOffsets_t offsets = {0U, 0U}; + bfd_size_type dataLength = 0U; + bfd_size_type firmwareLength = 0U; + bfd_size_type loaderLength = 0U; + char firmwarePath[1024]; + char loaderPath[1024]; + int result = EXIT_FAILURE; + + bfd_init(); + driver = openDriver(driverPath); + if (driver == NULL) { + fprintf(stderr, "Unable to open driver as a PE/COFF object\n"); + } + else { + dataSection = bfd_get_section_by_name(driver, ".data"); + if (dataSection == NULL) { + fprintf(stderr, "Driver does not contain a .data section\n"); + } + else if (findFirmwareOffsets(driver, dataSection, &offsets) != 0) { + fprintf(stderr, "Driver does not contain firmware symbols\n"); + } + else { + dataLength = bfd_section_size(dataSection); + firmwareLength = offsets.loaderOffset - offsets.firmwareOffset; + loaderLength = dataLength - offsets.loaderOffset; + data = (uint8_t*)malloc(dataLength); + + if (data == NULL) { + perror("Unable to allocate .data buffer"); + } + else if (!bfd_get_section_contents( + driver, dataSection, data, 0U, dataLength + )) { + bfd_perror("Unable to read .data section"); + } + else if ( + (buildOutputPath( + firmwarePath, + sizeof(firmwarePath), + outputDir, + driverPath, + "_firmware.hex" + ) != 0) || + (buildOutputPath( + loaderPath, + sizeof(loaderPath), + outputDir, + driverPath, + "_loader.hex" + ) != 0) + ) { + fprintf(stderr, "Output path is too long\n"); + } + else if ( + writeHexFile( + firmwarePath, + &data[offsets.firmwareOffset], + firmwareLength + ) != 0 + ) { + fprintf(stderr, "Unable to write %s\n", firmwarePath); + } + else if ( + writeHexFile( + loaderPath, + &data[offsets.loaderOffset], + loaderLength + ) != 0 + ) { + fprintf(stderr, "Unable to write %s\n", loaderPath); + } + else { + printf("Wrote %s and %s\n", firmwarePath, loaderPath); + result = EXIT_SUCCESS; + } + } + } + + free(data); + if (driver != NULL) { + bfd_close(driver); + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn openDriver */ +static bfd* openDriver(const char *driverPath) { + static const char * const targets[] = { + NULL, "pei-i386", "efi-app-ia32" + }; + bfd *driver = NULL; + size_t index = 0U; + + for ( + index = 0U; + (index < (sizeof(targets) / sizeof(targets[0]))) && (driver == NULL); + ++index + ) { + driver = bfd_openr(driverPath, targets[index]); + if (driver != NULL) { + if (!bfd_check_format(driver, bfd_object)) { + bfd_close(driver); + driver = NULL; + } + } + } + + return driver; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn findFirmwareOffsets */ +static int findFirmwareOffsets( + bfd *driver, + asection *dataSection, + SFirmwareOffsets_t *offsets +) { + asymbol **symbols = NULL; + long symbolStorage = 0L; + long symbolCount = 0L; + long index = 0L; + int firmwareFound = 0; + int loaderFound = 0; + int result = -1; + + symbolStorage = bfd_get_symtab_upper_bound(driver); + if (symbolStorage > 0L) { + symbols = (asymbol**)malloc((size_t)symbolStorage); + } + + if (symbols != NULL) { + symbolCount = bfd_canonicalize_symtab(driver, symbols); + for (index = 0L; index < symbolCount; ++index) { + if (strcmp(bfd_asymbol_name(symbols[index]), "_firmware") == 0) { + firmwareFound = normalizeSymbolOffset( + symbols[index], dataSection, &offsets->firmwareOffset + ) == 0; + } + else if (strcmp(bfd_asymbol_name(symbols[index]), "_loader") == 0) { + loaderFound = normalizeSymbolOffset( + symbols[index], dataSection, &offsets->loaderOffset + ) == 0; + } + else { + /* Other driver symbols are not relevant. */ + } + } + } + + if ( + (firmwareFound != 0) && + (loaderFound != 0) && + (offsets->firmwareOffset < offsets->loaderOffset) && + (offsets->loaderOffset < bfd_section_size(dataSection)) + ) { + result = 0; + } + + free(symbols); + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn normalizeSymbolOffset */ +static int normalizeSymbolOffset( + const asymbol *symbol, + const asection *section, + bfd_size_type *offset +) { + const bfd_vma value = symbol->value; + const bfd_size_type sectionLength = bfd_section_size(section); + const file_ptr fileOffset = section->filepos; + int result = -1; + + if ( + (value >= (bfd_vma)fileOffset) && + ((value - (bfd_vma)fileOffset) < sectionLength) + ) { + *offset = (bfd_size_type)(value - (bfd_vma)fileOffset); + result = 0; + } + else if (value < sectionLength) { + *offset = (bfd_size_type)value; + result = 0; + } + else { + /* Unsupported symbol encoding. */ + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn writeHexFile */ +static int writeHexFile( + const char *path, + const uint8_t *records, + const bfd_size_type length +) { + FILE *output = NULL; + bfd_size_type offset = 0U; + int eofFound = 0; + int result = -1; + + output = fopen(path, "w"); + if (output == NULL) { + perror(path); + } + else if ((length == 0U) || ((length % RECORD_SIZE) != 0U)) { + fprintf(stderr, "%s: invalid record array length\n", path); + } + else { + result = 0; + for ( + offset = 0U; + (offset < length) && (eofFound == 0) && (result == 0); + offset += RECORD_SIZE + ) { + if (isEmptyRecord(&records[offset]) == 0) { + result = writeHexRecord(output, &records[offset]); + if ( + (records[offset + RECORD_COUNT_OFFSET] == 0U) && + (records[offset + RECORD_TYPE_OFFSET] == RECORD_TYPE_EOF) + ) { + eofFound = 1; + } + } + } + + if (eofFound == 0) { + fprintf(stderr, "%s: Intel HEX EOF record not found\n", path); + result = -1; + } + } + + if (output != NULL) { + if (fclose(output) != 0) { + result = -1; + } + if (result != 0) { + remove(path); + } + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn writeHexRecord */ +static int writeHexRecord(FILE *output, const uint8_t *record) { + const uint8_t count = record[RECORD_COUNT_OFFSET]; + const uint16_t address = + (uint16_t)record[RECORD_ADDRESS_OFFSET] | + ((uint16_t)record[RECORD_ADDRESS_OFFSET + 1U] << 8U); + const uint8_t type = record[RECORD_TYPE_OFFSET]; + uint8_t checksum = count; + uint8_t index = 0U; + int result = -1; + + if ( + (count <= MAX_DATA_SIZE) && + ((type == RECORD_TYPE_DATA) || (type == RECORD_TYPE_EOF)) && + !((type == RECORD_TYPE_EOF) && ((count != 0U) || (address != 0U))) + ) { + checksum = (uint8_t)(checksum + (uint8_t)(address >> 8U)); + checksum = (uint8_t)(checksum + (uint8_t)address); + checksum = (uint8_t)(checksum + type); + fprintf(output, ":%02X%04X%02X", count, address, type); + + for (index = 0U; index < count; ++index) { + checksum = (uint8_t)( + checksum + record[RECORD_DATA_OFFSET + index] + ); + fprintf(output, "%02X", record[RECORD_DATA_OFFSET + index]); + } + + checksum = (uint8_t)(0U - checksum); + fprintf(output, "%02X\n", checksum); + result = ferror(output) == 0 ? 0 : -1; + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn isEmptyRecord */ +static int isEmptyRecord(const uint8_t *record) { + size_t index = 0U; + int result = 1; + + for (index = 0U; (index < RECORD_SIZE) && (result != 0); ++index) { + if (record[index] != 0U) { + result = 0; + } + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn buildOutputPath */ +static int buildOutputPath( + char *path, + const size_t pathLength, + const char *outputDir, + const char *driverPath, + const char *suffix +) { + const char *baseName = strrchr(driverPath, '/'); + const char *extension = NULL; + size_t modelLength = 0U; + int written = 0; + int result = -1; + + baseName = baseName == NULL ? driverPath : baseName + 1; + extension = strrchr(baseName, '.'); + modelLength = extension == NULL ? strlen(baseName) : + (size_t)(extension - baseName); + modelLength = getModelNameLength(baseName, modelLength); + + if ( + (modelLength >= 3U) && + (strncasecmp(baseName, "DSO", 3U) == 0) + ) { + written = snprintf( + path, + pathLength, + "%s/DSO%.*s%s", + outputDir, + (int)(modelLength - 3U), + &baseName[3], + suffix + ); + } + else { + written = snprintf( + path, + pathLength, + "%s/%.*s%s", + outputDir, + (int)modelLength, + baseName, + suffix + ); + } + if ((written >= 0) && ((size_t)written < pathLength)) { + result = 0; + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn getModelNameLength */ +static size_t getModelNameLength( + const char *baseName, + const size_t stemLength +) { + static const char * const architectures[] = {"x86", "AMD64", "IA64"}; + size_t modelLength = stemLength; + size_t architectureLength = 0U; + size_t index = 0U; + + if ( + (modelLength > 0U) && + ((baseName[modelLength - 1U] == '1') || + (baseName[modelLength - 1U] == '2')) + ) { + --modelLength; + } + + for (index = 0U; index < 3U; ++index) { + architectureLength = strlen(architectures[index]); + if ( + (modelLength >= architectureLength) && + (strncasecmp( + &baseName[modelLength - architectureLength], + architectures[index], + architectureLength + ) == 0) + ) { + modelLength -= architectureLength; + } + } + + return modelLength; +} + +/******************************************************************************/ \ No newline at end of file From 7a2c93b1e5c9add43e64495f3cf882c2207fdb8a Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 13:44:57 +0300 Subject: [PATCH 4/7] Updated C++ style guidance --- .github/instructions/cpp-style.instructions.md | 4 ++++ app/main.cpp | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/instructions/cpp-style.instructions.md b/.github/instructions/cpp-style.instructions.md index cba3ea5..3c6275f 100644 --- a/.github/instructions/cpp-style.instructions.md +++ b/.github/instructions/cpp-style.instructions.md @@ -177,6 +177,10 @@ Always put the macro name in the closing comment: #endif /* SOME_OPTION */ ``` +## Preferences + +Prefer a `switch` statement over a long `if`/`else if` chain. + ## Doxygen API documentation Public **declarations** (in `.h`) carry a full Doxygen block; **definitions** diff --git a/app/main.cpp b/app/main.cpp index 4ed6147..53f938a 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -550,8 +550,8 @@ static void updateDemoMode( } } -static void drawOscilloscopeGrid(ImDrawList* drawList, const ImVec2& position, - const ImVec2& size) { +static void drawOscilloscopeGrid(ImDrawList *drawList, const ImVec2 &position, + const ImVec2 &size) { const ImU32 majorColor = IM_COL32(42, 75, 94, 255); const ImU32 minorColor = IM_COL32(25, 46, 60, 255); const int divisionsX = 10; From bd38c59aecd2a6462a1eccb67170fae138aab9e7 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 13:45:35 +0300 Subject: [PATCH 5/7] Added application window icon --- CMakeLists.txt | 9 +++++++++ app/main.cpp | 18 ++++++++++++++++++ oscilloscope.bmp | Bin 0 -> 3126 bytes 3 files changed, 27 insertions(+) create mode 100644 oscilloscope.bmp diff --git a/CMakeLists.txt b/CMakeLists.txt index 90b2b1e..86c8640 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,6 +64,15 @@ endif() add_executable(${APP_NAME} ${SOURCES_LIST}) +add_custom_command( + TARGET ${APP_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/oscilloscope.bmp + $/oscilloscope.bmp + VERBATIM +) + target_include_directories(${APP_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${imgui_SOURCE_DIR} diff --git a/app/main.cpp b/app/main.cpp index 53f938a..b7d1735 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -129,6 +129,10 @@ static void updateDemoMode( /********************* Application Programming Interface *********************/ int main (void) { + char *basePath = NULL; + SDL_Surface *windowIcon = NULL; + std::string iconPath; + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) { fprintf(stderr, "SDL initialization failed: %s\n", SDL_GetError()); return 1; @@ -156,6 +160,20 @@ int main (void) { return 1; } + basePath = SDL_GetBasePath(); + if (basePath != NULL) { + iconPath = std::string(basePath) + "oscilloscope.bmp"; + SDL_free(basePath); + windowIcon = SDL_LoadBMP(iconPath.c_str()); + if (windowIcon != NULL) { + SDL_SetWindowIcon(window, windowIcon); + SDL_FreeSurface(windowIcon); + } + else { + fprintf(stderr, "Window icon loading failed: %s\n", SDL_GetError()); + } + } + SDL_GLContext glContext = SDL_GL_CreateContext(window); if (glContext == NULL) { fprintf(stderr, "OpenGL context creation failed: %s\n", SDL_GetError()); diff --git a/oscilloscope.bmp b/oscilloscope.bmp new file mode 100644 index 0000000000000000000000000000000000000000..9a187bc33f260140c091a791ec5963b041b658f8 GIT binary patch literal 3126 zcmeHHOBRAK42)~txpC=|6L0|c-r1Ysw4u`&c(r_WK^}!kCz+&y$K%;vBwCAmgLUuM zNp3(e@Yl00-Pq;++RU_l$Zv5D$`Y(wg|Y-Y>31UTvV|Q5*BV(7woT9n4pW>$ol9AQ zLqEK@WE=~@F`v?W`01J%hKbpI#c9H2mSZQddCM_n36^VG1D zc?5#oRzjo*5|f{i^8JHm3PH|Eh!jC$@-tE+`iKBkNK}cMnE&}dGt?3Q literal 0 HcmV?d00001 From 017791e2dde977a7aa38cd3c1a0f2bee8562f269 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 14:06:31 +0300 Subject: [PATCH 6/7] Reorganized USB module layout --- CMakeLists.txt | 9 +++++---- HISTORY.md | 22 +++++++++++++--------- README.md | 2 ++ app/main.cpp | 2 +- capture/.gitkeep | 0 capture/acquisition_loop.h | 2 +- usb/{ => inc}/firmware_loader.h | 0 usb/{ => inc}/usb_device.h | 2 +- usb/{ => src}/firmware_loader.cpp | 0 usb/{ => src}/usb_device.cpp | 0 10 files changed, 23 insertions(+), 16 deletions(-) delete mode 100644 capture/.gitkeep rename usb/{ => inc}/firmware_loader.h (100%) rename usb/{ => inc}/usb_device.h (99%) rename usb/{ => src}/firmware_loader.cpp (100%) rename usb/{ => src}/usb_device.cpp (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 86c8640..8b60b2e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,8 +33,8 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O2") set(SOURCES_LIST app/main.cpp - usb/usb_device.cpp - usb/firmware_loader.cpp + usb/src/usb_device.cpp + usb/src/firmware_loader.cpp capture/acquisition_loop.cpp ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp @@ -45,8 +45,8 @@ set(SOURCES_LIST ) set(HEADERS_LIST - usb/usb_device.h - usb/firmware_loader.h + usb/inc/usb_device.h + usb/inc/firmware_loader.h capture/acquisition_loop.h ) @@ -75,6 +75,7 @@ add_custom_command( target_include_directories(${APP_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/usb/inc ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends ) diff --git a/HISTORY.md b/HISTORY.md index c7216d3..44547d6 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -115,15 +115,6 @@ Records key decisions, structural changes, and completed development stages. user's copy of the official Windows driver before the USB layer can work end-to-end. -### Next USB tasks - -- Add a firmware upload step (fxload-equivalent control/bulk transfer) before - claiming the interface, sourcing `.hex` files from an external, user- - provided location. -- Read endpoint data chunks in a dedicated acquisition path. -- Add buffering between USB reads and waveform processing. -- Handle timeouts, I/O errors, disconnects, and recovery states. - ## 2026-09-04 ### Stage 3 - Endpoint read loop complete @@ -171,3 +162,16 @@ Records key decisions, structural changes, and completed development stages. records, and generated checksums. - Verified extraction from the official `Dso2250x861.sys` driver end to end; both generated HEX files match the hardware-tested files byte for byte. + +### USB module structure + +- Moved USB headers to `usb/inc/` and implementations to `usb/src/` after the + module grew beyond a single source/header pair. +- Updated CMake source lists, include directories, and dependent includes for + the new layout. + +### 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. diff --git a/README.md b/README.md index e293a93..49332c8 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ Oscilloscope/ ├── tools/ Optional firmware extraction utilities. ├── ui/ User interface. ├── usb/ USB device communication. +│ ├── inc/ USB module headers. +│ └── src/ USB module implementations. ├── WorkingDocs/ Technical specification and device documentation. ├── OldQtCode/ Historical Qt4/KDE4 reference implementation. ├── CMakeLists.txt CMake build configuration. diff --git a/app/main.cpp b/app/main.cpp index b7d1735..cfec08d 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -73,7 +73,7 @@ #include #include "capture/acquisition_loop.h" -#include "usb/usb_device.h" +#include "usb_device.h" using oscilloscope::capture::SAcquisitionLoop; using oscilloscope::usb::EScanStatus; diff --git a/capture/.gitkeep b/capture/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/capture/acquisition_loop.h b/capture/acquisition_loop.h index 869420a..9d77a6a 100644 --- a/capture/acquisition_loop.h +++ b/capture/acquisition_loop.h @@ -27,7 +27,7 @@ #include #include -#include "usb/usb_device.h" +#include "usb_device.h" /********************************* Definitions ********************************/ diff --git a/usb/firmware_loader.h b/usb/inc/firmware_loader.h similarity index 100% rename from usb/firmware_loader.h rename to usb/inc/firmware_loader.h diff --git a/usb/usb_device.h b/usb/inc/usb_device.h similarity index 99% rename from usb/usb_device.h rename to usb/inc/usb_device.h index d076974..b101489 100644 --- a/usb/usb_device.h +++ b/usb/inc/usb_device.h @@ -114,7 +114,7 @@ SUsbScanResult enumerateSupportedDevices(); * @param[out] connection Connection state container that is filled in * @returns Connection status and error message when applicable * @note Uploads the FX2 firmware first when the device is still in its bare - * bootloader state; see `usb/firmware_loader.h`. + * bootloader state; see `usb/inc/firmware_loader.h`. */ SUsbConnectionResult connectToDevice( const SUsbDeviceInfo &deviceInfo, diff --git a/usb/firmware_loader.cpp b/usb/src/firmware_loader.cpp similarity index 100% rename from usb/firmware_loader.cpp rename to usb/src/firmware_loader.cpp diff --git a/usb/usb_device.cpp b/usb/src/usb_device.cpp similarity index 100% rename from usb/usb_device.cpp rename to usb/src/usb_device.cpp From 39cdafbbc27e551b8efc891653763567bfe00119 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 14:27:15 +0300 Subject: [PATCH 7/7] Updated Doxyfile --- docs/Doxyfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Doxyfile b/docs/Doxyfile index 99b5367..d0b3097 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -910,7 +910,8 @@ WARN_LOGFILE = INPUT = ./mainpage.md \ ../app \ - ../usb + ../usb/inc \ + ../usb/src # 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