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/.gitignore b/.gitignore index 83009db..b766ea8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ OldQtCode/ WorkingDocs **/imgui.ini +# Extracted FX2 firmware (redistribution rights are not established) +firmware/* +!firmware/.gitkeep + diff --git a/CMakeLists.txt b/CMakeLists.txt index 5556e8f..8b60b2e 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) @@ -32,7 +33,9 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O2") set(SOURCES_LIST app/main.cpp - usb/usb_device.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 ${imgui_SOURCE_DIR}/imgui_tables.cpp @@ -42,7 +45,9 @@ set(SOURCES_LIST ) set(HEADERS_LIST - usb/usb_device.h + usb/inc/usb_device.h + usb/inc/firmware_loader.h + capture/acquisition_loop.h ) if(CMAKE_BUILD_TYPE MATCHES "Debug") @@ -59,8 +64,18 @@ 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} + ${CMAKE_CURRENT_SOURCE_DIR}/usb/inc ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends ) @@ -69,5 +84,6 @@ target_link_libraries(${APP_NAME} PRIVATE SDL2::SDL2 OpenGL::GL PkgConfig::LIBUSB + Threads::Threads ) diff --git a/HISTORY.md b/HISTORY.md index 624903c..44547d6 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -88,8 +88,90 @@ 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 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. + +## 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 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 + 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. + +### 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. + +### 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 -- Read endpoint data chunks in a dedicated acquisition path. +- Decode capture-state responses and acquired sample packets. - Add buffering between USB reads and waveform processing. -- Handle timeouts, I/O errors, disconnects, and recovery states. +- Add automatic recovery for transfer errors and unexpected disconnections. diff --git a/README.md b/README.md index e1a05b5..49332c8 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 @@ -44,8 +49,11 @@ 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. +│ ├── 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. @@ -63,12 +71,15 @@ The current application requires: - SDL2 development files; - 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 +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. @@ -76,8 +87,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 +97,37 @@ sudo udevadm control --reload-rules sudo udevadm trigger ``` +### Hantek DSO-2250 Firmware + +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 +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. + +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 @@ -171,10 +214,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..cfec08d 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -72,8 +72,10 @@ #include #include -#include "usb/usb_device.h" +#include "capture/acquisition_loop.h" +#include "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 ); @@ -126,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; @@ -153,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()); @@ -172,13 +193,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 +245,11 @@ int main (void) { &demoMode, &usbScanResult, &usbConnection, + &acquisitionLoop, &connectedDevice, &deviceStatus ); + acquisitionRunning = false; } ImGui::EndMenu(); } @@ -264,12 +290,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 +322,8 @@ int main (void) { usbConnection.isConnected && !isUsbDevicePresent(usbScanResult, connectedDevice) ) { + oscilloscope::capture::stopAcquisitionLoop(&acquisitionLoop); + acquisitionRunning = false; const SUsbConnectionResult disconnectResult = oscilloscope::usb::disconnectFromDevice(&usbConnection); @@ -301,9 +348,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 +426,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 +462,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 +535,7 @@ static void updateDemoMode( bool *demoMode, SUsbScanResult *usbScanResult, SUsbConnection *connection, + SAcquisitionLoop *acquisitionLoop, SUsbDeviceInfo *connectedDevice, std::string *deviceStatus ) { @@ -489,6 +545,7 @@ static void updateDemoMode( if (*demoMode) { if (connection->isConnected) { + oscilloscope::capture::stopAcquisitionLoop(acquisitionLoop); const SUsbConnectionResult disconnectResult = oscilloscope::usb::disconnectFromDevice(connection); @@ -511,8 +568,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; 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..9d77a6a --- /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_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/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 diff --git a/docs/mainpage.md b/docs/mainpage.md index e8d9b17..3e8d418 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,27 @@ 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 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 +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 +73,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/capture/.gitkeep b/firmware/.gitkeep similarity index 100% rename from capture/.gitkeep rename to firmware/.gitkeep diff --git a/oscilloscope.bmp b/oscilloscope.bmp new file mode 100644 index 0000000..9a187bc Binary files /dev/null and b/oscilloscope.bmp differ 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 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/inc/firmware_loader.h b/usb/inc/firmware_loader.h new file mode 100644 index 0000000..14189f9 --- /dev/null +++ b/usb/inc/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.h b/usb/inc/usb_device.h similarity index 53% rename from usb/usb_device.h rename to usb/inc/usb_device.h index 1a43d05..b101489 100644 --- a/usb/usb_device.h +++ b/usb/inc/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 EUsbTransferStatus { + eSuccess, /**< The full requested length was transferred */ + eTimeout, /**< All retry attempts timed out */ + eNoDevice, /**< The device was disconnected mid-transfer */ + eError /**< Some other libusb error occurred */ +}; + +/** @brief Holds the result of a bulk endpoint transfer */ +struct SUsbTransferResult { + EUsbTransferStatus 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/inc/firmware_loader.h`. */ SUsbConnectionResult connectToDevice( const SUsbDeviceInfo &deviceInfo, @@ -109,8 +128,91 @@ 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 + */ +SUsbTransferResult 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 + */ +SUsbTransferResult bulkRead( + const SUsbConnection &connection, + uint8_t endpointAddress, + uint8_t *buffer, + int length, + unsigned int timeoutMs = 500U, + 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 - -#endif // USB_DEVICE_H_ /******************************************************************************/ +#endif // USB_DEVICE_H_ diff --git a/usb/src/firmware_loader.cpp b/usb/src/firmware_loader.cpp new file mode 100644 index 0000000..d4589c2 --- /dev/null +++ b/usb/src/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/src/usb_device.cpp b/usb/src/usb_device.cpp new file mode 100644 index 0000000..c57c6a8 --- /dev/null +++ b/usb/src/usb_device.cpp @@ -0,0 +1,756 @@ +/** + * @file usb_device.cpp + * @version 0.2.2 + * @authors Anton Chernov + * @date 2026-09-01 + * @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" +#include "usb_device.h" + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace usb { + +/** @brief Defines a supported model signature */ +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 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[] = { + /* 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 *****************************/ + +/** + * @brief Finds the supported-model entry for a USB VID/PID pair + * @param[in] vendorId USB vendor identifier + * @param[in] productId USB product identifier + * @returns Matching model entry, or NULL when the device is unsupported + */ +static const SSupportedDevice* findSupportedDevice( + uint16_t vendorId, + uint16_t productId +); + +/** + * @brief Finds a specific bus/address combination in the libusb device list + * @param[in] deviceList libusb device list to search + * @param[in] deviceInfo Requested device descriptor + * @returns Matching libusb device, or NULL when not found + */ +static libusb_device* findDeviceByInfo( + libusb_device **deviceList, + const SUsbDeviceInfo &deviceInfo, + 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 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 **********************/ + +/** @fn enumerateSupportedDevices */ +SUsbScanResult enumerateSupportedDevices() { + libusb_context *context = NULL; + libusb_device **deviceList = NULL; + SUsbScanResult result = {EScanStatus::eSuccess, {}, ""}; + const int initializationResult = libusb_init(&context); + + if (initializationResult != LIBUSB_SUCCESS) { + result.status = EScanStatus::eInitializationFailed; + result.errorMessage = libusb_error_name(initializationResult); + } + else { + const ssize_t deviceCount = libusb_get_device_list(context, &deviceList); + + if (deviceCount < 0) { + result.status = EScanStatus::eEnumerationFailed; + result.errorMessage = libusb_error_name(static_cast(deviceCount)); + } + else { + for (ssize_t index = 0; index < deviceCount; ++index) { + libusb_device_descriptor descriptor; + const int descriptorResult = libusb_get_device_descriptor( + deviceList[index], + &descriptor + ); + + if (descriptorResult == LIBUSB_SUCCESS) { + const SSupportedDevice *supportedDevice = findSupportedDevice( + descriptor.idVendor, + descriptor.idProduct + ); + + if (supportedDevice != NULL) { + result.devices.push_back({ + descriptor.idVendor, + descriptor.idProduct, + libusb_get_bus_number(deviceList[index]), + libusb_get_device_address(deviceList[index]), + supportedDevice->modelName + }); + } + } + } + } + + if (deviceList != NULL) { + libusb_free_device_list(deviceList, 1); + } + libusb_exit(context); + } + + return result; +} + +/** @fn connectToDevice */ +SUsbConnectionResult connectToDevice( + const SUsbDeviceInfo &deviceInfo, + SUsbConnection *connection +) { + SUsbConnectionResult result = {EConnectionStatus::eDisconnected, ""}; + libusb_context *context = NULL; + libusb_device **deviceList = NULL; + 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; + int claimResult = LIBUSB_SUCCESS; + unsigned int reenumerationAttempt = 0U; + + if (connection == NULL) { + result.status = EConnectionStatus::eDeviceNotFound; + result.errorMessage = "Connection handle is null"; + } + else { + if (connection->isConnected) { + result = disconnectFromDevice(connection); + } + + if (result.status == EConnectionStatus::eDisconnected) { + initializationResult = libusb_init(&context); + + if (initializationResult != LIBUSB_SUCCESS) { + result.status = EConnectionStatus::eInitializationFailed; + result.errorMessage = libusb_error_name(initializationResult); + } + else { + deviceCount = libusb_get_device_list(context, &deviceList); + device = findDeviceByInfo(deviceList, deviceInfo, deviceCount); + supportedDevice = findSupportedDevice( + deviceInfo.vendorId, + deviceInfo.productId + ); + + if ((deviceCount < 0) || (device == NULL) || + (supportedDevice == NULL)) { + result.status = EConnectionStatus::eDeviceNotFound; + result.errorMessage = "Requested USB device was not found"; + } + else { + 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 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. */ + libusb_free_device_list(deviceList, 1); + deviceList = NULL; + 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; + 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) { + result.status = EConnectionStatus::eOpenFailed; + result.errorMessage = libusb_error_name(openResult); + } + else { + claimResult = libusb_claim_interface( + handle, + supportedDevice->interfaceNumber + ); + + if (claimResult != LIBUSB_SUCCESS) { + result.status = EConnectionStatus::eClaimInterfaceFailed; + result.errorMessage = libusb_error_name(claimResult); + } + else { + 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; + } + } + } + } + } + } + } + + if (deviceList != NULL) { + libusb_free_device_list(deviceList, 1); + } + if (handle != NULL) { + libusb_close(handle); + } + if (context != NULL) { + libusb_exit(context); + } + + return result; +} + +/** @fn disconnectFromDevice */ +SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection) { + SUsbConnectionResult result = {EConnectionStatus::eDisconnected, ""}; + int releaseResult = LIBUSB_SUCCESS; + + if (connection == NULL) { + result.status = EConnectionStatus::eDeviceNotFound; + result.errorMessage = "Connection handle is null"; + } + else { + if ((connection->handle != NULL) && connection->isConnected) { + releaseResult = libusb_release_interface( + connection->handle, + connection->interfaceNumber + ); + + if ( + (releaseResult != LIBUSB_SUCCESS) && + (releaseResult != LIBUSB_ERROR_NO_DEVICE) + ) { + result.status = EConnectionStatus::eReleaseFailed; + result.errorMessage = libusb_error_name(releaseResult); + } + } + + if (connection->handle != NULL) { + libusb_close(connection->handle); + } + if (connection->context != NULL) { + libusb_exit(connection->context); + } + + connection->context = NULL; + connection->handle = NULL; + connection->interfaceNumber = 0U; + connection->isConnected = false; + } + + return result; +} + +/** @fn bulkWrite */ +SUsbTransferResult bulkWrite( + const SUsbConnection &connection, + const uint8_t endpointAddress, + const uint8_t *data, + const int length, + const unsigned int timeoutMs, + const unsigned int attempts +) { + SUsbTransferResult result = {EUsbTransferStatus::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 != EUsbTransferStatus::eSuccess) { + result.errorMessage = libusb_error_name(transferResult); + } + + return result; +} + +/** @fn bulkRead */ +SUsbTransferResult bulkRead( + const SUsbConnection &connection, + const uint8_t endpointAddress, + uint8_t *buffer, + const int length, + const unsigned int timeoutMs, + const unsigned int attempts +) { + SUsbTransferResult result = {EUsbTransferStatus::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 != 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); + } + + return result; +} + +/****************************** Private functions *****************************/ + +/** @fn findSupportedDevice */ +static const SSupportedDevice* findSupportedDevice( + const uint16_t vendorId, + const uint16_t productId +) { + const SSupportedDevice *result = NULL; + const size_t deviceCount = + sizeof(supported_devices) / sizeof(supported_devices[0]); + + for (size_t index = 0U; index < deviceCount; ++index) { + if ( + (supported_devices[index].vendorId == vendorId) && + (supported_devices[index].productId == productId) + ) { + result = &supported_devices[index]; + break; + } + } + + return result; +} + +/** @fn findDeviceByInfo */ +static libusb_device* findDeviceByInfo( + libusb_device **deviceList, + const SUsbDeviceInfo &deviceInfo, + 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 == deviceInfo.vendorId) && + (descriptor.idProduct == deviceInfo.productId) && + (libusb_get_bus_number(candidate) == deviceInfo.busNumber) && + (libusb_get_device_address(candidate) == deviceInfo.deviceAddress) + ) { + result = candidate; + break; + } + } + } + + 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 EUsbTransferStatus classifyBulkTransferResult(const int transferResult) { + EUsbTransferStatus result = EUsbTransferStatus::eError; + + if (transferResult == LIBUSB_SUCCESS) { + 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 = EUsbTransferStatus::eTimeout; + } + else if (transferResult == LIBUSB_ERROR_NO_DEVICE) { + result = EUsbTransferStatus::eNoDevice; + } + + return result; +} + +} // namespace usb +} // namespace oscilloscope +/******************************************************************************/ diff --git a/usb/usb_device.cpp b/usb/usb_device.cpp deleted file mode 100644 index f205f79..0000000 --- a/usb/usb_device.cpp +++ /dev/null @@ -1,321 +0,0 @@ -/** - * @file usb_device.cpp - * @version 0.2.2 - * @authors Anton Chernov - * @date 2026-09-01 - * @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 "usb_device.h" - -/********************************* Definitions ********************************/ - -namespace oscilloscope { -namespace usb { - -/** @brief Defines a supported model signature */ -struct SSupportedDevice { - uint16_t vendorId; - uint16_t productId; - uint8_t interfaceNumber; - const char *modelName; -}; - -/****************************** Module variables ******************************/ - -/** @brief Maps supported USB VID/PID pairs to model display names */ -static const SSupportedDevice supported_devices[] = { - {0x04B4U, 0x2250U, 0U, "Hantek DSO-2250"} -}; - -/***************************** Private prototypes *****************************/ - -/** - * @brief Finds the supported-model entry for a USB VID/PID pair - * @param[in] vendorId USB vendor identifier - * @param[in] productId USB product identifier - * @returns Matching model entry, or NULL when the device is unsupported - */ -static const SSupportedDevice* findSupportedDevice( - uint16_t vendorId, - uint16_t productId -); - -/** - * @brief Finds a specific bus/address combination in the libusb device list - * @param[in] deviceList libusb device list to search - * @param[in] deviceInfo Requested device descriptor - * @returns Matching libusb device, or NULL when not found - */ -static libusb_device* findDeviceByInfo( - libusb_device **deviceList, - const SUsbDeviceInfo &deviceInfo, - const ssize_t deviceCount -); - -/********************* Application Programming Interface **********************/ - -/** @fn enumerateSupportedDevices */ -SUsbScanResult enumerateSupportedDevices() { - libusb_context *context = NULL; - libusb_device **deviceList = NULL; - SUsbScanResult result = {EScanStatus::eSuccess, {}, ""}; - const int initializationResult = libusb_init(&context); - - if (initializationResult != LIBUSB_SUCCESS) { - result.status = EScanStatus::eInitializationFailed; - result.errorMessage = libusb_error_name(initializationResult); - } - else { - const ssize_t deviceCount = libusb_get_device_list(context, &deviceList); - - if (deviceCount < 0) { - result.status = EScanStatus::eEnumerationFailed; - result.errorMessage = libusb_error_name(static_cast(deviceCount)); - } - else { - for (ssize_t index = 0; index < deviceCount; ++index) { - libusb_device_descriptor descriptor; - const int descriptorResult = libusb_get_device_descriptor( - deviceList[index], - &descriptor - ); - - if (descriptorResult == LIBUSB_SUCCESS) { - const SSupportedDevice *supportedDevice = findSupportedDevice( - descriptor.idVendor, - descriptor.idProduct - ); - - if (supportedDevice != NULL) { - result.devices.push_back({ - descriptor.idVendor, - descriptor.idProduct, - libusb_get_bus_number(deviceList[index]), - libusb_get_device_address(deviceList[index]), - supportedDevice->modelName - }); - } - } - } - } - - if (deviceList != NULL) { - libusb_free_device_list(deviceList, 1); - } - libusb_exit(context); - } - - return result; -} - -/** @fn connectToDevice */ -SUsbConnectionResult connectToDevice( - const SUsbDeviceInfo &deviceInfo, - SUsbConnection *connection -) { - SUsbConnectionResult result = {EConnectionStatus::eDisconnected, ""}; - libusb_context *context = NULL; - libusb_device **deviceList = NULL; - libusb_device *device = NULL; - libusb_device_handle *handle = NULL; - const SSupportedDevice *supportedDevice = NULL; - ssize_t deviceCount = 0; - int initializationResult = LIBUSB_SUCCESS; - int openResult = LIBUSB_SUCCESS; - int claimResult = LIBUSB_SUCCESS; - - if (connection == NULL) { - result.status = EConnectionStatus::eDeviceNotFound; - result.errorMessage = "Connection handle is null"; - } - else { - if (connection->isConnected) { - result = disconnectFromDevice(connection); - } - - if (result.status == EConnectionStatus::eDisconnected) { - initializationResult = libusb_init(&context); - - if (initializationResult != LIBUSB_SUCCESS) { - result.status = EConnectionStatus::eInitializationFailed; - result.errorMessage = libusb_error_name(initializationResult); - } - else { - deviceCount = libusb_get_device_list(context, &deviceList); - device = findDeviceByInfo(deviceList, deviceInfo, deviceCount); - supportedDevice = findSupportedDevice( - deviceInfo.vendorId, - deviceInfo.productId - ); - - if ((deviceCount < 0) || (device == NULL) || - (supportedDevice == NULL)) { - result.status = EConnectionStatus::eDeviceNotFound; - result.errorMessage = "Requested USB device was not found"; - } - else { - openResult = libusb_open(device, &handle); - - if (openResult != LIBUSB_SUCCESS) { - result.status = EConnectionStatus::eOpenFailed; - result.errorMessage = libusb_error_name(openResult); - } - else { - claimResult = libusb_claim_interface( - handle, - supportedDevice->interfaceNumber - ); - - if (claimResult != LIBUSB_SUCCESS) { - result.status = EConnectionStatus::eClaimInterfaceFailed; - 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; - } - } - } - } - } - } - - if (deviceList != NULL) { - libusb_free_device_list(deviceList, 1); - } - if (handle != NULL) { - libusb_close(handle); - } - if (context != NULL) { - libusb_exit(context); - } - - return result; -} - -/** @fn disconnectFromDevice */ -SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection) { - SUsbConnectionResult result = {EConnectionStatus::eDisconnected, ""}; - int releaseResult = LIBUSB_SUCCESS; - - if (connection == NULL) { - result.status = EConnectionStatus::eDeviceNotFound; - result.errorMessage = "Connection handle is null"; - } - else { - if ((connection->handle != NULL) && connection->isConnected) { - releaseResult = libusb_release_interface( - connection->handle, - connection->interfaceNumber - ); - - if ( - (releaseResult != LIBUSB_SUCCESS) && - (releaseResult != LIBUSB_ERROR_NO_DEVICE) - ) { - result.status = EConnectionStatus::eReleaseFailed; - result.errorMessage = libusb_error_name(releaseResult); - } - } - - if (connection->handle != NULL) { - libusb_close(connection->handle); - } - if (connection->context != NULL) { - libusb_exit(connection->context); - } - - connection->context = NULL; - connection->handle = NULL; - connection->interfaceNumber = 0U; - connection->isConnected = false; - } - - return result; -} - -/****************************** Private functions *****************************/ - -/** @fn findSupportedDevice */ -static const SSupportedDevice* findSupportedDevice( - const uint16_t vendorId, - const uint16_t productId -) { - const SSupportedDevice *result = NULL; - const size_t deviceCount = - sizeof(supported_devices) / sizeof(supported_devices[0]); - - for (size_t index = 0U; index < deviceCount; ++index) { - if ( - (supported_devices[index].vendorId == vendorId) && - (supported_devices[index].productId == productId) - ) { - result = &supported_devices[index]; - break; - } - } - - return result; -} - -/** @fn findDeviceByInfo */ -static libusb_device* findDeviceByInfo( - libusb_device **deviceList, - const SUsbDeviceInfo &deviceInfo, - 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 == deviceInfo.vendorId) && - (descriptor.idProduct == deviceInfo.productId) && - (libusb_get_bus_number(candidate) == deviceInfo.busNumber) && - (libusb_get_device_address(candidate) == deviceInfo.deviceAddress) - ) { - result = candidate; - break; - } - } - } - - return result; -} - -} // namespace usb -} // namespace oscilloscope -/******************************************************************************/