From 4c0219df5610f2409f4bd2da84a0ce0224b76f54 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 18:03:30 +0300 Subject: [PATCH 01/10] Tracked test sources for version updates --- CMakeLists.txt | 15 ++++++++- HISTORY.md | 13 ++++++++ README.md | 1 + docs/mainpage.md | 2 +- make_release.py | 2 +- tests/release_updater_test.py | 59 +++++++++++++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/release_updater_test.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 144e0e8..9903233 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,10 @@ set(HEADERS_LIST capture/inc/raw_packet_queue.h ) +set(TEST_SOURCES_LIST + tests/raw_packet_queue_test.cpp +) + if(CMAKE_BUILD_TYPE MATCHES "Debug") message(STATUS ">>> Debug build") add_compile_definitions(_DEBUG) @@ -103,8 +107,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 + ${TEST_SOURCES_LIST} capture/src/raw_packet_queue.cpp ) target_include_directories(raw_packet_queue_tests PRIVATE @@ -113,5 +119,12 @@ 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_test( + NAME release_updater + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/release_updater_test.py + ${CMAKE_CURRENT_SOURCE_DIR} + ) endif() diff --git a/HISTORY.md b/HISTORY.md index 0e2515d..f196250 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,6 +5,19 @@ Records key decisions, structural changes, and completed development stages. --- +## 2026-09-05 + +### Stage 4 preparation - Release metadata coverage + +- Registered standalone C++ test sources in `TEST_SOURCES_LIST` in the CMake + build configuration. +- 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 endpoint configuration to interface 0, + alternate setting 1. + ## 2026-08-27 ### Project inception diff --git a/README.md b/README.md index 277297f..19c2b3f 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,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 `TEST_SOURCES_LIST`; - 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`. diff --git a/docs/mainpage.md b/docs/mainpage.md index 93bfc3f..6a885e8 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -52,7 +52,7 @@ 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. +`04b5:2250`, and opens its bulk endpoints on interface 0, alternate setting 1. 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 diff --git a/make_release.py b/make_release.py index dd187ea..e633d1d 100755 --- a/make_release.py +++ b/make_release.py @@ -116,7 +116,7 @@ def findSourceFiles(self) -> tuple: cmake_content = opened_file.read() source_files = [] - for list_name in ("SOURCES_LIST", "HEADERS_LIST"): + for list_name in ("SOURCES_LIST", "HEADERS_LIST", "TEST_SOURCES_LIST"): match = re.search( r"set\s*\(\s*{}\s*(.*?)\)".format(list_name), cmake_content, diff --git a/tests/release_updater_test.py b/tests/release_updater_test.py new file mode 100644 index 0000000..db8e600 --- /dev/null +++ b/tests/release_updater_test.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Regression coverage for release metadata updates in standalone tests.""" +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def main() -> int: + source_directory = Path(sys.argv[1]).resolve() + expected_version = "9.8.7" + result = 1 + + with tempfile.TemporaryDirectory() as temporary_directory: + project_directory = Path(temporary_directory) / "project" + project_directory.mkdir() + for path in ( + "CMakeLists.txt", + "make_release.py", + "app", + "capture", + "docs", + "tests/raw_packet_queue_test.cpp", + "usb", + ): + source_path = source_directory / path + destination_path = project_directory / path + if source_path.is_dir(): + shutil.copytree(source_path, destination_path) + else: + destination_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, destination_path) + + completed_process = subprocess.run( + [ + sys.executable, + "make_release.py", + expected_version, + "--skip-build", + "--skip-readme", + ], + cwd=project_directory, + check=False, + ) + test_source = project_directory / "tests/raw_packet_queue_test.cpp" + expected_header = " * @version {}".format(expected_version) + + if ( + (completed_process.returncode == 0) and + (expected_header in test_source.read_text(encoding="utf-8")) + ): + result = 0 + + return result + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file From e1749f5cb5c31aa751e5b0a077d32abcfb51231a Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 18:05:32 +0300 Subject: [PATCH 02/10] Added DSO-2250 waveform parser --- CMakeLists.txt | 20 ++++- HISTORY.md | 13 ++- README.md | 7 +- capture/inc/waveform_parser.h | 86 ++++++++++++++++++ capture/src/waveform_parser.cpp | 139 ++++++++++++++++++++++++++++ docs/mainpage.md | 5 +- make_release.py | 15 ++-- tests/release_updater_test.py | 9 +- tests/waveform_parser_test.cpp | 155 ++++++++++++++++++++++++++++++++ 9 files changed, 429 insertions(+), 20 deletions(-) create mode 100644 capture/inc/waveform_parser.h create mode 100644 capture/src/waveform_parser.cpp create mode 100644 tests/waveform_parser_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9903233..28549b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 @@ -58,12 +59,17 @@ 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(TEST_SOURCES_LIST +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") message(STATUS ">>> Debug build") add_compile_definitions(_DEBUG) @@ -110,7 +116,7 @@ if(BUILD_TESTING) find_package(Python3 REQUIRED COMPONENTS Interpreter) add_executable(raw_packet_queue_tests - ${TEST_SOURCES_LIST} + ${RAW_PACKET_QUEUE_TEST_SOURCES_LIST} capture/src/raw_packet_queue.cpp ) target_include_directories(raw_packet_queue_tests PRIVATE @@ -120,6 +126,16 @@ if(BUILD_TESTING) 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} diff --git a/HISTORY.md b/HISTORY.md index f196250..72a9536 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -9,8 +9,7 @@ Records key decisions, structural changes, and completed development stages. ### Stage 4 preparation - Release metadata coverage -- Registered standalone C++ test sources in `TEST_SOURCES_LIST` in the CMake - build configuration. +- 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 @@ -18,6 +17,16 @@ Records key decisions, structural changes, and completed development stages. - Corrected the documented DSO-2250 endpoint configuration to interface 0, alternate setting 1. +### 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. + ## 2026-08-27 ### Project inception diff --git a/README.md b/README.md index 19c2b3f..d56d273 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ The application currently provides: - 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. +The DSO-2250 capture-state response and its two-channel interleaved sample +format are decoded by deterministic, hardware-independent parser functions. +Waveform reads and live waveform rendering are not yet implemented. ## Planned Stack @@ -194,7 +195,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 `TEST_SOURCES_LIST`; +- 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`. diff --git a/capture/inc/waveform_parser.h b/capture/inc/waveform_parser.h new file mode 100644 index 0000000..7fe3b9b --- /dev/null +++ b/capture/inc/waveform_parser.h @@ -0,0 +1,86 @@ +/** + * @file waveform_parser.h + * @version 0.2.6 + * @authors Anton Chernov + * @date 2026-09-05 + * @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 WAVEFORM_PARSER_H_ +#define WAVEFORM_PARSER_H_ + +/******************************* Included files ******************************/ +#include +#include +#include + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace capture { + +/** @brief Maximum samples in a DSO-2250 two-channel capture */ +static const size_t WAVEFORM_MAX_SAMPLE_COUNT = 32768U; + +/** @brief Decoded capture state and normalized trigger sample position */ +struct SCaptureStateResponse { + uint8_t captureState; /**< Device capture state byte */ + uint32_t triggerPoint; /**< Trigger position in the sample ring */ +}; + +/** @brief One decoded two-channel DSO-2250 waveform capture */ +struct SWaveformSamples { + std::array channelOne; /**< CH1 */ + std::array channelTwo; /**< CH2 */ + size_t sampleCount; /**< Valid sample count in both channel arrays */ +}; + +/********************* Application Programming Interface *********************/ + +/** + * @brief Decodes the DSO-2250 capture-state response packet + * @param[in] data Raw USB response data + * @param[in] length Number of bytes in data + * @param[out] response Destination for decoded state and trigger position + * @returns True when the response contains the required four bytes + */ +bool parseCaptureStateResponse( + const uint8_t *data, + size_t length, + SCaptureStateResponse *response +); + +/** + * @brief Decodes an interleaved two-channel DSO-2250 sample buffer + * @param[in] data Raw USB capture data + * @param[in] length Number of bytes in data + * @param[in] sampleCount Expected samples in each channel + * @param[out] samples Destination for decoded channel samples + * @returns True when the complete expected two-channel buffer was decoded + * @note The legacy protocol orders each pair as CH2 then CH1 + */ +bool parseInterleavedWaveformSamples( + const uint8_t *data, + size_t length, + size_t sampleCount, + SWaveformSamples *samples +); + +} // namespace capture +} // namespace oscilloscope +/******************************************************************************/ +#endif //! WAVEFORM_PARSER_H_ \ No newline at end of file diff --git a/capture/src/waveform_parser.cpp b/capture/src/waveform_parser.cpp new file mode 100644 index 0000000..5dcb56c --- /dev/null +++ b/capture/src/waveform_parser.cpp @@ -0,0 +1,139 @@ +/** + * @file waveform_parser.cpp + * @version 0.2.6 + * @authors Anton Chernov + * @date 2026-09-05 + * @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 "waveform_parser.h" + +/***************************** Private prototypes *****************************/ + +namespace oscilloscope { +namespace capture { + +/** + * @brief Converts the device trigger position to its sample-ring position + * @param[in] triggerPoint Trigger position reported by the device + * @returns Trigger position used by the legacy waveform buffer + */ +static uint32_t normalizeTriggerPoint(uint32_t triggerPoint); + +/********************* Application Programming Interface *********************/ + +/** @fn parseCaptureStateResponse */ +bool parseCaptureStateResponse( + const uint8_t *data, + size_t length, + SCaptureStateResponse *response +) { + bool result = false; + uint32_t triggerPoint = 0U; + + if ((data != NULL) && (response != NULL) && (length >= 4U)) { + triggerPoint = + (static_cast(data[1]) << 16U) | + (static_cast(data[3]) << 8U) | + static_cast(data[2]); + response->captureState = data[0]; + response->triggerPoint = normalizeTriggerPoint(triggerPoint); + result = true; + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn parseInterleavedWaveformSamples */ +bool parseInterleavedWaveformSamples( + const uint8_t *data, + size_t length, + size_t sampleCount, + SWaveformSamples *samples +) { + bool result = false; + size_t sampleIndex = 0U; + size_t expectedLength = 0U; + + if (sampleCount <= WAVEFORM_MAX_SAMPLE_COUNT) { + expectedLength = sampleCount * 2U; + if ( + (data != NULL) && + (samples != NULL) && + (length == expectedLength) + ) { + for (sampleIndex = 0U; sampleIndex < sampleCount; ++sampleIndex) { + samples->channelTwo[sampleIndex] = data[sampleIndex * 2U]; + samples->channelOne[sampleIndex] = data[sampleIndex * 2U + 1U]; + } + samples->sampleCount = sampleCount; + result = true; + } + } + + return result; +} + +/****************************** Private functions *****************************/ + +/** @fn normalizeTriggerPoint */ +static uint32_t normalizeTriggerPoint(uint32_t triggerPoint) { + uint32_t upperBound = triggerPoint; + uint32_t lowerBound = 0U; + uint32_t offset = 0U; + uint32_t midpoint = 0U; + uint32_t highestPowerOfTwo = 1U; + bool direction = true; + bool result = true; + + while ((upperBound > 0U) && (highestPowerOfTwo <= (UINT32_MAX / 2U))) { + upperBound >>= 1U; + highestPowerOfTwo <<= 1U; + } + if (upperBound > 0U) { + result = false; + } + if (result) { + upperBound = highestPowerOfTwo - 1U; + while (upperBound > lowerBound) { + midpoint = (upperBound - lowerBound + 1U) / 2U + offset; + if ((midpoint > triggerPoint) == direction) { + if (!direction) { + offset = midpoint; + } + upperBound = (upperBound + lowerBound - 1U) / 2U; + direction = true; + } + else { + if (direction) { + offset = midpoint; + } + lowerBound = (lowerBound + upperBound + 1U) / 2U; + direction = false; + } + } + } + + return lowerBound; +} + +} // namespace capture +} // namespace oscilloscope +/******************************************************************************/ \ No newline at end of file diff --git a/docs/mainpage.md b/docs/mainpage.md index 6a885e8..8c87043 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -27,6 +27,7 @@ libusb for communication with supported instruments. - bounded buffering between USB reads and packet processing; - capture-state polling through vendor control requests and bulk endpoints; - bounded USB error recovery and safe device-loss handling. +- deterministic DSO-2250 capture-state and two-channel sample parsing. ## Architecture @@ -76,7 +77,7 @@ complete installation, build, and run instructions. ## Planned work -The next tasks are decoding capture responses and sample packets, reading -waveform data, and displaying live waveforms. Instrument controls, broader +The next tasks are reading waveform data and displaying live waveforms. +Instrument controls, broader recovery behavior, and persistent configuration follow in later milestones. diff --git a/make_release.py b/make_release.py index e633d1d..457cb90 100755 --- a/make_release.py +++ b/make_release.py @@ -116,15 +116,12 @@ def findSourceFiles(self) -> tuple: cmake_content = opened_file.read() source_files = [] - for list_name in ("SOURCES_LIST", "HEADERS_LIST", "TEST_SOURCES_LIST"): - match = re.search( - r"set\s*\(\s*{}\s*(.*?)\)".format(list_name), - cmake_content, - re.DOTALL, - ) - if match is None: - continue - + matches = re.finditer( + r"set\s*\(\s*\w*(?:SOURCES|HEADERS)_LIST\s*(.*?)\)", + cmake_content, + re.DOTALL, + ) + for match in matches: for line in match.group(1).splitlines(): path = line.split("#", 1)[0].strip() if path and "${" not in path and isfile(path): diff --git a/tests/release_updater_test.py b/tests/release_updater_test.py index db8e600..f4529cb 100644 --- a/tests/release_updater_test.py +++ b/tests/release_updater_test.py @@ -22,6 +22,7 @@ def main() -> int: "capture", "docs", "tests/raw_packet_queue_test.cpp", + "tests/waveform_parser_test.cpp", "usb", ): source_path = source_directory / path @@ -43,12 +44,16 @@ def main() -> int: cwd=project_directory, check=False, ) - test_source = project_directory / "tests/raw_packet_queue_test.cpp" expected_header = " * @version {}".format(expected_version) if ( (completed_process.returncode == 0) and - (expected_header in test_source.read_text(encoding="utf-8")) + (expected_header in ( + project_directory / "tests/raw_packet_queue_test.cpp" + ).read_text(encoding="utf-8")) and + (expected_header in ( + project_directory / "tests/waveform_parser_test.cpp" + ).read_text(encoding="utf-8")) ): result = 0 diff --git a/tests/waveform_parser_test.cpp b/tests/waveform_parser_test.cpp new file mode 100644 index 0000000..630449a --- /dev/null +++ b/tests/waveform_parser_test.cpp @@ -0,0 +1,155 @@ +/** + * @file waveform_parser_test.cpp + * @version 0.2.6 + * @authors Anton Chernov + * @date 2026-09-05 + * @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 "waveform_parser.h" + +/********************************* Definitions ********************************/ + +namespace { + +using oscilloscope::capture::parseCaptureStateResponse; +using oscilloscope::capture::parseInterleavedWaveformSamples; +using oscilloscope::capture::SCaptureStateResponse; +using oscilloscope::capture::SWaveformSamples; + +/***************************** Private prototypes *****************************/ + +static bool expect(bool condition, const char *message); +static bool testCaptureStateResponse(); +static bool testInterleavedSamples(); +static bool testInvalidSampleBuffers(); + +/****************************** Private functions *****************************/ + +/** @fn expect */ +static bool expect(bool condition, const char *message) { + bool result = condition; + + if (!condition) { + std::cerr << "FAILED: " << message << std::endl; + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testCaptureStateResponse */ +static bool testCaptureStateResponse() { + const uint8_t packet[] = {2U, 0U, 3U, 0U}; + SCaptureStateResponse response = {0U, 0U}; + bool result = true; + + result = expect( + parseCaptureStateResponse(packet, sizeof(packet), &response), + "parse capture state" + ) && result; + result = expect(response.captureState == 2U, "capture state value") && + result; + result = expect(response.triggerPoint == 2U, "legacy trigger transform") && + result; + result = expect( + !parseCaptureStateResponse(packet, 3U, &response), + "reject short capture state" + ) && result; + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testInterleavedSamples */ +static bool testInterleavedSamples() { + const uint8_t packet[] = {20U, 10U, 21U, 11U, 22U, 12U}; + SWaveformSamples samples = {}; + bool result = true; + + result = expect( + parseInterleavedWaveformSamples( + packet, sizeof(packet), 3U, &samples + ), + "parse interleaved samples" + ) && result; + result = expect(samples.sampleCount == 3U, "sample count") && result; + result = expect( + (samples.channelOne[0] == 10U) && + (samples.channelOne[1] == 11U) && + (samples.channelOne[2] == 12U), + "channel one order" + ) && result; + result = expect( + (samples.channelTwo[0] == 20U) && + (samples.channelTwo[1] == 21U) && + (samples.channelTwo[2] == 22U), + "channel two order" + ) && result; + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testInvalidSampleBuffers */ +static bool testInvalidSampleBuffers() { + const uint8_t packet[] = {20U, 10U, 21U, 11U}; + SWaveformSamples samples = {}; + bool result = true; + + samples.sampleCount = 99U; + result = expect( + !parseInterleavedWaveformSamples( + packet, sizeof(packet) - 1U, 2U, &samples + ), + "reject short sample buffer" + ) && result; + result = expect(samples.sampleCount == 99U, "preserve rejected samples") && + result; + result = expect( + !parseInterleavedWaveformSamples(packet, sizeof(packet), 32769U, &samples), + "reject oversized sample count" + ) && result; + + return result; +} + +} // namespace + +/********************* Application Programming Interface *********************/ + +/** @fn main */ +int main() { + bool passed = true; + int result = 1; + + passed = testCaptureStateResponse() && passed; + passed = testInterleavedSamples() && passed; + passed = testInvalidSampleBuffers() && passed; + if (passed) { + result = 0; + } + + return result; +} +/******************************************************************************/ \ No newline at end of file From 5d3bbe92abd6cc350515fd9330894f74f7db26d0 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 18:06:43 +0300 Subject: [PATCH 03/10] Corrected DSO-2250 interface documentation --- HISTORY.md | 4 ++-- docs/mainpage.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 72a9536..5fd0777 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -14,8 +14,8 @@ Records key decisions, structural changes, and completed development stages. `@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 endpoint configuration to interface 0, - alternate setting 1. +- Corrected the documented DSO-2250 operational endpoint configuration to + interface 0, alternate setting 0. ### Stage 4 - Sample parser foundation diff --git a/docs/mainpage.md b/docs/mainpage.md index 8c87043..27c995b 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -53,7 +53,7 @@ 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 1. +`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 From b4072a3cdf29c60e47b5a12f4725f80335b44f4c Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 18:38:11 +0300 Subject: [PATCH 04/10] Aligned immutable object naming --- HISTORY.md | 6 +++ app/main.cpp | 22 +++++------ capture/inc/raw_packet_queue.h | 4 +- capture/inc/waveform_parser.h | 6 +-- capture/src/acquisition_loop.cpp | 66 ++++++++++++++++---------------- capture/src/raw_packet_queue.cpp | 2 +- capture/src/waveform_parser.cpp | 2 +- tests/waveform_parser_test.cpp | 2 +- usb/src/usb_device.cpp | 18 ++++----- 9 files changed, 67 insertions(+), 61 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 5fd0777..695609e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -27,6 +27,12 @@ Records key decisions, structural changes, and completed development stages. - Added deterministic CTest coverage for state parsing, trigger decoding, channel order, and malformed capture buffers. +### 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 diff --git a/app/main.cpp b/app/main.cpp index 362d869..63a1f30 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -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 = {0U, 0U, 0U, 0U, NULL}; #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" @@ -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"; @@ -315,13 +315,13 @@ int main (void) { usbScanResult.devices.empty(); 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" @@ -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( @@ -730,7 +730,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; } @@ -760,7 +760,7 @@ static void pollUsbPresence( (static_cast(currentTicks - *nextPresenceCheck) >= 0) ) { *usbScanResult = oscilloscope::usb::enumerateSupportedDevices(); - *nextPresenceCheck = currentTicks + USB_PRESENCE_INTERVAL_MS; + *nextPresenceCheck = currentTicks + kUsbPresenceIntervalMs; if ( connection->isConnected && @@ -768,7 +768,7 @@ static void pollUsbPresence( !isUsbDevicePresent(*usbScanResult, *connectedDevice) ) { oscilloscope::usb::disconnectFromDevice(connection); - *connectedDevice = EMPTY_DEVICE_INFO; + *connectedDevice = kEmptyDeviceInfo; *deviceWasDisconnected = true; *deviceStatus = "Device disconnected"; } @@ -809,7 +809,7 @@ static void updateDemoMode( const SUsbConnectionResult disconnectResult = oscilloscope::usb::disconnectFromDevice(connection); - *connectedDevice = EMPTY_DEVICE_INFO; + *connectedDevice = kEmptyDeviceInfo; if (!disconnectResult.errorMessage.empty()) { *deviceStatus = formatUsbConnectionError( "Disconnect", diff --git a/capture/inc/raw_packet_queue.h b/capture/inc/raw_packet_queue.h index 1735328..bae179f 100644 --- a/capture/inc/raw_packet_queue.h +++ b/capture/inc/raw_packet_queue.h @@ -37,11 +37,11 @@ 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; +static const size_t kRawUsbPacketMaxSize = 65536U; /** @brief Owns one raw USB response and its valid byte count */ struct SRawUsbPacket { - std::array payload; /**< Raw bytes */ + std::array payload; /**< Raw bytes */ size_t validLength; /**< Number of valid bytes in payload */ }; diff --git a/capture/inc/waveform_parser.h b/capture/inc/waveform_parser.h index 7fe3b9b..6022b3b 100644 --- a/capture/inc/waveform_parser.h +++ b/capture/inc/waveform_parser.h @@ -34,7 +34,7 @@ namespace oscilloscope { namespace capture { /** @brief Maximum samples in a DSO-2250 two-channel capture */ -static const size_t WAVEFORM_MAX_SAMPLE_COUNT = 32768U; +static const size_t kWaveformMaxSampleCount = 32768U; /** @brief Decoded capture state and normalized trigger sample position */ struct SCaptureStateResponse { @@ -44,8 +44,8 @@ struct SCaptureStateResponse { /** @brief One decoded two-channel DSO-2250 waveform capture */ struct SWaveformSamples { - std::array channelOne; /**< CH1 */ - std::array channelTwo; /**< CH2 */ + std::array channelOne; /**< CH1 */ + std::array channelTwo; /**< CH2 */ size_t sampleCount; /**< Valid sample count in both channel arrays */ }; diff --git a/capture/src/acquisition_loop.cpp b/capture/src/acquisition_loop.cpp index 1dd47b5..5e06438 100644 --- a/capture/src/acquisition_loop.cpp +++ b/capture/src/acquisition_loop.cpp @@ -33,26 +33,26 @@ 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; +static const uint8_t kEpBulkOut = 0x02U; +static const uint8_t kEpBulkIn = 0x86U; +static const uint16_t kEpBulkInMaxPacketLen = 512U; /** @brief Vendor control requests used by the polling cycle */ -static const uint8_t CONTROL_GETSPEED = 0xB2U; -static const uint8_t CONTROL_BEGINCOMMAND = 0xB3U; +static const uint8_t kControlGetSpeed = 0xB2U; +static const uint8_t kControlBeginCommand = 0xB3U; /** @brief Length of the FX2 connection-speed control response */ -static const uint16_t SPEED_RESPONSE_LEN = 10U; +static const uint16_t kSpeedResponseLen = 10U; /** @brief Protocol command byte for the "get capture state" request */ -static const uint8_t CMD_GET_CAPTURE_STATE = 6U; +static const uint8_t kCmdGetCaptureState = 6U; /** @brief Delay between poll cycles; also the effective host-activity rate */ -static const unsigned int POLL_INTERVAL_MS = 100U; -static const unsigned int RECOVERY_INTERVAL_MS = 250U; -static const unsigned int TRANSFER_TIMEOUT_MS = 250U; -static const unsigned int TRANSFER_ATTEMPTS = 1U; -static const unsigned int MAX_CONSECUTIVE_FAILURES = 3U; +static const unsigned int kPollIntervalMs = 100U; +static const unsigned int kRecoveryIntervalMs = 250U; +static const unsigned int kTransferTimeoutMs = 250U; +static const unsigned int kTransferAttempts = 1U; +static const unsigned int kMaxConsecutiveFailures = 3U; /***************************** Private prototypes *****************************/ @@ -165,13 +165,13 @@ static void pollCaptureState( SAcquisitionLoop *loop, usb::SUsbConnection connection ) { - uint8_t response[EP_BULK_IN_MAX_PACKET_LEN]; + uint8_t response[kEpBulkInMaxPacketLen]; usb::SUsbTransferResult transferResult = { usb::EUsbTransferStatus::eSuccess, 0, "" }; EAcquisitionOperation failedOperation = EAcquisitionOperation::eNone; unsigned int consecutiveFailures = 0U; - unsigned int delayMs = POLL_INTERVAL_MS; + unsigned int delayMs = kPollIntervalMs; while (!loop->stopRequested.load()) { transferResult = executePollingTransaction( @@ -192,7 +192,7 @@ static void pollCaptureState( usb::EUsbTransferStatus::eSuccess ); consecutiveFailures = 0U; - delayMs = POLL_INTERVAL_MS; + delayMs = kPollIntervalMs; } else { loop->status.failedOperation.store(failedOperation); @@ -203,13 +203,13 @@ static void pollCaptureState( loop->status.state.store(EAcquisitionState::eDeviceLost); break; } - else if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { + else if (consecutiveFailures >= kMaxConsecutiveFailures) { loop->status.state.store(EAcquisitionState::eFailed); break; } else { loop->status.state.store(EAcquisitionState::eRecovering); - delayMs = RECOVERY_INTERVAL_MS; + delayMs = kRecoveryIntervalMs; } } @@ -244,8 +244,8 @@ static usb::SUsbTransferResult executePollingTransaction( ) { uint8_t beginCommandPayload[10] = {0x0FU, 0x03U, 0x03U, 0x03U, 0U, 0U, 0U, 0U, 0U, 0U}; - uint8_t speedBuffer[SPEED_RESPONSE_LEN]; - uint8_t captureStateCommand[2] = {CMD_GET_CAPTURE_STATE, 0U}; + uint8_t speedBuffer[kSpeedResponseLen]; + uint8_t captureStateCommand[2] = {kCmdGetCaptureState, 0U}; usb::SUsbTransferResult result = { usb::EUsbTransferStatus::eSuccess, 0, "" }; @@ -253,13 +253,13 @@ static usb::SUsbTransferResult executePollingTransaction( *failedOperation = EAcquisitionOperation::eBeginCommand; result = usb::controlWrite( connection, - CONTROL_BEGINCOMMAND, + kControlBeginCommand, beginCommandPayload, sizeof(beginCommandPayload), 0U, 0U, - TRANSFER_TIMEOUT_MS, - TRANSFER_ATTEMPTS + kTransferTimeoutMs, + kTransferAttempts ); if (result.status == usb::EUsbTransferStatus::eSuccess) { @@ -270,11 +270,11 @@ static usb::SUsbTransferResult executePollingTransaction( *failedOperation = EAcquisitionOperation::eCaptureStateCommand; result = usb::bulkWrite( connection, - EP_BULK_OUT, + kEpBulkOut, captureStateCommand, sizeof(captureStateCommand), - TRANSFER_TIMEOUT_MS, - TRANSFER_ATTEMPTS + kTransferTimeoutMs, + kTransferAttempts ); } if (result.status == usb::EUsbTransferStatus::eSuccess) { @@ -285,11 +285,11 @@ static usb::SUsbTransferResult executePollingTransaction( *failedOperation = EAcquisitionOperation::eCaptureStateResponse; result = usb::bulkRead( connection, - EP_BULK_IN, + kEpBulkIn, response, - EP_BULK_IN_MAX_PACKET_LEN, - TRANSFER_TIMEOUT_MS, - TRANSFER_ATTEMPTS, + kEpBulkInMaxPacketLen, + kTransferTimeoutMs, + kTransferAttempts, 4 ); } @@ -309,13 +309,13 @@ static usb::SUsbTransferResult readConnectionSpeed( ) { return usb::controlRead( connection, - CONTROL_GETSPEED, + kControlGetSpeed, speedBuffer, - SPEED_RESPONSE_LEN, + kSpeedResponseLen, 0U, 0U, - TRANSFER_TIMEOUT_MS, - TRANSFER_ATTEMPTS, + kTransferTimeoutMs, + kTransferAttempts, 1U ); } diff --git a/capture/src/raw_packet_queue.cpp b/capture/src/raw_packet_queue.cpp index 7f1c7bb..7109cb8 100644 --- a/capture/src/raw_packet_queue.cpp +++ b/capture/src/raw_packet_queue.cpp @@ -54,7 +54,7 @@ bool RawPacketQueue::push(const uint8_t *data, const size_t length) { !packets.empty() && (data != NULL) && (length > 0U) && - (length <= RAW_USB_PACKET_MAX_SIZE) + (length <= kRawUsbPacketMaxSize) ) { if (packetCount == packets.size()) { readIndex = (readIndex + 1U) % packets.size(); diff --git a/capture/src/waveform_parser.cpp b/capture/src/waveform_parser.cpp index 5dcb56c..e38d8b3 100644 --- a/capture/src/waveform_parser.cpp +++ b/capture/src/waveform_parser.cpp @@ -72,7 +72,7 @@ bool parseInterleavedWaveformSamples( size_t sampleIndex = 0U; size_t expectedLength = 0U; - if (sampleCount <= WAVEFORM_MAX_SAMPLE_COUNT) { + if (sampleCount <= kWaveformMaxSampleCount) { expectedLength = sampleCount * 2U; if ( (data != NULL) && diff --git a/tests/waveform_parser_test.cpp b/tests/waveform_parser_test.cpp index 630449a..8567645 100644 --- a/tests/waveform_parser_test.cpp +++ b/tests/waveform_parser_test.cpp @@ -82,7 +82,7 @@ static bool testCaptureStateResponse() { /** @fn testInterleavedSamples */ static bool testInterleavedSamples() { - const uint8_t packet[] = {20U, 10U, 21U, 11U, 22U, 12U}; + const uint8_t packet[] = { 20U, 10U, 21U, 11U, 22U, 12U }; SWaveformSamples samples = {}; bool result = true; diff --git a/usb/src/usb_device.cpp b/usb/src/usb_device.cpp index c8d7396..1232854 100644 --- a/usb/src/usb_device.cpp +++ b/usb/src/usb_device.cpp @@ -47,14 +47,14 @@ struct SSupportedDevice { }; /** @brief Delay between scans while waiting for FX2 re-enumeration */ -static const unsigned int FIRMWARE_REENUMERATION_POLL_DELAY_US = 100000U; +static const unsigned int kFirmwareReenumerationPollDelayUs = 100000U; /** @brief Maximum scans while waiting for the operational USB device */ -static const unsigned int FIRMWARE_REENUMERATION_ATTEMPTS = 50U; +static const unsigned int kFirmwareReenumerationAttempts = 50U; /****************************** Module variables ******************************/ /** @brief Maps supported USB VID/PID pairs to model display names */ -static const SSupportedDevice supported_devices[] = { +static const SSupportedDevice kSupportedDevices[] = { /* The bootloader exposes the bulk pair on alt setting 1. */ { 0x04B4U, 0x2250U, 0x04B5U, 0U, 1U, true, @@ -302,11 +302,11 @@ SUsbConnectionResult connectToDevice( for ( reenumerationAttempt = 0U; (reenumerationAttempt < - FIRMWARE_REENUMERATION_ATTEMPTS) && + kFirmwareReenumerationAttempts) && (device == NULL); ++reenumerationAttempt ) { - usleep(FIRMWARE_REENUMERATION_POLL_DELAY_US); + usleep(kFirmwareReenumerationPollDelayUs); deviceCount = libusb_get_device_list( context, &deviceList @@ -659,14 +659,14 @@ static const SSupportedDevice* findSupportedDevice( ) { const SSupportedDevice *result = NULL; const size_t deviceCount = - sizeof(supported_devices) / sizeof(supported_devices[0]); + sizeof(kSupportedDevices) / sizeof(kSupportedDevices[0]); for (size_t index = 0U; index < deviceCount; ++index) { if ( - (supported_devices[index].vendorId == vendorId) && - (supported_devices[index].productId == productId) + (kSupportedDevices[index].vendorId == vendorId) && + (kSupportedDevices[index].productId == productId) ) { - result = &supported_devices[index]; + result = &kSupportedDevices[index]; break; } } From eefc9ecd7ab3dc4835fd429170df51c3222eb3d2 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 18:46:52 +0300 Subject: [PATCH 05/10] Read complete DSO-2250 capture buffers --- HISTORY.md | 13 ++ README.md | 4 +- app/main.cpp | 12 ++ capture/inc/acquisition_loop.h | 6 +- capture/src/acquisition_loop.cpp | 271 +++++++++++++++++++++++++++---- docs/mainpage.md | 3 +- 6 files changed, 272 insertions(+), 37 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 695609e..ee70d99 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -27,6 +27,19 @@ Records key decisions, structural changes, and completed development stages. - 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 + 32,768-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. + ### Code style - Immutable object naming - Reserved `UPPER_CASE` for preprocessor macros. diff --git a/README.md b/README.md index d56d273..8fd3c57 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ The application currently provides: The DSO-2250 capture-state response and its two-channel interleaved sample format are decoded by deterministic, hardware-independent parser functions. -Waveform reads and live waveform rendering are not yet implemented. +After a completed capture, the acquisition worker reads its full 32,768-sample +two-channel buffer in 512-byte USB packets and starts the next capture. Live +waveform rendering is not yet implemented. ## Planned Stack diff --git a/app/main.cpp b/app/main.cpp index 63a1f30..1a5b22e 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -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; diff --git a/capture/inc/acquisition_loop.h b/capture/inc/acquisition_loop.h index 3fe16bf..e8d75c0 100644 --- a/capture/inc/acquisition_loop.h +++ b/capture/inc/acquisition_loop.h @@ -51,7 +51,11 @@ 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 */ diff --git a/capture/src/acquisition_loop.cpp b/capture/src/acquisition_loop.cpp index 5e06438..b8e1873 100644 --- a/capture/src/acquisition_loop.cpp +++ b/capture/src/acquisition_loop.cpp @@ -21,9 +21,11 @@ */ /******************************* Included files *******************************/ +#include #include #include "acquisition_loop.h" +#include "waveform_parser.h" /********************************* Definitions ********************************/ @@ -46,6 +48,22 @@ static const uint16_t kSpeedResponseLen = 10U; /** @brief Protocol command byte for the "get capture state" request */ static const uint8_t kCmdGetCaptureState = 6U; +/** @brief Protocol command byte for the "get channel data" request */ +static const uint8_t kCmdGetChannelData = 5U; +/** @brief Protocol command byte that starts a new capture */ +static const uint8_t kCmdCaptureStart = 3U; +/** @brief Protocol command byte that enables the trigger */ +static const uint8_t kCmdTriggerEnabled = 4U; + +/** @brief Capture state returned when the DSO-2250 buffer is complete */ +static const uint8_t kCaptureSuccess = 2U; +/** @brief Complete two-channel samples read for each DSO-2250 capture */ +static const size_t kCaptureSampleCount = kWaveformMaxSampleCount; +/** @brief Raw bytes in a complete interleaved two-channel capture */ +static const size_t kCaptureDataLength = kCaptureSampleCount * 2U; +/** @brief Bulk packets required to receive a complete capture */ +static const size_t kCapturePacketCount = + kCaptureDataLength / kEpBulkInMaxPacketLen; /** @brief Delay between poll cycles; also the effective host-activity rate */ static const unsigned int kPollIntervalMs = 100U; @@ -85,6 +103,47 @@ static usb::SUsbTransferResult executePollingTransaction( EAcquisitionOperation *failedOperation ); +/** + * @brief Reads a complete DSO-2250 capture buffer after a successful trigger + * @param[in] connection USB connection carrying the capture data + * @param[out] data Destination for the complete interleaved capture + * @param[out] failedOperation First operation that failed + * @returns Result of the first failed operation or the final successful read + */ +static usb::SUsbTransferResult readCaptureData( + const usb::SUsbConnection &connection, + uint8_t *data, + EAcquisitionOperation *failedOperation +); + +/** + * @brief Starts a DSO-2250 capture and enables its trigger + * @param[in] connection USB connection controlling the capture + * @param[out] failedOperation First operation that failed + * @returns Result of the first failed operation or the final successful write + */ +static usb::SUsbTransferResult restartCapture( + const usb::SUsbConnection &connection, + EAcquisitionOperation *failedOperation +); + +/** + * @brief Executes a begin-command, speed-check, and bulk command transaction + * @param[in] connection USB connection for the command + * @param[in] command Command bytes to send + * @param[in] commandLength Number of command bytes + * @param[in] commandOperation Operation to report when the bulk write fails + * @param[out] failedOperation First operation that failed + * @returns Result of the first failed operation or the successful command write + */ +static usb::SUsbTransferResult executeCommand( + const usb::SUsbConnection &connection, + const uint8_t *command, + int commandLength, + EAcquisitionOperation commandOperation, + EAcquisitionOperation *failedOperation +); + /** * @brief Reads the FX2 connection-speed response used to pace the protocol * @param[in] connection USB connection to read from @@ -166,23 +225,63 @@ static void pollCaptureState( usb::SUsbConnection connection ) { uint8_t response[kEpBulkInMaxPacketLen]; + std::array captureData; usb::SUsbTransferResult transferResult = { usb::EUsbTransferStatus::eSuccess, 0, "" }; EAcquisitionOperation failedOperation = EAcquisitionOperation::eNone; + SCaptureStateResponse captureState = {0U, 0U}; unsigned int consecutiveFailures = 0U; unsigned int delayMs = kPollIntervalMs; + bool captureStartRequired = true; while (!loop->stopRequested.load()) { - transferResult = executePollingTransaction( - connection, response, &failedOperation - ); + if (captureStartRequired) { + transferResult = restartCapture(connection, &failedOperation); + if (transferResult.status == usb::EUsbTransferStatus::eSuccess) { + captureStartRequired = false; + } + } + else { + transferResult = executePollingTransaction( + connection, response, &failedOperation + ); + + if ( + (transferResult.status == usb::EUsbTransferStatus::eSuccess) && + parseCaptureStateResponse( + response, + static_cast(transferResult.transferredBytes), + &captureState + ) + ) { + loop->status.lastCaptureState.store( + static_cast(captureState.captureState) + ); + if (captureState.captureState == kCaptureSuccess) { + transferResult = readCaptureData( + connection, + captureData.data(), + &failedOperation + ); + if ( + transferResult.status == + usb::EUsbTransferStatus::eSuccess + ) { + loop->rawPacketQueue.push( + captureData.data(), + captureData.size() + ); + transferResult = restartCapture( + connection, + &failedOperation + ); + } + } + } + } if (transferResult.status == usb::EUsbTransferStatus::eSuccess) { - loop->rawPacketQueue.push( - response, - static_cast(transferResult.transferredBytes) - ); loop->status.droppedPacketCount.store( loop->rawPacketQueue.getDroppedPacketCount() ); @@ -226,11 +325,6 @@ 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]) - ); - } } } @@ -242,55 +336,84 @@ static usb::SUsbTransferResult executePollingTransaction( uint8_t *response, EAcquisitionOperation *failedOperation ) { - uint8_t beginCommandPayload[10] = - {0x0FU, 0x03U, 0x03U, 0x03U, 0U, 0U, 0U, 0U, 0U, 0U}; uint8_t speedBuffer[kSpeedResponseLen]; uint8_t captureStateCommand[2] = {kCmdGetCaptureState, 0U}; usb::SUsbTransferResult result = { usb::EUsbTransferStatus::eSuccess, 0, "" }; - *failedOperation = EAcquisitionOperation::eBeginCommand; - result = usb::controlWrite( + result = executeCommand( connection, - kControlBeginCommand, - beginCommandPayload, - sizeof(beginCommandPayload), - 0U, - 0U, - kTransferTimeoutMs, - kTransferAttempts + captureStateCommand, + sizeof(captureStateCommand), + EAcquisitionOperation::eCaptureStateCommand, + failedOperation ); if (result.status == usb::EUsbTransferStatus::eSuccess) { - *failedOperation = EAcquisitionOperation::eSpeedBeforeCommand; + *failedOperation = EAcquisitionOperation::eSpeedBeforeResponse; result = readConnectionSpeed(connection, speedBuffer); } if (result.status == usb::EUsbTransferStatus::eSuccess) { - *failedOperation = EAcquisitionOperation::eCaptureStateCommand; - result = usb::bulkWrite( + *failedOperation = EAcquisitionOperation::eCaptureStateResponse; + result = usb::bulkRead( connection, - kEpBulkOut, - captureStateCommand, - sizeof(captureStateCommand), + kEpBulkIn, + response, + kEpBulkInMaxPacketLen, kTransferTimeoutMs, - kTransferAttempts + kTransferAttempts, + 4 ); } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = EAcquisitionOperation::eNone; + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn readCaptureData */ +static usb::SUsbTransferResult readCaptureData( + const usb::SUsbConnection &connection, + uint8_t *data, + EAcquisitionOperation *failedOperation +) { + uint8_t channelDataCommand[2] = {kCmdGetChannelData, 0U}; + uint8_t speedBuffer[kSpeedResponseLen]; + usb::SUsbTransferResult result = { + usb::EUsbTransferStatus::eSuccess, 0, "" + }; + size_t packetIndex = 0U; + + result = executeCommand( + connection, + channelDataCommand, + sizeof(channelDataCommand), + EAcquisitionOperation::eChannelDataCommand, + failedOperation + ); if (result.status == usb::EUsbTransferStatus::eSuccess) { *failedOperation = EAcquisitionOperation::eSpeedBeforeResponse; result = readConnectionSpeed(connection, speedBuffer); } - if (result.status == usb::EUsbTransferStatus::eSuccess) { - *failedOperation = EAcquisitionOperation::eCaptureStateResponse; + for ( + packetIndex = 0U; + (packetIndex < kCapturePacketCount) && + (result.status == usb::EUsbTransferStatus::eSuccess); + ++packetIndex + ) { + *failedOperation = EAcquisitionOperation::eChannelDataResponse; result = usb::bulkRead( connection, kEpBulkIn, - response, + data + packetIndex * kEpBulkInMaxPacketLen, kEpBulkInMaxPacketLen, kTransferTimeoutMs, kTransferAttempts, - 4 + kEpBulkInMaxPacketLen ); } if (result.status == usb::EUsbTransferStatus::eSuccess) { @@ -302,6 +425,86 @@ static usb::SUsbTransferResult executePollingTransaction( /*----------------------------------------------------------------------------*/ +/** @fn restartCapture */ +static usb::SUsbTransferResult restartCapture( + const usb::SUsbConnection &connection, + EAcquisitionOperation *failedOperation +) { + const uint8_t captureStartCommand[2] = {kCmdCaptureStart, 0U}; + const uint8_t triggerEnabledCommand[2] = {kCmdTriggerEnabled, 0U}; + usb::SUsbTransferResult result = { + usb::EUsbTransferStatus::eSuccess, 0, "" + }; + + result = executeCommand( + connection, + captureStartCommand, + sizeof(captureStartCommand), + EAcquisitionOperation::eCaptureStartCommand, + failedOperation + ); + if (result.status == usb::EUsbTransferStatus::eSuccess) { + result = executeCommand( + connection, + triggerEnabledCommand, + sizeof(triggerEnabledCommand), + EAcquisitionOperation::eTriggerEnabledCommand, + failedOperation + ); + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn executeCommand */ +static usb::SUsbTransferResult executeCommand( + const usb::SUsbConnection &connection, + const uint8_t *command, + const int commandLength, + const EAcquisitionOperation commandOperation, + EAcquisitionOperation *failedOperation +) { + const uint8_t beginCommandPayload[10] = + {0x0FU, 0x03U, 0x03U, 0x03U, 0U, 0U, 0U, 0U, 0U, 0U}; + uint8_t speedBuffer[kSpeedResponseLen]; + usb::SUsbTransferResult result = { + usb::EUsbTransferStatus::eSuccess, 0, "" + }; + + *failedOperation = EAcquisitionOperation::eBeginCommand; + result = usb::controlWrite( + connection, + kControlBeginCommand, + beginCommandPayload, + sizeof(beginCommandPayload), + 0U, + 0U, + kTransferTimeoutMs, + kTransferAttempts + ); + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = EAcquisitionOperation::eSpeedBeforeCommand; + result = readConnectionSpeed(connection, speedBuffer); + } + if (result.status == usb::EUsbTransferStatus::eSuccess) { + *failedOperation = commandOperation; + result = usb::bulkWrite( + connection, + kEpBulkOut, + command, + commandLength, + kTransferTimeoutMs, + kTransferAttempts + ); + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + /** @fn readConnectionSpeed */ static usb::SUsbTransferResult readConnectionSpeed( const usb::SUsbConnection &connection, diff --git a/docs/mainpage.md b/docs/mainpage.md index 27c995b..7b3b912 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -28,6 +28,7 @@ libusb for communication with supported instruments. - capture-state polling through vendor control requests and bulk endpoints; - bounded USB error recovery and safe device-loss handling. - deterministic DSO-2250 capture-state and two-channel sample parsing. +- complete 32,768-sample two-channel capture reads after a successful trigger. ## Architecture @@ -77,7 +78,7 @@ complete installation, build, and run instructions. ## Planned work -The next tasks are reading waveform data and displaying live waveforms. +The next tasks are publishing decoded captures and displaying live waveforms. Instrument controls, broader recovery behavior, and persistent configuration follow in later milestones. From f0c834e58c13722b4cb2c54f17e775800b7e2f53 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 18:48:46 +0300 Subject: [PATCH 06/10] Published decoded waveform frames --- HISTORY.md | 11 +++++++ README.md | 3 +- capture/inc/acquisition_loop.h | 19 ++++++++++++ capture/inc/raw_packet_queue.h | 10 +++++-- capture/src/acquisition_loop.cpp | 50 +++++++++++++++++++++++++++++++- capture/src/raw_packet_queue.cpp | 9 ++++-- docs/mainpage.md | 4 +-- tests/raw_packet_queue_test.cpp | 15 ++++++++-- 8 files changed, 110 insertions(+), 11 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index ee70d99..4a82de3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -40,6 +40,17 @@ Records key decisions, structural changes, and completed development stages. - 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. + ### Code style - Immutable object naming - Reserved `UPPER_CASE` for preprocessor macros. diff --git a/README.md b/README.md index 8fd3c57..21f07fa 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ The DSO-2250 capture-state response and its two-channel interleaved sample format are decoded by deterministic, hardware-independent parser functions. After a completed capture, the acquisition worker reads its full 32,768-sample two-channel buffer in 512-byte USB packets and starts the next capture. Live -waveform rendering is not yet implemented. +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 diff --git a/capture/inc/acquisition_loop.h b/capture/inc/acquisition_loop.h index e8d75c0..83c3fb8 100644 --- a/capture/inc/acquisition_loop.h +++ b/capture/inc/acquisition_loop.h @@ -25,10 +25,12 @@ /******************************* Included files ******************************/ #include +#include #include #include "raw_packet_queue.h" #include "usb_device.h" +#include "waveform_parser.h" /********************************* Definitions ********************************/ @@ -80,6 +82,10 @@ struct SAcquisitionLoop { RawPacketQueue rawPacketQueue{8U}; /**< Bounded raw response FIFO */ std::atomic stopRequested{false}; /**< Set to request a stop */ SAcquisitionStatus status; /**< Shared poll status */ + mutable std::mutex waveformMutex; /**< Guards the latest waveform */ + SWaveformSamples latestWaveform{}; /**< Most recently decoded frame */ + uint32_t latestTriggerPoint{0U}; /**< Trigger point for latest frame */ + bool hasWaveform{false}; /**< True after first decoded frame */ }; /********************* Application Programming Interface *********************/ @@ -110,6 +116,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 /******************************************************************************/ diff --git a/capture/inc/raw_packet_queue.h b/capture/inc/raw_packet_queue.h index bae179f..a3ba7ea 100644 --- a/capture/inc/raw_packet_queue.h +++ b/capture/inc/raw_packet_queue.h @@ -42,7 +42,8 @@ static const size_t kRawUsbPacketMaxSize = 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 */ + size_t validLength; /**< Number of valid bytes in payload */ + uint32_t triggerPoint; /**< Trigger position associated with payload */ }; /** @brief Provides a bounded thread-safe FIFO for raw USB responses */ @@ -58,10 +59,15 @@ class RawPacketQueue { * @brief Copies a raw response into the queue without blocking * @param[in] data Raw response bytes * @param[in] length Number of bytes to copy + * @param[in] triggerPoint Trigger position associated with the response * @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); + bool push( + const uint8_t *data, + size_t length, + uint32_t triggerPoint = 0U + ); /** * @brief Waits for and removes the oldest retained packet diff --git a/capture/src/acquisition_loop.cpp b/capture/src/acquisition_loop.cpp index b8e1873..08b3c57 100644 --- a/capture/src/acquisition_loop.cpp +++ b/capture/src/acquisition_loop.cpp @@ -172,6 +172,13 @@ void startAcquisitionLoop( loop->status.lastTransferStatus.store( usb::EUsbTransferStatus::eSuccess ); + { + std::lock_guard lock(loop->waveformMutex); + + loop->latestWaveform = {}; + loop->latestTriggerPoint = 0U; + loop->hasWaveform = false; + } loop->processingThread = std::thread(processRawPackets, loop); loop->workerThread = std::thread(pollCaptureState, loop, connection); } @@ -202,6 +209,31 @@ bool joinFinishedAcquisitionLoop(SAcquisitionLoop *loop) { return joined; } +/*----------------------------------------------------------------------------*/ + +/** @fn getLatestWaveform */ +bool getLatestWaveform( + const SAcquisitionLoop *loop, + SWaveformSamples *waveform, + uint32_t *triggerPoint +) { + bool result = false; + + if ((loop != NULL) && (waveform != NULL) && (triggerPoint != NULL)) { + std::lock_guard lock(loop->waveformMutex); + + if (loop->hasWaveform) { + *waveform = loop->latestWaveform; + *triggerPoint = loop->latestTriggerPoint; + result = true; + } + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + /** @fn stopAcquisitionLoop */ void stopAcquisitionLoop(SAcquisitionLoop *loop) { if (loop != NULL) { @@ -270,7 +302,8 @@ static void pollCaptureState( ) { loop->rawPacketQueue.push( captureData.data(), - captureData.size() + captureData.size(), + captureState.triggerPoint ); transferResult = restartCapture( connection, @@ -323,8 +356,23 @@ static void pollCaptureState( /** @fn processRawPackets */ static void processRawPackets(SAcquisitionLoop *loop) { SRawUsbPacket packet; + SWaveformSamples waveform; while (loop->rawPacketQueue.waitPop(&packet)) { + if ( + parseInterleavedWaveformSamples( + packet.payload.data(), + packet.validLength, + kCaptureSampleCount, + &waveform + ) + ) { + std::lock_guard lock(loop->waveformMutex); + + loop->latestWaveform = waveform; + loop->latestTriggerPoint = packet.triggerPoint; + loop->hasWaveform = true; + } } } diff --git a/capture/src/raw_packet_queue.cpp b/capture/src/raw_packet_queue.cpp index 7109cb8..78f670f 100644 --- a/capture/src/raw_packet_queue.cpp +++ b/capture/src/raw_packet_queue.cpp @@ -44,8 +44,12 @@ RawPacketQueue::RawPacketQueue(const size_t capacity) : /*----------------------------------------------------------------------------*/ -/** @fn bool RawPacketQueue::push(const uint8_t *data, size_t length) */ -bool RawPacketQueue::push(const uint8_t *data, const size_t length) { +/** @fn bool RawPacketQueue::push(const uint8_t *data, size_t length, uint32_t) */ +bool RawPacketQueue::push( + const uint8_t *data, + const size_t length, + const uint32_t triggerPoint +) { bool accepted = false; std::lock_guard lock(mutex); @@ -64,6 +68,7 @@ bool RawPacketQueue::push(const uint8_t *data, const size_t length) { std::copy(data, data + length, packets[writeIndex].payload.begin()); packets[writeIndex].validLength = length; + packets[writeIndex].triggerPoint = triggerPoint; writeIndex = (writeIndex + 1U) % packets.size(); ++packetCount; accepted = true; diff --git a/docs/mainpage.md b/docs/mainpage.md index 7b3b912..0a8423e 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -29,6 +29,7 @@ libusb for communication with supported instruments. - bounded USB error recovery and safe device-loss handling. - deterministic DSO-2250 capture-state and two-channel sample parsing. - complete 32,768-sample two-channel capture reads after a successful trigger. +- thread-safe publication of the latest decoded waveform and trigger point. ## Architecture @@ -78,7 +79,6 @@ complete installation, build, and run instructions. ## Planned work -The next tasks are publishing decoded captures and displaying live waveforms. -Instrument controls, broader +The next task is 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 index e3349cc..a721409 100644 --- a/tests/raw_packet_queue_test.cpp +++ b/tests/raw_packet_queue_test.cpp @@ -66,13 +66,20 @@ static bool testFifoAndLength() { SRawUsbPacket packet; bool result = true; - result = expect(queue.push(first, sizeof(first)), "push first packet") && + result = expect( + queue.push(first, sizeof(first), 101U), + "push first packet" + ) && result; - result = expect(queue.push(second, sizeof(second)), "push second packet") - && result; + result = expect( + queue.push(second, sizeof(second), 202U), + "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.triggerPoint == 101U, "first trigger point") && + result; result = expect( (packet.payload[0] == first[0]) && (packet.payload[1] == first[1]), @@ -81,6 +88,8 @@ static bool testFifoAndLength() { result = expect(queue.waitPop(&packet), "pop second packet") && result; result = expect(packet.validLength == sizeof(second), "second length") && result; + result = expect(packet.triggerPoint == 202U, "second trigger point") && + result; result = expect( (packet.payload[0] == second[0]) && (packet.payload[1] == second[1]) && From 2761a1e511062d00340e3212e61c0061f285e36a Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 19:04:45 +0300 Subject: [PATCH 07/10] Added device capture protocol profiles --- HISTORY.md | 9 +++ README.md | 2 + app/main.cpp | 4 +- capture/inc/acquisition_loop.h | 5 +- capture/inc/waveform_parser.h | 13 ++-- capture/src/acquisition_loop.cpp | 110 +++++++++++++++++-------------- capture/src/waveform_parser.cpp | 24 +++++-- docs/mainpage.md | 1 + tests/waveform_parser_test.cpp | 8 ++- usb/inc/firmware_loader.h | 2 +- usb/inc/usb_device.h | 22 ++++++- usb/src/firmware_loader.cpp | 2 +- usb/src/usb_device.cpp | 34 ++++++---- 13 files changed, 151 insertions(+), 85 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 4a82de3..7a7b295 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -51,6 +51,15 @@ Records key decisions, structural changes, and completed development stages. - 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. + ### Code style - Immutable object naming - Reserved `UPPER_CASE` for preprocessor macros. diff --git a/README.md b/README.md index 21f07fa..5fd298e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ After a completed capture, the acquisition worker reads its full 32,768-sample two-channel buffer in 512-byte USB packets and 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. +Each supported-device entry supplies its capture protocol, including endpoints, +packet size, commands, channel layout, sample count, and completion state. ## Planned Stack diff --git a/app/main.cpp b/app/main.cpp index 1a5b22e..d4fedb6 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -101,7 +101,7 @@ using oscilloscope::usb::SUsbScanResult; static const uint32_t kUsbPresenceIntervalMs = 1000U; /** @brief Cleared device identity used to reset connection bookkeeping */ -static const SUsbDeviceInfo kEmptyDeviceInfo = {0U, 0U, 0U, 0U, NULL}; +static const SUsbDeviceInfo kEmptyDeviceInfo = {NULL, 0U, 0U, 0U, 0U}; #ifdef __GNUC__ // GCC/MinGW only const char kVersionInfo[] __attribute__((section(".version"), used)) = @@ -313,7 +313,7 @@ int main (void) { 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 = kEmptyDeviceInfo; std::string deviceStatus = formatUsbConnectionStatus( diff --git a/capture/inc/acquisition_loop.h b/capture/inc/acquisition_loop.h index 83c3fb8..9f11521 100644 --- a/capture/inc/acquisition_loop.h +++ b/capture/inc/acquisition_loop.h @@ -62,8 +62,8 @@ 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 lastCaptureState{-1}; /**< Last raw capture state */ std::atomic state{ EAcquisitionState::eStopped }; /**< Current worker state */ @@ -80,11 +80,12 @@ struct SAcquisitionLoop { 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 */ + 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 stopRequested{false}; /**< Set to request a stop */ bool hasWaveform{false}; /**< True after first decoded frame */ }; diff --git a/capture/inc/waveform_parser.h b/capture/inc/waveform_parser.h index 6022b3b..b4330ea 100644 --- a/capture/inc/waveform_parser.h +++ b/capture/inc/waveform_parser.h @@ -38,8 +38,8 @@ static const size_t kWaveformMaxSampleCount = 32768U; /** @brief Decoded capture state and normalized trigger sample position */ struct SCaptureStateResponse { - uint8_t captureState; /**< Device capture state byte */ uint32_t triggerPoint; /**< Trigger position in the sample ring */ + uint8_t captureState; /**< Device capture state byte */ }; /** @brief One decoded two-channel DSO-2250 waveform capture */ @@ -52,7 +52,7 @@ struct SWaveformSamples { /********************* Application Programming Interface *********************/ /** - * @brief Decodes the DSO-2250 capture-state response packet + * @brief Decodes a capture-state response packet * @param[in] data Raw USB response data * @param[in] length Number of bytes in data * @param[out] response Destination for decoded state and trigger position @@ -65,18 +65,21 @@ bool parseCaptureStateResponse( ); /** - * @brief Decodes an interleaved two-channel DSO-2250 sample buffer + * @brief Decodes an interleaved waveform sample buffer * @param[in] data Raw USB capture data * @param[in] length Number of bytes in data * @param[in] sampleCount Expected samples in each channel + * @param[in] channelCount Number of interleaved channels + * @param[in] channelOneSecond True when each pair is CH2 then CH1 * @param[out] samples Destination for decoded channel samples - * @returns True when the complete expected two-channel buffer was decoded - * @note The legacy protocol orders each pair as CH2 then CH1 + * @returns True when the complete expected buffer was decoded */ bool parseInterleavedWaveformSamples( const uint8_t *data, size_t length, size_t sampleCount, + uint8_t channelCount, + bool channelOneSecond, SWaveformSamples *samples ); diff --git a/capture/src/acquisition_loop.cpp b/capture/src/acquisition_loop.cpp index 08b3c57..b121d2b 100644 --- a/capture/src/acquisition_loop.cpp +++ b/capture/src/acquisition_loop.cpp @@ -34,11 +34,6 @@ namespace capture { /****************************** Module variables ******************************/ -/** @brief Hantek DSO-2250 bulk endpoint addresses */ -static const uint8_t kEpBulkOut = 0x02U; -static const uint8_t kEpBulkIn = 0x86U; -static const uint16_t kEpBulkInMaxPacketLen = 512U; - /** @brief Vendor control requests used by the polling cycle */ static const uint8_t kControlGetSpeed = 0xB2U; static const uint8_t kControlBeginCommand = 0xB3U; @@ -46,25 +41,6 @@ static const uint8_t kControlBeginCommand = 0xB3U; /** @brief Length of the FX2 connection-speed control response */ static const uint16_t kSpeedResponseLen = 10U; -/** @brief Protocol command byte for the "get capture state" request */ -static const uint8_t kCmdGetCaptureState = 6U; -/** @brief Protocol command byte for the "get channel data" request */ -static const uint8_t kCmdGetChannelData = 5U; -/** @brief Protocol command byte that starts a new capture */ -static const uint8_t kCmdCaptureStart = 3U; -/** @brief Protocol command byte that enables the trigger */ -static const uint8_t kCmdTriggerEnabled = 4U; - -/** @brief Capture state returned when the DSO-2250 buffer is complete */ -static const uint8_t kCaptureSuccess = 2U; -/** @brief Complete two-channel samples read for each DSO-2250 capture */ -static const size_t kCaptureSampleCount = kWaveformMaxSampleCount; -/** @brief Raw bytes in a complete interleaved two-channel capture */ -static const size_t kCaptureDataLength = kCaptureSampleCount * 2U; -/** @brief Bulk packets required to receive a complete capture */ -static const size_t kCapturePacketCount = - kCaptureDataLength / kEpBulkInMaxPacketLen; - /** @brief Delay between poll cycles; also the effective host-activity rate */ static const unsigned int kPollIntervalMs = 100U; static const unsigned int kRecoveryIntervalMs = 250U; @@ -104,7 +80,7 @@ static usb::SUsbTransferResult executePollingTransaction( ); /** - * @brief Reads a complete DSO-2250 capture buffer after a successful trigger + * @brief Reads a complete capture buffer after a successful trigger * @param[in] connection USB connection carrying the capture data * @param[out] data Destination for the complete interleaved capture * @param[out] failedOperation First operation that failed @@ -117,7 +93,7 @@ static usb::SUsbTransferResult readCaptureData( ); /** - * @brief Starts a DSO-2250 capture and enables its trigger + * @brief Starts a capture and enables its trigger * @param[in] connection USB connection controlling the capture * @param[out] failedOperation First operation that failed * @returns Result of the first failed operation or the final successful write @@ -165,6 +141,7 @@ void startAcquisitionLoop( if (loop != NULL) { loop->stopRequested.store(false); loop->rawPacketQueue.reset(); + loop->captureProtocol = connection.captureProtocol; loop->status.lastCaptureState.store(-1); loop->status.droppedPacketCount.store(0U); loop->status.state.store(EAcquisitionState::eRunning); @@ -256,13 +233,16 @@ static void pollCaptureState( SAcquisitionLoop *loop, usb::SUsbConnection connection ) { - uint8_t response[kEpBulkInMaxPacketLen]; - std::array captureData; + uint8_t response[kRawUsbPacketMaxSize]; + std::array captureData; usb::SUsbTransferResult transferResult = { usb::EUsbTransferStatus::eSuccess, 0, "" }; EAcquisitionOperation failedOperation = EAcquisitionOperation::eNone; SCaptureStateResponse captureState = {0U, 0U}; + const size_t captureDataLength = + connection.captureProtocol.sampleCount * + connection.captureProtocol.channelCount; unsigned int consecutiveFailures = 0U; unsigned int delayMs = kPollIntervalMs; bool captureStartRequired = true; @@ -290,7 +270,10 @@ static void pollCaptureState( loop->status.lastCaptureState.store( static_cast(captureState.captureState) ); - if (captureState.captureState == kCaptureSuccess) { + if ( + captureState.captureState == + connection.captureProtocol.captureCompleteState + ) { transferResult = readCaptureData( connection, captureData.data(), @@ -302,7 +285,7 @@ static void pollCaptureState( ) { loop->rawPacketQueue.push( captureData.data(), - captureData.size(), + captureDataLength, captureState.triggerPoint ); transferResult = restartCapture( @@ -363,7 +346,9 @@ static void processRawPackets(SAcquisitionLoop *loop) { parseInterleavedWaveformSamples( packet.payload.data(), packet.validLength, - kCaptureSampleCount, + loop->captureProtocol.sampleCount, + loop->captureProtocol.channelCount, + loop->captureProtocol.channelOneSecond, &waveform ) ) { @@ -385,7 +370,9 @@ static usb::SUsbTransferResult executePollingTransaction( EAcquisitionOperation *failedOperation ) { uint8_t speedBuffer[kSpeedResponseLen]; - uint8_t captureStateCommand[2] = {kCmdGetCaptureState, 0U}; + uint8_t captureStateCommand[2] = { + connection.captureProtocol.captureStateCommand, 0U + }; usb::SUsbTransferResult result = { usb::EUsbTransferStatus::eSuccess, 0, "" }; @@ -406,9 +393,9 @@ static usb::SUsbTransferResult executePollingTransaction( *failedOperation = EAcquisitionOperation::eCaptureStateResponse; result = usb::bulkRead( connection, - kEpBulkIn, + connection.captureProtocol.bulkInEndpoint, response, - kEpBulkInMaxPacketLen, + connection.captureProtocol.bulkInPacketLength, kTransferTimeoutMs, kTransferAttempts, 4 @@ -429,39 +416,58 @@ static usb::SUsbTransferResult readCaptureData( uint8_t *data, EAcquisitionOperation *failedOperation ) { - uint8_t channelDataCommand[2] = {kCmdGetChannelData, 0U}; + uint8_t channelDataCommand[2] = { + connection.captureProtocol.channelDataCommand, 0U + }; uint8_t speedBuffer[kSpeedResponseLen]; usb::SUsbTransferResult result = { usb::EUsbTransferStatus::eSuccess, 0, "" }; + const size_t captureDataLength = + connection.captureProtocol.sampleCount * + connection.captureProtocol.channelCount; + size_t capturePacketCount = 0U; size_t packetIndex = 0U; - result = executeCommand( - connection, - channelDataCommand, - sizeof(channelDataCommand), - EAcquisitionOperation::eChannelDataCommand, - failedOperation - ); + if ( + (connection.captureProtocol.bulkInPacketLength == 0U) || + (captureDataLength > kRawUsbPacketMaxSize) || + ((captureDataLength % connection.captureProtocol.bulkInPacketLength) != + 0U) + ) { + result.status = usb::EUsbTransferStatus::eError; + result.errorMessage = "Invalid capture protocol"; + } + else { + capturePacketCount = + captureDataLength / connection.captureProtocol.bulkInPacketLength; + result = executeCommand( + connection, + channelDataCommand, + sizeof(channelDataCommand), + EAcquisitionOperation::eChannelDataCommand, + failedOperation + ); + } if (result.status == usb::EUsbTransferStatus::eSuccess) { *failedOperation = EAcquisitionOperation::eSpeedBeforeResponse; result = readConnectionSpeed(connection, speedBuffer); } for ( packetIndex = 0U; - (packetIndex < kCapturePacketCount) && + (packetIndex < capturePacketCount) && (result.status == usb::EUsbTransferStatus::eSuccess); ++packetIndex ) { *failedOperation = EAcquisitionOperation::eChannelDataResponse; result = usb::bulkRead( connection, - kEpBulkIn, - data + packetIndex * kEpBulkInMaxPacketLen, - kEpBulkInMaxPacketLen, + connection.captureProtocol.bulkInEndpoint, + data + packetIndex * connection.captureProtocol.bulkInPacketLength, + connection.captureProtocol.bulkInPacketLength, kTransferTimeoutMs, kTransferAttempts, - kEpBulkInMaxPacketLen + connection.captureProtocol.bulkInPacketLength ); } if (result.status == usb::EUsbTransferStatus::eSuccess) { @@ -478,8 +484,12 @@ static usb::SUsbTransferResult restartCapture( const usb::SUsbConnection &connection, EAcquisitionOperation *failedOperation ) { - const uint8_t captureStartCommand[2] = {kCmdCaptureStart, 0U}; - const uint8_t triggerEnabledCommand[2] = {kCmdTriggerEnabled, 0U}; + const uint8_t captureStartCommand[2] = { + connection.captureProtocol.captureStartCommand, 0U + }; + const uint8_t triggerEnabledCommand[2] = { + connection.captureProtocol.triggerEnabledCommand, 0U + }; usb::SUsbTransferResult result = { usb::EUsbTransferStatus::eSuccess, 0, "" }; @@ -540,7 +550,7 @@ static usb::SUsbTransferResult executeCommand( *failedOperation = commandOperation; result = usb::bulkWrite( connection, - kEpBulkOut, + connection.captureProtocol.bulkOutEndpoint, command, commandLength, kTransferTimeoutMs, diff --git a/capture/src/waveform_parser.cpp b/capture/src/waveform_parser.cpp index e38d8b3..fd16b61 100644 --- a/capture/src/waveform_parser.cpp +++ b/capture/src/waveform_parser.cpp @@ -66,22 +66,38 @@ bool parseInterleavedWaveformSamples( const uint8_t *data, size_t length, size_t sampleCount, + uint8_t channelCount, + bool channelOneSecond, SWaveformSamples *samples ) { bool result = false; size_t sampleIndex = 0U; size_t expectedLength = 0U; - if (sampleCount <= kWaveformMaxSampleCount) { - expectedLength = sampleCount * 2U; + if ( + (sampleCount <= kWaveformMaxSampleCount) && + ((channelCount == 1U) || (channelCount == 2U)) + ) { + expectedLength = sampleCount * channelCount; if ( (data != NULL) && (samples != NULL) && (length == expectedLength) ) { for (sampleIndex = 0U; sampleIndex < sampleCount; ++sampleIndex) { - samples->channelTwo[sampleIndex] = data[sampleIndex * 2U]; - samples->channelOne[sampleIndex] = data[sampleIndex * 2U + 1U]; + if (channelCount == 1U) { + samples->channelOne[sampleIndex] = data[sampleIndex]; + } + else if (channelOneSecond) { + samples->channelTwo[sampleIndex] = data[sampleIndex * 2U]; + samples->channelOne[sampleIndex] = + data[sampleIndex * 2U + 1U]; + } + else { + samples->channelOne[sampleIndex] = data[sampleIndex * 2U]; + samples->channelTwo[sampleIndex] = + data[sampleIndex * 2U + 1U]; + } } samples->sampleCount = sampleCount; result = true; diff --git a/docs/mainpage.md b/docs/mainpage.md index 0a8423e..7676872 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -30,6 +30,7 @@ libusb for communication with supported instruments. - deterministic DSO-2250 capture-state and two-channel sample parsing. - complete 32,768-sample two-channel capture reads after a successful trigger. - thread-safe publication of the latest decoded waveform and trigger point. +- a per-device capture profile selected from the supported-device table. ## Architecture diff --git a/tests/waveform_parser_test.cpp b/tests/waveform_parser_test.cpp index 8567645..9fb13cf 100644 --- a/tests/waveform_parser_test.cpp +++ b/tests/waveform_parser_test.cpp @@ -88,7 +88,7 @@ static bool testInterleavedSamples() { result = expect( parseInterleavedWaveformSamples( - packet, sizeof(packet), 3U, &samples + packet, sizeof(packet), 3U, 2U, true, &samples ), "parse interleaved samples" ) && result; @@ -120,14 +120,16 @@ static bool testInvalidSampleBuffers() { samples.sampleCount = 99U; result = expect( !parseInterleavedWaveformSamples( - packet, sizeof(packet) - 1U, 2U, &samples + packet, sizeof(packet) - 1U, 2U, 2U, true, &samples ), "reject short sample buffer" ) && result; result = expect(samples.sampleCount == 99U, "preserve rejected samples") && result; result = expect( - !parseInterleavedWaveformSamples(packet, sizeof(packet), 32769U, &samples), + !parseInterleavedWaveformSamples( + packet, sizeof(packet), 32769U, 2U, true, &samples + ), "reject oversized sample count" ) && result; diff --git a/usb/inc/firmware_loader.h b/usb/inc/firmware_loader.h index 446e9c2..7e9eb63 100644 --- a/usb/inc/firmware_loader.h +++ b/usb/inc/firmware_loader.h @@ -48,8 +48,8 @@ struct SFirmwarePaths { /** @brief Holds the result of a firmware upload attempt */ struct SFirmwareLoadResult { - EFirmwareLoadStatus status; /**< Upload outcome */ std::string errorMessage; /**< Details when applicable */ + EFirmwareLoadStatus status; /**< Upload outcome */ }; /********************* Application Programming Interface **********************/ diff --git a/usb/inc/usb_device.h b/usb/inc/usb_device.h index d8fee62..8425206 100644 --- a/usb/inc/usb_device.h +++ b/usb/inc/usb_device.h @@ -57,32 +57,48 @@ enum class EConnectionStatus { /** @brief Identifies one supported USB oscilloscope instance */ struct SUsbDeviceInfo { + const char *modelName; /**< Supported model display name */ uint16_t vendorId; /**< USB vendor identifier */ uint16_t productId; /**< USB product identifier */ uint8_t busNumber; /**< USB bus number */ uint8_t deviceAddress; /**< Address assigned on the USB bus */ - const char *modelName; /**< Supported model display name */ }; /** @brief Holds the outcome and matching devices from a USB scan */ struct SUsbScanResult { - EScanStatus status; /**< Overall scan status */ std::vector devices; /**< All supported devices found */ std::string errorMessage; /**< libusb error for a failed scan */ + EScanStatus status; /**< Overall scan status */ +}; + +/** @brief Describes the capture protocol for one supported device family */ +struct SUsbCaptureProtocol { + size_t sampleCount; /**< Samples in each enabled channel */ + uint16_t bulkInPacketLength; /**< Bulk IN packet size in bytes */ + uint8_t bulkOutEndpoint; /**< Bulk OUT endpoint address */ + uint8_t bulkInEndpoint; /**< Bulk IN endpoint address */ + uint8_t channelCount; /**< Interleaved analog channel count */ + bool channelOneSecond; /**< True when each pair is CH2 then CH1 */ + uint8_t captureCompleteState; /**< State value indicating a full buffer */ + uint8_t captureStateCommand; /**< Command byte that reads capture state */ + uint8_t channelDataCommand; /**< Command byte that reads sample data */ + uint8_t captureStartCommand; /**< Command byte that starts capture */ + uint8_t triggerEnabledCommand; /**< Command byte that enables trigger */ }; /** @brief Describes an active USB connection to a supported device */ struct SUsbConnection { libusb_context *context; /**< libusb connection context */ libusb_device_handle *handle; /**< Open USB device handle */ + SUsbCaptureProtocol captureProtocol; /**< Protocol selected for device */ uint8_t interfaceNumber; /**< Claimed interface number */ bool isConnected; /**< True once the interface is claimed */ }; /** @brief Holds the result of a connection lifecycle operation */ struct SUsbConnectionResult { - EConnectionStatus status; /**< Operation outcome */ std::string errorMessage; /**< libusb error string when applicable */ + EConnectionStatus status; /**< Operation outcome */ }; /** @brief Describes the outcome of a bulk endpoint transfer */ diff --git a/usb/src/firmware_loader.cpp b/usb/src/firmware_loader.cpp index 82dd3b0..d20e31b 100644 --- a/usb/src/firmware_loader.cpp +++ b/usb/src/firmware_loader.cpp @@ -41,7 +41,7 @@ SFirmwareLoadResult loadFx2Firmware( const uint8_t deviceAddress, const SFirmwarePaths &paths ) { - SFirmwareLoadResult result = {EFirmwareLoadStatus::eLoaded, ""}; + SFirmwareLoadResult result = {"", EFirmwareLoadStatus::eLoaded}; char devicePath[32]; pid_t childPid = -1; int childStatus = 0; diff --git a/usb/src/usb_device.cpp b/usb/src/usb_device.cpp index 1232854..d195718 100644 --- a/usb/src/usb_device.cpp +++ b/usb/src/usb_device.cpp @@ -36,14 +36,15 @@ namespace usb { /** @brief Defines a supported model signature */ struct SSupportedDevice { + const char *modelName; + const char *firmwareBaseName; + SUsbCaptureProtocol captureProtocol; 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 */ @@ -57,12 +58,14 @@ static const unsigned int kFirmwareReenumerationAttempts = 50U; static const SSupportedDevice kSupportedDevices[] = { /* The bootloader exposes the bulk pair on alt setting 1. */ { - 0x04B4U, 0x2250U, 0x04B5U, 0U, 1U, true, - "Hantek DSO-2250 Bootloader", "DSO2250" + "Hantek DSO-2250 Bootloader", "DSO2250", + {32768U, 512U, 0x02U, 0x86U, 2U, true, 2U, 6U, 5U, 3U, 4U}, + 0x04B4U, 0x2250U, 0x04B5U, 0U, 1U, true }, { - 0x04B5U, 0x2250U, 0x04B5U, 0U, 0U, false, - "Hantek DSO-2250", "DSO2250" + "Hantek DSO-2250", "DSO2250", + {32768U, 512U, 0x02U, 0x86U, 2U, true, 2U, 6U, 5U, 3U, 4U}, + 0x04B5U, 0x2250U, 0x04B5U, 0U, 0U, false } }; @@ -172,7 +175,7 @@ static SUsbTransferResult makeTransferResult( SUsbScanResult enumerateSupportedDevices() { libusb_context *context = NULL; libusb_device **deviceList = NULL; - SUsbScanResult result = {EScanStatus::eSuccess, {}, ""}; + SUsbScanResult result = {{}, "", EScanStatus::eSuccess}; const int initializationResult = libusb_init(&context); if (initializationResult != LIBUSB_SUCCESS) { @@ -202,11 +205,11 @@ SUsbScanResult enumerateSupportedDevices() { if (supportedDevice != NULL) { result.devices.push_back({ + supportedDevice->modelName, descriptor.idVendor, descriptor.idProduct, libusb_get_bus_number(deviceList[index]), - libusb_get_device_address(deviceList[index]), - supportedDevice->modelName + libusb_get_device_address(deviceList[index]) }); } } @@ -228,13 +231,13 @@ SUsbConnectionResult connectToDevice( const SUsbDeviceInfo &deviceInfo, SUsbConnection *connection ) { - SUsbConnectionResult result = {EConnectionStatus::eDisconnected, ""}; + 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, ""}; + SFirmwareLoadResult firmwareResult = {"", EFirmwareLoadStatus::eLoaded}; ssize_t deviceCount = 0; int initializationResult = LIBUSB_SUCCESS; int openResult = LIBUSB_SUCCESS; @@ -390,6 +393,8 @@ SUsbConnectionResult connectToDevice( connection->interfaceNumber = supportedDevice->interfaceNumber; connection->isConnected = true; + connection->captureProtocol = + supportedDevice->captureProtocol; result.status = EConnectionStatus::eConnected; context = NULL; handle = NULL; @@ -417,7 +422,7 @@ SUsbConnectionResult connectToDevice( /** @fn disconnectFromDevice */ SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection) { - SUsbConnectionResult result = {EConnectionStatus::eDisconnected, ""}; + SUsbConnectionResult result = {"", EConnectionStatus::eDisconnected}; int releaseResult = LIBUSB_SUCCESS; if (connection == NULL) { @@ -451,6 +456,7 @@ SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection) { connection->handle = NULL; connection->interfaceNumber = 0U; connection->isConnected = false; + connection->captureProtocol = {}; } return result; @@ -482,11 +488,11 @@ bool getConnectedDeviceInfo( ); if (supportedDevice != NULL) { *deviceInfo = { + supportedDevice->modelName, descriptor.idVendor, descriptor.idProduct, libusb_get_bus_number(device), - libusb_get_device_address(device), - supportedDevice->modelName + libusb_get_device_address(device) }; result = true; } From 727f29caf0b476d84876c9fe812ea78cf413afa7 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 19:09:10 +0300 Subject: [PATCH 08/10] Corrected numeric formatting --- HISTORY.md | 2 +- README.md | 2 +- app/main.cpp | 2 +- docs/mainpage.md | 2 +- usb/src/usb_device.cpp | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 7a7b295..fb4f50f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -32,7 +32,7 @@ Records key decisions, structural changes, and completed development stages. - 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 - 32,768-sample buffer as 128 consecutive 512-byte bulk packets. + 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 diff --git a/README.md b/README.md index 5fd298e..4588f01 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The application currently provides: The DSO-2250 capture-state response and its two-channel interleaved sample format are decoded by deterministic, hardware-independent parser functions. -After a completed capture, the acquisition worker reads its full 32,768-sample +After a completed capture, the acquisition worker reads its full 32768-sample two-channel buffer in 512-byte USB packets and 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. diff --git a/app/main.cpp b/app/main.cpp index d4fedb6..87e6509 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -101,7 +101,7 @@ using oscilloscope::usb::SUsbScanResult; static const uint32_t kUsbPresenceIntervalMs = 1000U; /** @brief Cleared device identity used to reset connection bookkeeping */ -static const SUsbDeviceInfo kEmptyDeviceInfo = {NULL, 0U, 0U, 0U, 0U}; +static const SUsbDeviceInfo kEmptyDeviceInfo = { NULL, 0U, 0U, 0U, 0U }; #ifdef __GNUC__ // GCC/MinGW only const char kVersionInfo[] __attribute__((section(".version"), used)) = diff --git a/docs/mainpage.md b/docs/mainpage.md index 7676872..21296ff 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -28,7 +28,7 @@ libusb for communication with supported instruments. - capture-state polling through vendor control requests and bulk endpoints; - bounded USB error recovery and safe device-loss handling. - deterministic DSO-2250 capture-state and two-channel sample parsing. -- complete 32,768-sample two-channel capture reads after a successful trigger. +- complete 32768-sample two-channel capture reads after a successful trigger. - thread-safe publication of the latest decoded waveform and trigger point. - a per-device capture profile selected from the supported-device table. diff --git a/usb/src/usb_device.cpp b/usb/src/usb_device.cpp index d195718..2e8d50d 100644 --- a/usb/src/usb_device.cpp +++ b/usb/src/usb_device.cpp @@ -59,12 +59,12 @@ static const SSupportedDevice kSupportedDevices[] = { /* The bootloader exposes the bulk pair on alt setting 1. */ { "Hantek DSO-2250 Bootloader", "DSO2250", - {32768U, 512U, 0x02U, 0x86U, 2U, true, 2U, 6U, 5U, 3U, 4U}, + { 32768U, 512U, 0x02U, 0x86U, 2U, true, 2U, 6U, 5U, 3U, 4U }, 0x04B4U, 0x2250U, 0x04B5U, 0U, 1U, true }, { "Hantek DSO-2250", "DSO2250", - {32768U, 512U, 0x02U, 0x86U, 2U, true, 2U, 6U, 5U, 3U, 4U}, + { 32768U, 512U, 0x02U, 0x86U, 2U, true, 2U, 6U, 5U, 3U, 4U }, 0x04B5U, 0x2250U, 0x04B5U, 0U, 0U, false } }; @@ -175,7 +175,7 @@ static SUsbTransferResult makeTransferResult( SUsbScanResult enumerateSupportedDevices() { libusb_context *context = NULL; libusb_device **deviceList = NULL; - SUsbScanResult result = {{}, "", EScanStatus::eSuccess}; + SUsbScanResult result = { {}, "", EScanStatus::eSuccess }; const int initializationResult = libusb_init(&context); if (initializationResult != LIBUSB_SUCCESS) { @@ -231,13 +231,13 @@ SUsbConnectionResult connectToDevice( const SUsbDeviceInfo &deviceInfo, SUsbConnection *connection ) { - SUsbConnectionResult result = {"", EConnectionStatus::eDisconnected}; + 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}; + SFirmwareLoadResult firmwareResult = { "", EFirmwareLoadStatus::eLoaded }; ssize_t deviceCount = 0; int initializationResult = LIBUSB_SUCCESS; int openResult = LIBUSB_SUCCESS; @@ -422,7 +422,7 @@ SUsbConnectionResult connectToDevice( /** @fn disconnectFromDevice */ SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection) { - SUsbConnectionResult result = {"", EConnectionStatus::eDisconnected}; + SUsbConnectionResult result = { "", EConnectionStatus::eDisconnected }; int releaseResult = LIBUSB_SUCCESS; if (connection == NULL) { From 6640f15edc4b4229327e78c4b065a19f31cdcd11 Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 19:28:06 +0300 Subject: [PATCH 09/10] Set 5 V per division defaults --- app/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.cpp b/app/main.cpp index 87e6509..49852bf 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -307,7 +307,7 @@ 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 = From ff7404f28ac8561fd772b290431d88b5922536bc Mon Sep 17 00:00:00 2001 From: Anton Chernov Date: Sat, 5 Sep 2026 19:44:13 +0300 Subject: [PATCH 10/10] Recorded Stage 4 hardware verification --- HISTORY.md | 15 +++++++++++++++ README.md | 23 +++++++++++------------ docs/mainpage.md | 4 ++-- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index fb4f50f..0db1725 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -60,6 +60,21 @@ Records key decisions, structural changes, and completed development stages. - 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. diff --git a/README.md b/README.md index 4588f01..15006ee 100644 --- a/README.md +++ b/README.md @@ -12,19 +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. -The DSO-2250 capture-state response and its two-channel interleaved sample -format are decoded by deterministic, hardware-independent parser functions. -After a completed capture, the acquisition worker reads its full 32768-sample -two-channel buffer in 512-byte USB packets and 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. -Each supported-device entry supplies its capture protocol, including endpoints, -packet size, commands, channel layout, sample count, and completion state. +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 @@ -213,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`. diff --git a/docs/mainpage.md b/docs/mainpage.md index 21296ff..a2273e3 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -27,8 +27,8 @@ libusb for communication with supported instruments. - bounded buffering between USB reads and packet processing; - capture-state polling through vendor control requests and bulk endpoints; - bounded USB error recovery and safe device-loss handling. -- deterministic DSO-2250 capture-state and two-channel sample parsing. -- complete 32768-sample two-channel capture reads after a successful trigger. +- deterministic profile-driven capture-state and sample parsing. +- complete profile-defined capture reads after a successful trigger. - thread-safe publication of the latest decoded waveform and trigger point. - a per-device capture profile selected from the supported-device table.