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
31 changes: 30 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ set(SOURCES_LIST
usb/src/firmware_loader.cpp
capture/src/acquisition_loop.cpp
capture/src/raw_packet_queue.cpp
capture/src/waveform_parser.cpp
${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_tables.cpp
Expand All @@ -58,6 +59,15 @@ set(HEADERS_LIST
usb/inc/firmware_loader.h
capture/inc/acquisition_loop.h
capture/inc/raw_packet_queue.h
capture/inc/waveform_parser.h
)

set(RAW_PACKET_QUEUE_TEST_SOURCES_LIST
tests/raw_packet_queue_test.cpp
)

set(WAVEFORM_PARSER_TEST_SOURCES_LIST
tests/waveform_parser_test.cpp
)

if(CMAKE_BUILD_TYPE MATCHES "Debug")
Expand Down Expand Up @@ -103,8 +113,10 @@ enable_project_warnings(${APP_NAME})
include(CTest)

if(BUILD_TESTING)
find_package(Python3 REQUIRED COMPONENTS Interpreter)

add_executable(raw_packet_queue_tests
tests/raw_packet_queue_test.cpp
${RAW_PACKET_QUEUE_TEST_SOURCES_LIST}
capture/src/raw_packet_queue.cpp
)
target_include_directories(raw_packet_queue_tests PRIVATE
Expand All @@ -113,5 +125,22 @@ if(BUILD_TESTING)
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)
add_dependencies(${APP_NAME} raw_packet_queue_tests)
add_executable(waveform_parser_tests
${WAVEFORM_PARSER_TEST_SOURCES_LIST}
capture/src/waveform_parser.cpp
)
target_include_directories(waveform_parser_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/capture/inc
)
enable_project_warnings(waveform_parser_tests)
add_test(NAME waveform_parser COMMAND waveform_parser_tests)
add_dependencies(${APP_NAME} waveform_parser_tests)
add_test(
NAME release_updater
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/release_updater_test.py
${CMAKE_CURRENT_SOURCE_DIR}
)
endif()

76 changes: 76 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,82 @@ Records key decisions, structural changes, and completed development stages.

---

## 2026-09-05

### Stage 4 preparation - Release metadata coverage

- Registered standalone C++ test sources in dedicated CMake source lists.
- Extended the release updater to discover C++ test sources and update their
`@version` metadata together with production sources.
- Added deterministic CTest coverage that runs the updater in an isolated copy
of the project and verifies test-source version updates.
- Corrected the documented DSO-2250 operational endpoint configuration to
interface 0, alternate setting 0.

### Stage 4 - Sample parser foundation

- Added pure DSO-2250 parser functions for capture-state responses and complete
interleaved two-channel waveform buffers.
- Preserved the legacy byte layout: each sample pair is CH2 followed by CH1.
- Preserved the legacy capture-state trigger-point transformation in the new,
Qt-independent parser.
- Added deterministic CTest coverage for state parsing, trigger decoding,
channel order, and malformed capture buffers.

### Stage 4 - Complete DSO-2250 capture reads

- Start acquisition with the legacy capture-start and trigger-enable command
sequence before capture-state polling begins.
- On a complete DSO-2250 capture-state response, read the fixed two-channel
32768-sample buffer as 128 consecutive 512-byte bulk packets.
- Stop incomplete waveform publication at the first failed packet and preserve
the existing bounded recovery and device-loss behavior.
- Restart capture and re-enable the trigger only after a complete buffer was
read and queued for processing.
- Added acquisition-status diagnostics for channel-data, capture-start, and
trigger-enable failures.

### Stage 4 - Decoded waveform publication

- Preserved the decoded capture trigger point alongside its raw waveform bytes
while the frame passes through the FIFO.
- Decode complete queued DSO-2250 frames in the processing worker instead of
interpreting raw channel bytes as capture-state data.
- Added a mutex-protected latest-waveform snapshot and `getLatestWaveform()`
API for the future renderer.
- Extended FIFO tests to verify trigger-point metadata is retained with each
packet.

### Capture protocol profiles

- Moved capture endpoints, command bytes, packet length, channel layout,
sample count, and completion state into each supported-device entry.
- Preserve the selected profile in the active USB connection and use it for
acquisition and waveform parsing instead of model-named function contracts.
- Validate profile packetisation and fixed capture-storage bounds before
requesting sample data.

### Stage 4 - Hardware verification

- Verified a 60-second acquisition on the initial Hantek DSO-2250 profile.
- Verified six 10-second Start/Stop cycles: the UI stayed responsive and the
connected device was ready for every subsequent Start.
- Verified active USB removal: acquisition stopped without a crash, deadlock,
or unbounded wait and reported the lost device while beginning a command.
- Verified automatic rediscovery, explicit reconnection, and a subsequent
successful acquisition after the device was reattached.
- Verified application shutdown during acquisition without a crash, deadlock,
or unbounded wait; the instrument returned to its connected idle LED state,
then turned off after application exit.
- Verified the Release build without warnings. Normal acquisition showed no
persistent recovery state or unexpected acquisition stop.

### Code style - Immutable object naming

- Reserved `UPPER_CASE` for preprocessor macros.
- Renamed immutable module objects to the `kPascalCase` convention while
retaining `const` storage and unchanged runtime behavior.

## 2026-08-27

### Project inception
Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ The application currently provides:

- an SDL2, Dear ImGui, and OpenGL user interface;
- Demo and Live operating modes;
- Hantek DSO-2250 discovery and connection through libusb;
- supported Hantek device discovery and connection through libusb;
- FX2 firmware upload and operational-device re-enumeration;
- 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.
Capture-state responses and sample buffers are decoded by deterministic,
hardware-independent parser functions. Each supported-device entry supplies its
capture protocol, including endpoints, packet size, commands, channel layout,
sample count, and completion state. After a completed capture, the acquisition
worker reads and queues the complete profile-defined sample buffer, then starts
the next capture. Live waveform rendering is not yet implemented. A processing
worker decodes complete captures and publishes the latest frame and trigger
point safely for rendering.

## Planned Stack

Expand Down Expand Up @@ -194,6 +200,7 @@ The script synchronizes:

- the CMake project version in `CMakeLists.txt`;
- the `@version` field in `app/main.cpp`;
- the `@version` field in C++ test sources registered in CMake source lists;
- the `VERSION_MAJOR`, `VERSION_MINOR`, and `VERSION_PATCH` macros;
- the embedded `FileVersion` and `ProductVersion` strings;
- the version shown in `README.md`, `docs/mainpage.md`, and `docs/Doxyfile`.
Expand All @@ -206,9 +213,8 @@ CI can update only the version metadata by passing `--skip-build`.

## Next Steps

1. Decode capture-state responses and acquired sample packets.
2. Render live and demo waveforms on the display grid.
3. Implement the two-channel model, timebase, and instrument controls.
1. Render live and demo waveforms on the display grid.
2. Implement the two-channel model, timebase, and instrument controls.

The full goals, constraints, and architecture are documented in
`WorkingDocs/TECHNICAL_SPECIFICATION.md`.
Expand Down
38 changes: 25 additions & 13 deletions app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ using oscilloscope::usb::SUsbScanResult;
/****************************** Module variables ******************************/

/** @brief Interval between USB presence checks outside acquisition */
static const uint32_t USB_PRESENCE_INTERVAL_MS = 1000U;
static const uint32_t kUsbPresenceIntervalMs = 1000U;

/** @brief Cleared device identity used to reset connection bookkeeping */
static const SUsbDeviceInfo EMPTY_DEVICE_INFO = {0U, 0U, 0U, 0U, NULL};
static const SUsbDeviceInfo kEmptyDeviceInfo = { NULL, 0U, 0U, 0U, 0U };

#ifdef __GNUC__ // GCC/MinGW only
const char version_info[] __attribute__((section(".version"), used)) =
const char kVersionInfo[] __attribute__((section(".version"), used)) =
"FileDescription: Oscilloscope application\n"
"FileVersion: 0.2.6.0\n"
"ProductName: Oscilloscope\n"
Expand All @@ -113,7 +113,7 @@ const char version_info[] __attribute__((section(".version"), used)) =
"LegalCopyright: Copyright (C) Anton Chernov, 2026\n"
"OriginalFilename: run\n";

const char build_info[] __attribute__((section(".buildinfo"), used)) =
const char kBuildInfo[] __attribute__((section(".buildinfo"), used)) =
"Build date: " __DATE__ " " __TIME__ "\n"
"Compiler: GCC " __VERSION__ "\n";

Expand Down Expand Up @@ -307,21 +307,21 @@ int main (void) {
bool deviceWasDisconnected = false;
bool channelEnabled[] = {true, true};
int timebase = 6;
int voltsPerDivision[] = {2, 2};
int voltsPerDivision[] = {7, 7};
SUsbScanResult usbScanResult =
oscilloscope::usb::enumerateSupportedDevices();
bool demoMode =
(usbScanResult.status != EScanStatus::eSuccess) ||
usbScanResult.devices.empty();
SUsbConnection usbConnection = {NULL, NULL, 0U, false};
SUsbConnection usbConnection = { NULL, NULL, {}, 0U, false };
SAcquisitionLoop acquisitionLoop;
SUsbDeviceInfo connectedDevice = EMPTY_DEVICE_INFO;
SUsbDeviceInfo connectedDevice = kEmptyDeviceInfo;
std::string deviceStatus = formatUsbConnectionStatus(
usbScanResult,
usbConnection
);
uint32_t nextUsbPresenceCheck =
SDL_GetTicks() + USB_PRESENCE_INTERVAL_MS;
SDL_GetTicks() + kUsbPresenceIntervalMs;
const char* timebases[] = {
"4 ns/div", "20 ns/div", "100 ns/div", "1 us/div", "10 us/div",
"100 us/div", "1 ms/div", "10 ms/div", "100 ms/div", "1 s/div"
Expand Down Expand Up @@ -459,7 +459,7 @@ int main (void) {
const SUsbConnectionResult disconnectResult =
oscilloscope::usb::disconnectFromDevice(&usbConnection);

connectedDevice = EMPTY_DEVICE_INFO;
connectedDevice = kEmptyDeviceInfo;
deviceWasDisconnected = false;
if (disconnectResult.errorMessage.empty()) {
deviceStatus = formatUsbConnectionStatus(
Expand Down Expand Up @@ -695,6 +695,18 @@ static std::string formatAcquisitionError(
case EAcquisitionOperation::eCaptureStateResponse:
status += " while reading capture state";
break;
case EAcquisitionOperation::eChannelDataCommand:
status += " while requesting channel data";
break;
case EAcquisitionOperation::eChannelDataResponse:
status += " while reading channel data";
break;
case EAcquisitionOperation::eCaptureStartCommand:
status += " while starting capture";
break;
case EAcquisitionOperation::eTriggerEnabledCommand:
status += " while enabling trigger";
break;
case EAcquisitionOperation::eNone:
default:
break;
Expand Down Expand Up @@ -730,7 +742,7 @@ static void handleAcquisitionFault(
*acquisitionRunning = false;
if (acquisitionState == EAcquisitionState::eDeviceLost) {
oscilloscope::usb::disconnectFromDevice(connection);
*connectedDevice = EMPTY_DEVICE_INFO;
*connectedDevice = kEmptyDeviceInfo;
*deviceWasDisconnected = true;
*deviceStatus = "Device disconnected: " + acquisitionError;
}
Expand Down Expand Up @@ -760,15 +772,15 @@ static void pollUsbPresence(
(static_cast<int32_t>(currentTicks - *nextPresenceCheck) >= 0)
) {
*usbScanResult = oscilloscope::usb::enumerateSupportedDevices();
*nextPresenceCheck = currentTicks + USB_PRESENCE_INTERVAL_MS;
*nextPresenceCheck = currentTicks + kUsbPresenceIntervalMs;

if (
connection->isConnected &&
(usbScanResult->status == EScanStatus::eSuccess) &&
!isUsbDevicePresent(*usbScanResult, *connectedDevice)
) {
oscilloscope::usb::disconnectFromDevice(connection);
*connectedDevice = EMPTY_DEVICE_INFO;
*connectedDevice = kEmptyDeviceInfo;
*deviceWasDisconnected = true;
*deviceStatus = "Device disconnected";
}
Expand Down Expand Up @@ -809,7 +821,7 @@ static void updateDemoMode(
const SUsbConnectionResult disconnectResult =
oscilloscope::usb::disconnectFromDevice(connection);

*connectedDevice = EMPTY_DEVICE_INFO;
*connectedDevice = kEmptyDeviceInfo;
if (!disconnectResult.errorMessage.empty()) {
*deviceStatus = formatUsbConnectionError(
"Disconnect",
Expand Down
30 changes: 27 additions & 3 deletions capture/inc/acquisition_loop.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@

/******************************* Included files ******************************/
#include <atomic>
#include <mutex>
#include <thread>

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

/********************************* Definitions ********************************/

Expand All @@ -51,13 +53,17 @@ enum class EAcquisitionOperation {
eSpeedBeforeCommand, /**< Speed control read before command write */
eCaptureStateCommand, /**< Capture-state bulk command write */
eSpeedBeforeResponse, /**< Speed control read before response read */
eCaptureStateResponse /**< Capture-state bulk response read */
eCaptureStateResponse, /**< Capture-state bulk response read */
eChannelDataCommand, /**< Channel-data bulk command write */
eChannelDataResponse, /**< Channel-data bulk response read */
eCaptureStartCommand, /**< Capture-start bulk command write */
eTriggerEnabledCommand /**< Trigger-enable bulk command write */
};

/** @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<int> lastCaptureState{-1}; /**< Last raw capture state */
std::atomic<EAcquisitionState> state{
EAcquisitionState::eStopped
}; /**< Current worker state */
Expand All @@ -74,8 +80,13 @@ struct SAcquisitionLoop {
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 */
usb::SUsbCaptureProtocol captureProtocol{}; /**< Active capture format */
mutable std::mutex waveformMutex; /**< Guards the latest waveform */
SWaveformSamples latestWaveform{}; /**< Most recently decoded frame */
uint32_t latestTriggerPoint{0U}; /**< Trigger point for latest frame */
std::atomic<bool> stopRequested{false}; /**< Set to request a stop */
bool hasWaveform{false}; /**< True after first decoded frame */
};

/********************* Application Programming Interface *********************/
Expand Down Expand Up @@ -106,6 +117,19 @@ void stopAcquisitionLoop(SAcquisitionLoop *loop);
*/
bool joinFinishedAcquisitionLoop(SAcquisitionLoop *loop);

/**
* @brief Copies the most recently decoded waveform frame
* @param[in] loop Acquisition loop that owns the latest frame
* @param[out] waveform Destination for waveform samples
* @param[out] triggerPoint Destination for the waveform trigger position
* @returns True when a decoded waveform frame is available
*/
bool getLatestWaveform(
const SAcquisitionLoop *loop,
SWaveformSamples *waveform,
uint32_t *triggerPoint
);

} // namespace capture
} // namespace oscilloscope
/******************************************************************************/
Expand Down
Loading
Loading