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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions .github/instructions/cpp-style.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -181,6 +184,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**
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ WorkingDocs
firmware/*
!firmware/.gitkeep

# Wiki repository
wiki/

33 changes: 28 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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
)
Expand All @@ -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()

32 changes: 31 additions & 1 deletion HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,39 @@ 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/`.

### 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.
- 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.
54 changes: 18 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -55,16 +35,19 @@ 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.
├── tests/ Automated tests run through CTest.
├── 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.
├── Makefile Make build entry points.
└── linux_build.sh Linux build script.
Expand Down Expand Up @@ -224,9 +207,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`.
Expand Down
10 changes: 7 additions & 3 deletions capture/acquisition_loop.h → capture/inc/acquisition_loop.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <atomic>
#include <thread>

#include "raw_packet_queue.h"
#include "usb_device.h"

/********************************* Definitions ********************************/
Expand Down Expand Up @@ -56,6 +57,7 @@ enum class EAcquisitionOperation {
/** @brief Acquisition state safe to read from any thread */
struct SAcquisitionStatus {
std::atomic<int> lastCaptureState{-1}; /**< Last raw capture state */
std::atomic<size_t> droppedPacketCount{0U}; /**< Queue overflow count */
std::atomic<EAcquisitionState> state{
EAcquisitionState::eStopped
}; /**< Current worker state */
Expand All @@ -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<bool> 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<bool> stopRequested{false}; /**< Set to request a stop */
SAcquisitionStatus status; /**< Shared poll status */
};

/********************* Application Programming Interface *********************/
Expand Down
99 changes: 99 additions & 0 deletions capture/inc/raw_packet_queue.h
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

#ifndef RAW_PACKET_QUEUE_H_
#define RAW_PACKET_QUEUE_H_

/******************************* Included files ******************************/
#include <array>
#include <condition_variable>
#include <stddef.h>
#include <stdint.h>
#include <mutex>
#include <vector>

/********************************* 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<uint8_t, RAW_USB_PACKET_MAX_SIZE> 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<SRawUsbPacket> 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_
Loading
Loading