From e33217ed6ebeb16f642500e796d021495bcfca13 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 22:35:14 +0300 Subject: [PATCH 1/8] Added DRY and KISS rules --- .github/instructions/cpp-style.instructions.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/instructions/cpp-style.instructions.md b/.github/instructions/cpp-style.instructions.md index 3c6275f..b45d076 100644 --- a/.github/instructions/cpp-style.instructions.md +++ b/.github/instructions/cpp-style.instructions.md @@ -181,6 +181,14 @@ Always put the macro name in the closing comment: Prefer a `switch` statement over a long `if`/`else if` chain. +Follow DRY: do not duplicate implementation logic. Reuse an existing helper or +abstraction when suitable; otherwise extract the smallest clearly named shared +helper that removes meaningful repetition. + +Follow KISS: prefer the simplest design that fully satisfies the current +requirements. Do not add layers, generic abstractions, configuration, or +extension points without a concrete present need. + ## Doxygen API documentation Public **declarations** (in `.h`) carry a full Doxygen block; **definitions** From 35d3c0d48d434bfa44ff5b98fdaea51e97830055 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 22:35:44 +0300 Subject: [PATCH 2/8] Added raw USB packet buffering --- CMakeLists.txt | 33 +++- HISTORY.md | 17 +- README.md | 53 ++---- capture/{ => inc}/acquisition_loop.h | 10 +- capture/inc/raw_packet_queue.h | 99 +++++++++++ capture/{ => src}/acquisition_loop.cpp | 42 ++++- capture/src/raw_packet_queue.cpp | 134 +++++++++++++++ docs/mainpage.md | 30 ++-- tests/raw_packet_queue_test.cpp | 222 +++++++++++++++++++++++++ 9 files changed, 578 insertions(+), 62 deletions(-) rename capture/{ => inc}/acquisition_loop.h (88%) create mode 100644 capture/inc/raw_packet_queue.h rename capture/{ => src}/acquisition_loop.cpp (87%) create mode 100644 capture/src/raw_packet_queue.cpp create mode 100644 tests/raw_packet_queue_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a93188d..6e3a077 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,14 @@ pkg_check_modules(LIBUSB REQUIRED IMPORTED_TARGET libusb-1.0) set(APP_NAME run) +function(enable_project_warnings target_name) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(${target_name} PRIVATE + -Wall -Wextra -Wpedantic + ) + endif() +endfunction() + if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() @@ -35,7 +43,8 @@ set(SOURCES_LIST app/main.cpp usb/src/usb_device.cpp usb/src/firmware_loader.cpp - capture/acquisition_loop.cpp + capture/src/acquisition_loop.cpp + capture/src/raw_packet_queue.cpp ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_tables.cpp @@ -47,7 +56,8 @@ set(SOURCES_LIST set(HEADERS_LIST usb/inc/usb_device.h usb/inc/firmware_loader.h - capture/acquisition_loop.h + capture/inc/acquisition_loop.h + capture/inc/raw_packet_queue.h ) if(CMAKE_BUILD_TYPE MATCHES "Debug") @@ -76,7 +86,7 @@ add_custom_command( target_include_directories(${APP_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/usb/inc - ${CMAKE_CURRENT_SOURCE_DIR}/capture + ${CMAKE_CURRENT_SOURCE_DIR}/capture/inc ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends ) @@ -88,7 +98,20 @@ target_link_libraries(${APP_NAME} PRIVATE Threads::Threads ) -if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - target_compile_options(${APP_NAME} PRIVATE -Wall -Wextra -Wpedantic) +enable_project_warnings(${APP_NAME}) + +include(CTest) + +if(BUILD_TESTING) + add_executable(raw_packet_queue_tests + tests/raw_packet_queue_test.cpp + capture/src/raw_packet_queue.cpp + ) + target_include_directories(raw_packet_queue_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/capture/inc + ) + target_link_libraries(raw_packet_queue_tests PRIVATE Threads::Threads) + enable_project_warnings(raw_packet_queue_tests) + add_test(NAME raw_packet_queue COMMAND raw_packet_queue_tests) endif() diff --git a/HISTORY.md b/HISTORY.md index 7ca55b6..babdbee 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -210,9 +210,24 @@ Records key decisions, structural changes, and completed development stages. shows the enumerated bootloader's model name, which correctly reflects the unprogrammed device. +### Stage 3 - Raw USB buffer management + +- Added a fixed-capacity FIFO between USB reads and packet processing. +- Store each raw response with its actual transferred length and support the + largest known two-channel DSO-2250 capture without allocation in the active + acquisition loop. +- Keep the USB producer non-blocking by discarding the oldest queued response + when processing falls behind, with a thread-safe dropped-packet count. +- Added a processing consumer that preserves the existing capture-state update + while decoupling it from the USB polling thread. +- Close the queue and wake the consumer during Stop and terminal USB errors; + join both workers before releasing USB resources. +- Added deterministic CTest coverage for FIFO order, response length, overflow, + close/reset behavior, and concurrent producer-consumer operation. +- Moved the expanded capture module into `capture/inc/` and `capture/src/`. + ### Next USB tasks - Decode capture-state responses and acquired sample packets. -- Add buffering between USB reads and waveform processing. - Add deterministic fault-injection tests for timeout and transfer-error recovery, then verify recovery with a connected physical device. diff --git a/README.md b/README.md index 1795ce7..cc3eb62 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,24 @@ # Oscilloscope -New cross-platform USB oscilloscope client developed without Qt. The primary -target platforms are Linux Mint, Ubuntu, and Debian, with a future Windows -build supported through CMake. - -The initial target device is the Hantek DSO-2250 USB oscilloscope. The -`OldQtCode` directory contains the historical Qt4/KDE4 implementation and is -used only as a reference for protocol details and algorithms. The new codebase -is developed independently and has no Qt dependency. +Modern cross-platform client for Hantek USB oscilloscopes. Linux Mint and +Ubuntu are the primary development and testing platforms; compatibility with +Windows 10 and 11 is planned through the portable CMake-based architecture. ## Status Current version: `0.2.5`. -The current iteration provides a working USB connection and endpoint polling -path for the Hantek DSO-2250: +The application currently provides: -- 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; -- discovery and connection through libusb; +- an SDL2, Dear ImGui, and OpenGL user interface; +- Demo and Live operating modes; +- Hantek DSO-2250 discovery and connection through libusb; - FX2 firmware upload and operational-device re-enumeration; -- a Start/Stop-controlled acquisition thread that polls the bulk endpoints; -- bounded recovery from USB timeouts and I/O errors; -- safe acquisition shutdown and status reporting when the device is lost. - -The application starts in Live mode when a supported device is present and in -Demo mode otherwise. Connecting prepares the device; endpoint traffic begins -only after pressing Start and stops after pressing Stop. Transient USB errors -are retried with a bounded delay. Repeated errors stop acquisition while -keeping an available device connected for another Start attempt. Disconnecting -the device during acquisition stops the worker before USB resources are -released and reports the device loss in the status line. Outside acquisition, -the application periodically checks device presence: unplugging a connected -device closes the stale connection, and plugging it back in updates the status -automatically. The disconnect status remains visible while the device is -absent. Reconnecting remains an explicit action. +- Start/Stop-controlled endpoint polling; +- bounded USB error recovery and safe device-loss handling. + +Waveform reads, sample decoding, and live waveform rendering are not yet +implemented. ## Planned Stack @@ -55,8 +35,11 @@ absent. Reconnecting remains an explicit action. Oscilloscope/ ├── app/ Application entry point. ├── capture/ Sample acquisition and processing. +│ ├── inc/ Capture module headers. +│ └── src/ Capture module implementations. ├── core/ Shared types and application logic. ├── docs/ Project documentation. +├── firmware/ Default path for local device firmware files. ├── render/ Oscilloscope waveform rendering. ├── tools/ Optional firmware extraction utilities. ├── ui/ User interface. @@ -64,7 +47,6 @@ Oscilloscope/ │ ├── 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. ├── Makefile Make build entry points. └── linux_build.sh Linux build script. @@ -224,9 +206,8 @@ CI can update only the version metadata by passing `--skip-build`. ## Next Steps 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. +2. Render live and demo waveforms on the display grid. +3. 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/capture/acquisition_loop.h b/capture/inc/acquisition_loop.h similarity index 88% rename from capture/acquisition_loop.h rename to capture/inc/acquisition_loop.h index 52b196e..26c80b2 100644 --- a/capture/acquisition_loop.h +++ b/capture/inc/acquisition_loop.h @@ -27,6 +27,7 @@ #include #include +#include "raw_packet_queue.h" #include "usb_device.h" /********************************* Definitions ********************************/ @@ -56,6 +57,7 @@ enum class EAcquisitionOperation { /** @brief Acquisition state safe to read from any thread */ struct SAcquisitionStatus { std::atomic lastCaptureState{-1}; /**< Last raw capture state */ + std::atomic droppedPacketCount{0U}; /**< Queue overflow count */ std::atomic state{ EAcquisitionState::eStopped }; /**< Current worker state */ @@ -69,9 +71,11 @@ struct SAcquisitionStatus { /** @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 */ + std::thread workerThread; /**< Background USB producer */ + std::thread processingThread; /**< Background packet consumer */ + RawPacketQueue rawPacketQueue{8U}; /**< Bounded raw response FIFO */ + std::atomic stopRequested{false}; /**< Set to request a stop */ + SAcquisitionStatus status; /**< Shared poll status */ }; /********************* Application Programming Interface *********************/ diff --git a/capture/inc/raw_packet_queue.h b/capture/inc/raw_packet_queue.h new file mode 100644 index 0000000..c0c2338 --- /dev/null +++ b/capture/inc/raw_packet_queue.h @@ -0,0 +1,99 @@ +/** + * @file raw_packet_queue.h + * @version 0.2.5 + * @authors Anton Chernov + * @date 2026-09-04 + * @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 RAW_PACKET_QUEUE_H_ +#define RAW_PACKET_QUEUE_H_ + +/******************************* Included files ******************************/ +#include +#include +#include +#include +#include +#include + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace capture { + +/** @brief Maximum raw two-channel USB capture size accepted by the queue */ +static const size_t RAW_USB_PACKET_MAX_SIZE = 65536U; + +/** @brief Owns one raw USB response and its valid byte count */ +struct SRawUsbPacket { + std::array payload; /**< Raw bytes */ + size_t validLength; /**< Number of valid bytes in payload */ +}; + +/** @brief Provides a bounded thread-safe FIFO for raw USB responses */ +class RawPacketQueue { +public: + /** + * @brief Creates an empty queue with fixed packet capacity + * @param[in] capacity Maximum number of retained packets + */ + explicit RawPacketQueue(size_t capacity); + + /** + * @brief Copies a raw response into the queue without blocking + * @param[in] data Raw response bytes + * @param[in] length Number of bytes to copy + * @returns True when the packet was accepted + * @note When full, the oldest packet is discarded before insertion. + */ + bool push(const uint8_t *data, size_t length); + + /** + * @brief Waits for and removes the oldest retained packet + * @param[out] packet Destination receiving the removed packet + * @returns True when a packet was returned, false when closed and empty + */ + bool waitPop(SRawUsbPacket *packet); + + /** @brief Closes the queue and wakes every waiting consumer */ + void close(); + + /** @brief Reopens and clears the queue for a new acquisition run */ + void reset(); + + /** + * @brief Reads the number of packets discarded since the last reset + * @returns Number of packets discarded by the overflow policy + */ + size_t getDroppedPacketCount() const; + +private: + std::vector packets; + mutable std::mutex mutex; + std::condition_variable packetAvailable; + size_t readIndex; + size_t writeIndex; + size_t packetCount; + size_t droppedPacketCount; + bool closed; +}; + +} // namespace capture +} // namespace oscilloscope +/******************************************************************************/ +#endif //! RAW_PACKET_QUEUE_H_ \ No newline at end of file diff --git a/capture/acquisition_loop.cpp b/capture/src/acquisition_loop.cpp similarity index 87% rename from capture/acquisition_loop.cpp rename to capture/src/acquisition_loop.cpp index 57fe7ef..00f6c41 100644 --- a/capture/acquisition_loop.cpp +++ b/capture/src/acquisition_loop.cpp @@ -66,6 +66,12 @@ static void pollCaptureState( usb::SUsbConnection connection ); +/** + * @brief Consumes queued USB responses without blocking the USB producer + * @param[in,out] loop Loop control block containing the queue and status + */ +static void processRawPackets(SAcquisitionLoop *loop); + /** * @brief Executes one complete capture-state polling transaction * @param[in] connection USB connection to poll @@ -99,12 +105,15 @@ void startAcquisitionLoop( ) { if (loop != NULL) { loop->stopRequested.store(false); + loop->rawPacketQueue.reset(); loop->status.lastCaptureState.store(-1); + loop->status.droppedPacketCount.store(0U); loop->status.state.store(EAcquisitionState::eRunning); loop->status.failedOperation.store(EAcquisitionOperation::eNone); loop->status.lastTransferStatus.store( usb::EUsbTransferStatus::eSuccess ); + loop->processingThread = std::thread(processRawPackets, loop); loop->workerThread = std::thread(pollCaptureState, loop, connection); } } @@ -123,6 +132,10 @@ bool joinFinishedAcquisitionLoop(SAcquisitionLoop *loop) { if (loop->workerThread.joinable()) { loop->workerThread.join(); } + loop->rawPacketQueue.close(); + if (loop->processingThread.joinable()) { + loop->processingThread.join(); + } joined = true; } } @@ -134,9 +147,13 @@ bool joinFinishedAcquisitionLoop(SAcquisitionLoop *loop) { void stopAcquisitionLoop(SAcquisitionLoop *loop) { if (loop != NULL) { loop->stopRequested.store(true); + loop->rawPacketQueue.close(); if (loop->workerThread.joinable()) { loop->workerThread.join(); } + if (loop->processingThread.joinable()) { + loop->processingThread.join(); + } loop->status.state.store(EAcquisitionState::eStopped); } } @@ -162,7 +179,13 @@ static void pollCaptureState( ); if (transferResult.status == usb::EUsbTransferStatus::eSuccess) { - loop->status.lastCaptureState.store(static_cast(response[0])); + loop->rawPacketQueue.push( + response, + static_cast(transferResult.transferredBytes) + ); + loop->status.droppedPacketCount.store( + loop->rawPacketQueue.getDroppedPacketCount() + ); loop->status.state.store(EAcquisitionState::eRunning); loop->status.failedOperation.store(EAcquisitionOperation::eNone); loop->status.lastTransferStatus.store( @@ -192,6 +215,23 @@ static void pollCaptureState( std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); } + + loop->rawPacketQueue.close(); +} + +/*----------------------------------------------------------------------------*/ + +/** @fn processRawPackets */ +static void processRawPackets(SAcquisitionLoop *loop) { + SRawUsbPacket packet; + + while (loop->rawPacketQueue.waitPop(&packet)) { + if (packet.validLength > 0U) { + loop->status.lastCaptureState.store( + static_cast(packet.payload[0]) + ); + } + } } /*----------------------------------------------------------------------------*/ diff --git a/capture/src/raw_packet_queue.cpp b/capture/src/raw_packet_queue.cpp new file mode 100644 index 0000000..25b5a12 --- /dev/null +++ b/capture/src/raw_packet_queue.cpp @@ -0,0 +1,134 @@ +/** + * @file raw_packet_queue.cpp + * @version 0.2.5 + * @authors Anton Chernov + * @date 2026-09-04 + * @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 "raw_packet_queue.h" + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace capture { + +/********************* Application Programming Interface *********************/ + +/** @fn RawPacketQueue */ +RawPacketQueue::RawPacketQueue(const size_t capacity) : + packets(capacity), + readIndex(0U), + writeIndex(0U), + packetCount(0U), + droppedPacketCount(0U), + closed(false) { +} + +/*----------------------------------------------------------------------------*/ + +/** @fn push */ +bool RawPacketQueue::push(const uint8_t *data, const size_t length) { + bool accepted = false; + std::lock_guard lock(mutex); + + if ( + !closed && + !packets.empty() && + (data != NULL) && + (length > 0U) && + (length <= RAW_USB_PACKET_MAX_SIZE) + ) { + if (packetCount == packets.size()) { + readIndex = (readIndex + 1U) % packets.size(); + --packetCount; + ++droppedPacketCount; + } + + std::copy(data, data + length, packets[writeIndex].payload.begin()); + packets[writeIndex].validLength = length; + writeIndex = (writeIndex + 1U) % packets.size(); + ++packetCount; + accepted = true; + packetAvailable.notify_one(); + } + + return accepted; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn waitPop */ +bool RawPacketQueue::waitPop(SRawUsbPacket *packet) { + bool packetReturned = false; + std::unique_lock lock(mutex); + + packetAvailable.wait(lock, [this]() { + return (packetCount > 0U) || closed; + }); + + if ((packet != NULL) && (packetCount > 0U)) { + *packet = packets[readIndex]; + readIndex = (readIndex + 1U) % packets.size(); + --packetCount; + packetReturned = true; + } + + return packetReturned; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn close */ +void RawPacketQueue::close() { + std::lock_guard lock(mutex); + + closed = true; + packetAvailable.notify_all(); +} + +/*----------------------------------------------------------------------------*/ + +/** @fn reset */ +void RawPacketQueue::reset() { + std::lock_guard lock(mutex); + + readIndex = 0U; + writeIndex = 0U; + packetCount = 0U; + droppedPacketCount = 0U; + closed = false; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn getDroppedPacketCount */ +size_t RawPacketQueue::getDroppedPacketCount() const { + size_t result = 0U; + std::lock_guard lock(mutex); + + result = droppedPacketCount; + + return result; +} + +} // namespace capture +} // namespace oscilloscope +/******************************************************************************/ \ No newline at end of file diff --git a/docs/mainpage.md b/docs/mainpage.md index 268634a..679688a 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -2,8 +2,9 @@ **Version:** 0.2.5 | **Status:** active development -Cross-platform USB oscilloscope client for Linux Mint, Ubuntu, and Debian. -The project is written from scratch in C++14 and does not depend on Qt. +Modern cross-platform client for Hantek USB oscilloscopes. Linux Mint and +Ubuntu are the primary development and testing platforms; compatibility with +Windows 10 and 11 is planned through the portable CMake-based architecture. ## Overview @@ -11,10 +12,6 @@ The initial target device is the Hantek DSO-2250 USB oscilloscope. The application uses SDL2, Dear ImGui, and OpenGL for the user interface, and libusb for communication with supported instruments. -The `OldQtCode/` directory is retained solely as a historical reference for -USB protocol research and signal-processing algorithms. It is not a dependency -of the new implementation. - ## Current functionality - SDL2 window with an OpenGL rendering context; @@ -26,9 +23,10 @@ of the new implementation. 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; +- Start/Stop-controlled endpoint polling; +- bounded buffering between USB reads and packet processing; - capture-state polling through vendor control requests and bulk endpoints; -- successful-poll and transfer-error counters in the status line. +- bounded USB error recovery and safe device-loss handling. ## Architecture @@ -57,10 +55,11 @@ 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. +Start launches a USB producer 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. Successful responses enter a bounded +FIFO and a processing consumer reads them independently. Stop closes the queue, +wakes the consumer, and joins both threads before releasing the connection. See `oscilloscope::usb::enumerateSupportedDevices()` for the public discovery API. @@ -73,8 +72,7 @@ complete installation, build, and run instructions. ## Planned work -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. +The next tasks are decoding capture responses and sample packets, reading +waveform data, and displaying live waveforms. Instrument controls, broader +recovery behavior, and persistent configuration follow in later milestones. diff --git a/tests/raw_packet_queue_test.cpp b/tests/raw_packet_queue_test.cpp new file mode 100644 index 0000000..e3349cc --- /dev/null +++ b/tests/raw_packet_queue_test.cpp @@ -0,0 +1,222 @@ +/** + * @file raw_packet_queue_test.cpp + * @version 0.2.5 + * @authors Anton Chernov + * @date 2026-09-04 + * @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 "raw_packet_queue.h" + +/********************************* Definitions ********************************/ + +namespace { + +using oscilloscope::capture::RawPacketQueue; +using oscilloscope::capture::SRawUsbPacket; + +/***************************** Private prototypes *****************************/ + +static bool expect(const bool condition, const char *message); +static bool testFifoAndLength(); +static bool testOverflowDropsOldest(); +static bool testCloseWakesConsumer(); +static bool testResetReopensQueue(); +static bool testProducerConsumer(); + +/****************************** Private functions *****************************/ + +/** @fn expect */ +static bool expect(const bool condition, const char *message) { + bool result = condition; + + if (!condition) { + std::cerr << "FAILED: " << message << std::endl; + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testFifoAndLength */ +static bool testFifoAndLength() { + RawPacketQueue queue(3U); + const uint8_t first[] = {1U, 2U}; + const uint8_t second[] = {3U, 4U, 5U}; + SRawUsbPacket packet; + bool result = true; + + result = expect(queue.push(first, sizeof(first)), "push first packet") && + result; + result = expect(queue.push(second, sizeof(second)), "push second packet") + && result; + result = expect(queue.waitPop(&packet), "pop first packet") && result; + result = expect(packet.validLength == sizeof(first), "first length") && + result; + result = expect( + (packet.payload[0] == first[0]) && + (packet.payload[1] == first[1]), + "first payload" + ) && result; + result = expect(queue.waitPop(&packet), "pop second packet") && result; + result = expect(packet.validLength == sizeof(second), "second length") && + result; + result = expect( + (packet.payload[0] == second[0]) && + (packet.payload[1] == second[1]) && + (packet.payload[2] == second[2]), + "second payload" + ) && result; + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testOverflowDropsOldest */ +static bool testOverflowDropsOldest() { + RawPacketQueue queue(2U); + const uint8_t first = 1U; + const uint8_t second = 2U; + const uint8_t third = 3U; + SRawUsbPacket packet; + bool result = true; + + result = expect(queue.push(&first, 1U), "overflow push first") && result; + result = expect(queue.push(&second, 1U), "overflow push second") && result; + result = expect(queue.push(&third, 1U), "overflow push third") && result; + queue.close(); + result = expect(queue.waitPop(&packet), "overflow pop second") && result; + result = expect(packet.payload[0] == second, "oldest packet dropped") && + result; + result = expect(queue.waitPop(&packet), "overflow pop third") && result; + result = expect(packet.payload[0] == third, "newest packet retained") && + result; + result = expect(!queue.waitPop(&packet), "closed empty queue") && result; + result = expect(queue.getDroppedPacketCount() == 1U, "drop counter") && + result; + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testCloseWakesConsumer */ +static bool testCloseWakesConsumer() { + RawPacketQueue queue(1U); + bool popResult = true; + std::thread consumer([&queue, &popResult]() { + SRawUsbPacket packet; + + popResult = queue.waitPop(&packet); + }); + bool result = true; + + queue.close(); + consumer.join(); + result = expect(!popResult, "close wakes an empty consumer") && result; + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testResetReopensQueue */ +static bool testResetReopensQueue() { + RawPacketQueue queue(1U); + const uint8_t first = 1U; + const uint8_t second = 2U; + SRawUsbPacket packet; + bool result = true; + + queue.push(&first, 1U); + queue.push(&second, 1U); + queue.close(); + queue.reset(); + result = expect(queue.getDroppedPacketCount() == 0U, "reset drop counter") + && result; + result = expect(queue.push(&first, 1U), "push after reset") && result; + result = expect(queue.waitPop(&packet), "pop after reset") && result; + result = expect(packet.payload[0] == first, "reset clears old packets") && + result; + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testProducerConsumer */ +static bool testProducerConsumer() { + static const size_t PACKET_TOTAL = 64U; + RawPacketQueue queue(PACKET_TOTAL); + std::vector received; + std::thread consumer([&queue, &received]() { + SRawUsbPacket packet; + + while (queue.waitPop(&packet)) { + received.push_back(packet.payload[0]); + } + }); + size_t packetIndex = 0U; + bool result = true; + + for (packetIndex = 0U; packetIndex < PACKET_TOTAL; ++packetIndex) { + const uint8_t value = static_cast(packetIndex); + + result = expect(queue.push(&value, 1U), "concurrent push") && result; + } + queue.close(); + consumer.join(); + result = expect(received.size() == PACKET_TOTAL, "concurrent packet count") + && result; + for (packetIndex = 0U; packetIndex < received.size(); ++packetIndex) { + result = expect( + received[packetIndex] == static_cast(packetIndex), + "concurrent FIFO order" + ) && result; + } + + return result; +} + +} // namespace + +/********************* Application Programming Interface *********************/ + +/** @fn main */ +int main() { + bool passed = true; + int result = 1; + + passed = testFifoAndLength() && passed; + passed = testOverflowDropsOldest() && passed; + passed = testCloseWakesConsumer() && passed; + passed = testResetReopensQueue() && passed; + passed = testProducerConsumer() && passed; + if (passed) { + result = 0; + } + + return result; +} +/******************************************************************************/ \ No newline at end of file From 6c3d3fcee609ffc53074b131664e4959f50f6070 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 23:57:28 +0300 Subject: [PATCH 3/8] Verified raw USB packet buffering --- HISTORY.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index babdbee..0e2515d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -226,6 +226,21 @@ Records key decisions, structural changes, and completed development stages. close/reset behavior, and concurrent producer-consumer operation. - Moved the expanded capture module into `capture/inc/` and `capture/src/`. +### Hardware verification + +- Verified a 60-second acquisition run and 11 repeated Start/Stop cycles on a + physical Hantek DSO-2250 without GUI stalls or loss of the USB connection. +- Confirmed the expected LED lifecycle: red while connected and stopped, green + during acquisition, and off after application shutdown. +- Verified active USB removal: acquisition stopped without a hang and reported + `USB device lost while beginning command`. +- Verified automatic device rediscovery, explicit reconnection, and successful + acquisition after reconnecting the instrument. +- Verified application shutdown during acquisition without a crash, deadlock, + or unbounded wait. +- No persistent recovery state or unexpected acquisition stop occurred during + normal operation; the Release build and FIFO test passed without warnings. + ### Next USB tasks - Decode capture-state responses and acquired sample packets. From bc12e963871332f26ade7e57284b33e45273a86c Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Fri, 4 Sep 2026 23:58:25 +0300 Subject: [PATCH 4/8] Updated gitignore file --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b766ea8..ef48c84 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ WorkingDocs firmware/* !firmware/.gitkeep +# Wiki repository +wiki/ + From cac70c85312b38ba1b98db5f4674455ee112444c Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 00:02:11 +0300 Subject: [PATCH 5/8] Documented tests directory --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cc3eb62..98864ac 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Oscilloscope/ ├── docs/ Project documentation. ├── firmware/ Default path for local device firmware files. ├── render/ Oscilloscope waveform rendering. +├── tests/ Automated tests run through CTest. ├── tools/ Optional firmware extraction utilities. ├── ui/ User interface. ├── usb/ USB device communication. From 9355eab58c1cf2a472099608a5a4e713b5b50d35 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 00:12:37 +0300 Subject: [PATCH 6/8] Updated architecture documentation --- docs/mainpage.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/mainpage.md b/docs/mainpage.md index 679688a..bd7bb6c 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -33,11 +33,15 @@ libusb for communication with supported instruments. | Module | Responsibility | | --- | --- | | `app/` | Application entry point, event loop, and UI composition | -| `core/` | Shared application types and state | -| `usb/` | Device discovery and USB communication | | `capture/` | Sample acquisition and buffering | -| `render/` | Waveform rendering | -| `ui/` | Reusable interface components | +| `core/` | Planned shared application types and state | +| `docs/` | Generated documentation sources | +| `firmware/` | Default path for local device firmware files | +| `render/` | Planned waveform rendering | +| `tests/` | Automated tests run through CTest | +| `tools/` | Optional firmware extraction utilities | +| `ui/` | Planned reusable interface components | +| `usb/` | Device discovery, firmware loading, and USB communication | ## USB device operation From 1f38efb775117bf745e0f8445651cc40826c1e46 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 00:17:44 +0300 Subject: [PATCH 7/8] Fixed queue Doxygen symbols --- .github/instructions/cpp-style.instructions.md | 11 +++++++---- capture/src/raw_packet_queue.cpp | 12 ++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/instructions/cpp-style.instructions.md b/.github/instructions/cpp-style.instructions.md index b45d076..ca00662 100644 --- a/.github/instructions/cpp-style.instructions.md +++ b/.github/instructions/cpp-style.instructions.md @@ -92,10 +92,13 @@ Every public or non-obvious `#define` gets a Doxygen block; align related values ## Functions -One-line `/** @fn name */` immediately before the **definition**. K&R brace -(opening brace on the signature line). If a condition does not fit on one line, -put the first sub-expression on the next line, align sub-expressions, and place -the closing parenthesis with the opening brace on their own line: +One-line `/** @fn name */` immediately before a free-function **definition**. +For a class member definition, use its qualified declaration, for example +`/** @fn bool ClassName::method(int value) */`, so Doxygen resolves the member. +Use a K&R brace (opening brace on the signature line). If a condition does not +fit on one line, put the first sub-expression on the next line, align +sub-expressions, and place the closing parenthesis with the opening brace on +their own lines: ```c /** @fn acroAddTask */ diff --git a/capture/src/raw_packet_queue.cpp b/capture/src/raw_packet_queue.cpp index 25b5a12..351cff6 100644 --- a/capture/src/raw_packet_queue.cpp +++ b/capture/src/raw_packet_queue.cpp @@ -32,7 +32,7 @@ namespace capture { /********************* Application Programming Interface *********************/ -/** @fn RawPacketQueue */ +/** @fn RawPacketQueue::RawPacketQueue(size_t capacity) */ RawPacketQueue::RawPacketQueue(const size_t capacity) : packets(capacity), readIndex(0U), @@ -44,7 +44,7 @@ RawPacketQueue::RawPacketQueue(const size_t capacity) : /*----------------------------------------------------------------------------*/ -/** @fn push */ +/** @fn bool RawPacketQueue::push(const uint8_t *data, size_t length) */ bool RawPacketQueue::push(const uint8_t *data, const size_t length) { bool accepted = false; std::lock_guard lock(mutex); @@ -75,7 +75,7 @@ bool RawPacketQueue::push(const uint8_t *data, const size_t length) { /*----------------------------------------------------------------------------*/ -/** @fn waitPop */ +/** @fn bool RawPacketQueue::waitPop(SRawUsbPacket *packet) */ bool RawPacketQueue::waitPop(SRawUsbPacket *packet) { bool packetReturned = false; std::unique_lock lock(mutex); @@ -96,7 +96,7 @@ bool RawPacketQueue::waitPop(SRawUsbPacket *packet) { /*----------------------------------------------------------------------------*/ -/** @fn close */ +/** @fn void RawPacketQueue::close() */ void RawPacketQueue::close() { std::lock_guard lock(mutex); @@ -106,7 +106,7 @@ void RawPacketQueue::close() { /*----------------------------------------------------------------------------*/ -/** @fn reset */ +/** @fn void RawPacketQueue::reset() */ void RawPacketQueue::reset() { std::lock_guard lock(mutex); @@ -119,7 +119,7 @@ void RawPacketQueue::reset() { /*----------------------------------------------------------------------------*/ -/** @fn getDroppedPacketCount */ +/** @fn size_t RawPacketQueue::getDroppedPacketCount() const */ size_t RawPacketQueue::getDroppedPacketCount() const { size_t result = 0U; std::lock_guard lock(mutex); From b9ae044ac6bc4de3e19c8e83d6068af9fe2164a5 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 00:19:23 +0300 Subject: [PATCH 8/8] Updated Doxygen capture paths --- docs/Doxyfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Doxyfile b/docs/Doxyfile index ec90f37..11ed396 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -912,7 +912,8 @@ INPUT = ./mainpage.md \ ../app \ ../usb/inc \ ../usb/src \ - ../capture + ../capture/inc \ + ../capture/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