diff --git a/CMakeLists.txt b/CMakeLists.txt index 339e0d3..c34af6a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ set(SOURCES_LIST capture/src/acquisition_loop.cpp capture/src/raw_packet_queue.cpp capture/src/waveform_parser.cpp + capture/src/waveform_ring_buffer.cpp ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_tables.cpp @@ -60,6 +61,7 @@ set(HEADERS_LIST capture/inc/acquisition_loop.h capture/inc/raw_packet_queue.h capture/inc/waveform_parser.h + capture/inc/waveform_ring_buffer.h ) set(RAW_PACKET_QUEUE_TEST_SOURCES_LIST @@ -70,6 +72,10 @@ set(WAVEFORM_PARSER_TEST_SOURCES_LIST tests/waveform_parser_test.cpp ) +set(WAVEFORM_RING_BUFFER_TEST_SOURCES_LIST + tests/waveform_ring_buffer_test.cpp +) + if(CMAKE_BUILD_TYPE MATCHES "Debug") message(STATUS ">>> Debug build") add_compile_definitions(_DEBUG) @@ -136,6 +142,17 @@ if(BUILD_TESTING) enable_project_warnings(waveform_parser_tests) add_test(NAME waveform_parser COMMAND waveform_parser_tests) add_dependencies(${APP_NAME} waveform_parser_tests) + add_executable(waveform_ring_buffer_tests + ${WAVEFORM_RING_BUFFER_TEST_SOURCES_LIST} + capture/src/waveform_ring_buffer.cpp + ) + target_include_directories(waveform_ring_buffer_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/capture/inc + ) + target_link_libraries(waveform_ring_buffer_tests PRIVATE Threads::Threads) + enable_project_warnings(waveform_ring_buffer_tests) + add_test(NAME waveform_ring_buffer COMMAND waveform_ring_buffer_tests) + add_dependencies(${APP_NAME} waveform_ring_buffer_tests) add_test( NAME release_updater COMMAND ${Python3_EXECUTABLE} diff --git a/HISTORY.md b/HISTORY.md index 0db1725..dc93e26 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,82 +5,6 @@ Records key decisions, structural changes, and completed development stages. --- -## 2026-09-05 - -### Stage 4 preparation - Release metadata coverage - -- Registered standalone C++ test sources in dedicated CMake source lists. -- Extended the release updater to discover C++ test sources and update their - `@version` metadata together with production sources. -- Added deterministic CTest coverage that runs the updater in an isolated copy - of the project and verifies test-source version updates. -- Corrected the documented DSO-2250 operational endpoint configuration to - interface 0, alternate setting 0. - -### Stage 4 - Sample parser foundation - -- Added pure DSO-2250 parser functions for capture-state responses and complete - interleaved two-channel waveform buffers. -- Preserved the legacy byte layout: each sample pair is CH2 followed by CH1. -- Preserved the legacy capture-state trigger-point transformation in the new, - Qt-independent parser. -- Added deterministic CTest coverage for state parsing, trigger decoding, - channel order, and malformed capture buffers. - -### Stage 4 - Complete DSO-2250 capture reads - -- Start acquisition with the legacy capture-start and trigger-enable command - sequence before capture-state polling begins. -- On a complete DSO-2250 capture-state response, read the fixed two-channel - 32768-sample buffer as 128 consecutive 512-byte bulk packets. -- Stop incomplete waveform publication at the first failed packet and preserve - the existing bounded recovery and device-loss behavior. -- Restart capture and re-enable the trigger only after a complete buffer was - read and queued for processing. -- Added acquisition-status diagnostics for channel-data, capture-start, and - trigger-enable failures. - -### Stage 4 - Decoded waveform publication - -- Preserved the decoded capture trigger point alongside its raw waveform bytes - while the frame passes through the FIFO. -- Decode complete queued DSO-2250 frames in the processing worker instead of - interpreting raw channel bytes as capture-state data. -- Added a mutex-protected latest-waveform snapshot and `getLatestWaveform()` - API for the future renderer. -- Extended FIFO tests to verify trigger-point metadata is retained with each - packet. - -### Capture protocol profiles - -- Moved capture endpoints, command bytes, packet length, channel layout, - sample count, and completion state into each supported-device entry. -- Preserve the selected profile in the active USB connection and use it for - acquisition and waveform parsing instead of model-named function contracts. -- Validate profile packetisation and fixed capture-storage bounds before - requesting sample data. - -### Stage 4 - Hardware verification - -- Verified a 60-second acquisition on the initial Hantek DSO-2250 profile. -- Verified six 10-second Start/Stop cycles: the UI stayed responsive and the - connected device was ready for every subsequent Start. -- Verified active USB removal: acquisition stopped without a crash, deadlock, - or unbounded wait and reported the lost device while beginning a command. -- Verified automatic rediscovery, explicit reconnection, and a subsequent - successful acquisition after the device was reattached. -- Verified application shutdown during acquisition without a crash, deadlock, - or unbounded wait; the instrument returned to its connected idle LED state, - then turned off after application exit. -- Verified the Release build without warnings. Normal acquisition showed no - persistent recovery state or unexpected acquisition stop. - -### Code style - Immutable object naming - -- Reserved `UPPER_CASE` for preprocessor macros. -- Renamed immutable module objects to the `kPascalCase` convention while - retaining `const` storage and unchanged runtime behavior. - ## 2026-08-27 ### Project inception @@ -322,3 +246,122 @@ Records key decisions, structural changes, and completed development stages. - Decode capture-state responses and acquired sample packets. - Add deterministic fault-injection tests for timeout and transfer-error recovery, then verify recovery with a connected physical device. + +## 2026-09-05 + +### Stage 4 - Data ring buffer + +- Added `WaveformRingBuffer`, a bounded thread-safe buffer of decoded + two-channel waveform frames connecting the capture processing thread to + render consumption. +- The producer side never blocks: pushing a frame while full discards the + oldest buffered frame and increments a thread-safe dropped-frame count. +- The consumer side is non-blocking as well: `popLatest()` drains any + backlog and returns only the freshest frame, matching a real-time render + loop that must never wait on stale or absent data. +- Replaced the single-slot mutex-protected waveform snapshot in + `SAcquisitionLoop` with a `WaveformRingBuffer` member; `getLatestWaveform()` + keeps its existing signature and now delegates to `popLatest()`. +- Wired the render loop to call `getLatestWaveform()` once per frame and + surface the decoded sample count and trigger point on the status line, + proving the capture-to-render path end to end; plotting the waveform shape + remains a separate future task. +- Added deterministic CTest coverage for latest-frame semantics, overflow + drop-oldest with its counter, reset/reopen, an empty buffer, and concurrent + producer/consumer operation. +- Verified warning-free Debug and Release builds and a full CTest pass. + +### Stage 4 preparation - Release metadata coverage + +- Registered standalone C++ test sources in dedicated CMake source lists. +- Extended the release updater to discover C++ test sources and update their + `@version` metadata together with production sources. +- Added deterministic CTest coverage that runs the updater in an isolated copy + of the project and verifies test-source version updates. +- Corrected the documented DSO-2250 operational endpoint configuration to + interface 0, alternate setting 0. + +### Stage 4 - Sample parser foundation + +- Added pure DSO-2250 parser functions for capture-state responses and complete + interleaved two-channel waveform buffers. +- Preserved the legacy byte layout: each sample pair is CH2 followed by CH1. +- Preserved the legacy capture-state trigger-point transformation in the new, + Qt-independent parser. +- Added deterministic CTest coverage for state parsing, trigger decoding, + channel order, and malformed capture buffers. + +### Stage 4 - Complete DSO-2250 capture reads + +- Start acquisition with the legacy capture-start and trigger-enable command + sequence before capture-state polling begins. +- On a complete DSO-2250 capture-state response, read the fixed two-channel + 32768-sample buffer as 128 consecutive 512-byte bulk packets. +- Stop incomplete waveform publication at the first failed packet and preserve + the existing bounded recovery and device-loss behavior. +- Restart capture and re-enable the trigger only after a complete buffer was + read and queued for processing. +- Added acquisition-status diagnostics for channel-data, capture-start, and + trigger-enable failures. + +### Stage 4 - Decoded waveform publication + +- Preserved the decoded capture trigger point alongside its raw waveform bytes + while the frame passes through the FIFO. +- Decode complete queued DSO-2250 frames in the processing worker instead of + interpreting raw channel bytes as capture-state data. +- Added a mutex-protected latest-waveform snapshot and `getLatestWaveform()` + API for the future renderer. +- Extended FIFO tests to verify trigger-point metadata is retained with each + packet. + +### Capture protocol profiles + +- Moved capture endpoints, command bytes, packet length, channel layout, + sample count, and completion state into each supported-device entry. +- Preserve the selected profile in the active USB connection and use it for + acquisition and waveform parsing instead of model-named function contracts. +- Validate profile packetisation and fixed capture-storage bounds before + requesting sample data. + +### Stage 4 - Hardware verification + +- Verified a 60-second acquisition on the initial Hantek DSO-2250 profile. +- Verified six 10-second Start/Stop cycles: the UI stayed responsive and the + connected device was ready for every subsequent Start. +- Verified active USB removal: acquisition stopped without a crash, deadlock, + or unbounded wait and reported the lost device while beginning a command. +- Verified automatic rediscovery, explicit reconnection, and a subsequent + successful acquisition after the device was reattached. +- Verified application shutdown during acquisition without a crash, deadlock, + or unbounded wait; the instrument returned to its connected idle LED state, + then turned off after application exit. +- Verified the Release build without warnings. Normal acquisition showed no + persistent recovery state or unexpected acquisition stop. + +### Code style - Immutable object naming + +- Reserved `UPPER_CASE` for preprocessor macros. +- Renamed immutable module objects to the `kPascalCase` convention while + retaining `const` storage and unchanged runtime behavior. + +## 2026-09-06 + +### Stage 4 - Data ring buffer hardware verification + +- Verified the Release build succeeded without warnings. +- Verified the Connect/Start/Stop status-line and status-LED lifecycle on a + physical Hantek DSO-2250: `Stopped | Live mode | Connected` with a red LED + after Connect, `Acquiring` with a green LED after Start, and back to + `Stopped` with a red LED after Stop, with the connection retained. +- Verified a 60-second acquisition run and six repeated 10-second Start/Stop + cycles: the GUI stayed responsive and the connection remained usable for a + new Start after every Stop. +- Verified active USB removal during acquisition: the application did not + hang, acquisition stopped, and the status line reported + `Device disconnected: USB device lost while beginning command`. +- Verified automatic rediscovery after reattaching the device, followed by a + successful Connect and Start with the same status/LED lifecycle as above. +- Verified application shutdown during acquisition: no hang or crash; the + status LED blinked red briefly, then turned off. + diff --git a/app/main.cpp b/app/main.cpp index a3c2b6c..c111af3 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -86,6 +86,7 @@ #include "usb_device.h" using oscilloscope::capture::SAcquisitionLoop; +using oscilloscope::capture::SWaveformSamples; using oscilloscope::capture::EAcquisitionOperation; using oscilloscope::capture::EAcquisitionState; using oscilloscope::usb::EScanStatus; @@ -322,6 +323,9 @@ int main (void) { ); uint32_t nextUsbPresenceCheck = SDL_GetTicks() + kUsbPresenceIntervalMs; + SWaveformSamples latestWaveform{}; + uint32_t latestTriggerPoint = 0U; + bool hasWaveform = false; 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" @@ -364,6 +368,14 @@ int main (void) { &deviceStatus ); + if ( + oscilloscope::capture::getLatestWaveform( + &acquisitionLoop, &latestWaveform, &latestTriggerPoint + ) + ) { + hasWaveform = true; + } + if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Exit")) { @@ -550,9 +562,24 @@ int main (void) { } ImGui::EndChild(); + char waveformStatus[64]; + + if (hasWaveform) { + snprintf( + waveformStatus, + sizeof(waveformStatus), + "Waveform %zu samples (trigger %u)", + latestWaveform.sampleCount, + static_cast(latestTriggerPoint) + ); + } + else { + snprintf(waveformStatus, sizeof(waveformStatus), "Waveform none"); + } + ImGui::SetCursorScreenPos(statusPosition); ImGui::Text( - "%s | %s | %s | CH1 %s | CH2 %s", + "%s | %s | %s | CH1 %s | CH2 %s | %s", acquisitionRunning ? (acquisitionLoop.status.state.load() == EAcquisitionState::eRecovering @@ -561,7 +588,8 @@ int main (void) { demoMode ? "Demo mode" : "Live mode", deviceStatus.c_str(), channelEnabled[0] ? "on" : "off", - channelEnabled[1] ? "on" : "off" + channelEnabled[1] ? "on" : "off", + waveformStatus ); ImGui::End(); diff --git a/capture/inc/acquisition_loop.h b/capture/inc/acquisition_loop.h index ff513da..2aabaae 100644 --- a/capture/inc/acquisition_loop.h +++ b/capture/inc/acquisition_loop.h @@ -25,12 +25,12 @@ /******************************* Included files ******************************/ #include -#include #include #include "raw_packet_queue.h" #include "usb_device.h" #include "waveform_parser.h" +#include "waveform_ring_buffer.h" /********************************* Definitions ********************************/ @@ -82,11 +82,10 @@ struct SAcquisitionLoop { RawPacketQueue rawPacketQueue{8U}; /**< Bounded raw response FIFO */ 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 */ + mutable WaveformRingBuffer waveformRingBuffer{ + kWaveformRingBufferCapacity + }; /**< Decoded frames awaiting render consumption */ std::atomic stopRequested{false}; /**< Set to request a stop */ - bool hasWaveform{false}; /**< True after first decoded frame */ }; /********************* Application Programming Interface *********************/ diff --git a/capture/inc/waveform_ring_buffer.h b/capture/inc/waveform_ring_buffer.h new file mode 100644 index 0000000..f02e302 --- /dev/null +++ b/capture/inc/waveform_ring_buffer.h @@ -0,0 +1,99 @@ +/** + * @file waveform_ring_buffer.h + * @version 0.2.7 + * @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_RING_BUFFER_H_ +#define WAVEFORM_RING_BUFFER_H_ + +/******************************* Included files *******************************/ +#include +#include +#include +#include + +#include "waveform_parser.h" + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace capture { + +/** @brief Number of decoded frames retained between capture and render */ +static const size_t kWaveformRingBufferCapacity = 4U; + +/** @brief One decoded waveform frame with its trigger metadata */ +struct SWaveformFrame { + SWaveformSamples samples; /**< Decoded two-channel sample buffer */ + uint32_t triggerPoint; /**< Trigger position within the samples */ +}; + +/** + * @brief Provides a bounded thread-safe buffer of decoded waveform frames + * @details Connects the capture processing thread, which pushes each + * decoded frame, to render consumption, which only needs the freshest + * frame and must never block waiting for one. + */ +class WaveformRingBuffer { +public: + /** + * @brief Creates an empty buffer with fixed frame capacity + * @param[in] capacity Maximum number of retained frames + */ + explicit WaveformRingBuffer(size_t capacity); + + /** + * @brief Copies a decoded frame into the buffer without blocking + * @param[in] samples Decoded waveform samples to store + * @param[in] triggerPoint Trigger position associated with samples + * @returns True when the frame was accepted + * @note When full, the oldest frame is discarded before insertion. + */ + bool push(const SWaveformSamples &samples, uint32_t triggerPoint); + + /** + * @brief Removes every buffered frame and returns only the newest one + * @param[out] frame Destination receiving the most recent frame + * @returns True when a frame was returned, false when the buffer is empty + */ + bool popLatest(SWaveformFrame *frame); + + /** @brief Clears the buffer for a new acquisition run */ + void reset(); + + /** + * @brief Reads the number of frames discarded since the last reset + * @returns Number of frames discarded by the overflow policy + */ + size_t getDroppedFrameCount() const; + +private: + std::vector frames; + mutable std::mutex mutex; + size_t readIndex; + size_t writeIndex; + size_t frameCount; + size_t droppedFrameCount; +}; + +} // namespace capture +} // namespace oscilloscope +/******************************************************************************/ +#endif //! WAVEFORM_RING_BUFFER_H_ diff --git a/capture/src/acquisition_loop.cpp b/capture/src/acquisition_loop.cpp index 2e24b4d..7551358 100644 --- a/capture/src/acquisition_loop.cpp +++ b/capture/src/acquisition_loop.cpp @@ -149,13 +149,7 @@ void startAcquisitionLoop( loop->status.lastTransferStatus.store( usb::EUsbTransferStatus::eSuccess ); - { - std::lock_guard lock(loop->waveformMutex); - - loop->latestWaveform = {}; - loop->latestTriggerPoint = 0U; - loop->hasWaveform = false; - } + loop->waveformRingBuffer.reset(); loop->processingThread = std::thread(processRawPackets, loop); loop->workerThread = std::thread(pollCaptureState, loop, connection); } @@ -195,15 +189,17 @@ bool getLatestWaveform( uint32_t *triggerPoint ) { bool result = false; + SWaveformFrame frame; - if ((loop != NULL) && (waveform != NULL) && (triggerPoint != NULL)) { - std::lock_guard lock(loop->waveformMutex); - - if (loop->hasWaveform) { - *waveform = loop->latestWaveform; - *triggerPoint = loop->latestTriggerPoint; - result = true; - } + if ( + (loop != NULL) && + (waveform != NULL) && + (triggerPoint != NULL) && + loop->waveformRingBuffer.popLatest(&frame) + ) { + *waveform = frame.samples; + *triggerPoint = frame.triggerPoint; + result = true; } return result; @@ -352,11 +348,7 @@ static void processRawPackets(SAcquisitionLoop *loop) { &waveform ) ) { - std::lock_guard lock(loop->waveformMutex); - - loop->latestWaveform = waveform; - loop->latestTriggerPoint = packet.triggerPoint; - loop->hasWaveform = true; + loop->waveformRingBuffer.push(waveform, packet.triggerPoint); } } } diff --git a/capture/src/waveform_ring_buffer.cpp b/capture/src/waveform_ring_buffer.cpp new file mode 100644 index 0000000..67f2484 --- /dev/null +++ b/capture/src/waveform_ring_buffer.cpp @@ -0,0 +1,115 @@ +/** + * @file waveform_ring_buffer.cpp + * @version 0.2.7 + * @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_ring_buffer.h" + +/********************************* Definitions ********************************/ + +namespace oscilloscope { +namespace capture { + +/********************* Application Programming Interface *********************/ + +/** @fn WaveformRingBuffer::WaveformRingBuffer(size_t capacity) */ +WaveformRingBuffer::WaveformRingBuffer(const size_t capacity) : + frames(capacity), + readIndex(0U), + writeIndex(0U), + frameCount(0U), + droppedFrameCount(0U) { +} + +/*----------------------------------------------------------------------------*/ + +/** @fn bool WaveformRingBuffer::push(const SWaveformSamples&, uint32_t) */ +bool WaveformRingBuffer::push( + const SWaveformSamples &samples, + const uint32_t triggerPoint +) { + bool accepted = false; + std::lock_guard lock(mutex); + + if (!frames.empty()) { + if (frameCount == frames.size()) { + readIndex = (readIndex + 1U) % frames.size(); + --frameCount; + ++droppedFrameCount; + } + + frames[writeIndex].samples = samples; + frames[writeIndex].triggerPoint = triggerPoint; + writeIndex = (writeIndex + 1U) % frames.size(); + ++frameCount; + accepted = true; + } + + return accepted; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn bool WaveformRingBuffer::popLatest(SWaveformFrame *frame) */ +bool WaveformRingBuffer::popLatest(SWaveformFrame *frame) { + bool frameReturned = false; + std::lock_guard lock(mutex); + + if ((frame != NULL) && (frameCount > 0U)) { + const size_t latestIndex = + (readIndex + frameCount - 1U) % frames.size(); + + *frame = frames[latestIndex]; + readIndex = writeIndex; + frameCount = 0U; + frameReturned = true; + } + + return frameReturned; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn void WaveformRingBuffer::reset() */ +void WaveformRingBuffer::reset() { + std::lock_guard lock(mutex); + + readIndex = 0U; + writeIndex = 0U; + frameCount = 0U; + droppedFrameCount = 0U; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn size_t WaveformRingBuffer::getDroppedFrameCount() const */ +size_t WaveformRingBuffer::getDroppedFrameCount() const { + size_t result = 0U; + std::lock_guard lock(mutex); + + result = droppedFrameCount; + + return result; +} + +} // namespace capture +} // namespace oscilloscope +/******************************************************************************/ diff --git a/tests/waveform_ring_buffer_test.cpp b/tests/waveform_ring_buffer_test.cpp new file mode 100644 index 0000000..4ff26c7 --- /dev/null +++ b/tests/waveform_ring_buffer_test.cpp @@ -0,0 +1,230 @@ +/** + * @file waveform_ring_buffer_test.cpp + * @version 0.2.7 + * @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 +#include + +#include "waveform_ring_buffer.h" + +/********************************* Definitions ********************************/ + +namespace { + +using oscilloscope::capture::SWaveformFrame; +using oscilloscope::capture::SWaveformSamples; +using oscilloscope::capture::WaveformRingBuffer; + +/***************************** Private prototypes *****************************/ + +static bool expect(bool condition, const char *message); +static SWaveformSamples makeSamples(uint8_t marker, size_t sampleCount); +static bool testPopLatestReturnsNewestWithoutOverflow(); +static bool testOverflowDropsOldestAndCountsDrop(); +static bool testResetClearsBufferAndDropCounter(); +static bool testEmptyBufferPopLatestFails(); +static bool testConcurrentProducerConsumer(); + +/****************************** Private functions *****************************/ + +/** @fn expect */ +static bool expect(const bool condition, const char *message) { + bool result = condition; + + if (!condition) { + std::cerr << "FAILED: " << message << std::endl; + } + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn makeSamples */ +static SWaveformSamples makeSamples( + const uint8_t marker, + const size_t sampleCount +) { + SWaveformSamples samples{}; + + samples.channelOne[0] = marker; + samples.sampleCount = sampleCount; + + return samples; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testPopLatestReturnsNewestWithoutOverflow */ +static bool testPopLatestReturnsNewestWithoutOverflow() { + WaveformRingBuffer buffer(3U); + SWaveformFrame frame; + bool result = true; + + result = expect( + buffer.push(makeSamples(1U, 100U), 11U), "push first frame" + ) && result; + result = expect( + buffer.push(makeSamples(2U, 200U), 22U), "push second frame" + ) && result; + result = expect(buffer.popLatest(&frame), "pop latest frame") && result; + result = expect(frame.samples.channelOne[0] == 2U, "newest marker") && + result; + result = expect(frame.samples.sampleCount == 200U, "newest sample count") + && result; + result = expect(frame.triggerPoint == 22U, "newest trigger point") && + result; + result = expect(!buffer.popLatest(&frame), "drained after pop latest") && + result; + result = expect(buffer.getDroppedFrameCount() == 0U, "no drops") && result; + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testOverflowDropsOldestAndCountsDrop */ +static bool testOverflowDropsOldestAndCountsDrop() { + WaveformRingBuffer buffer(2U); + SWaveformFrame frame; + bool result = true; + + result = expect(buffer.push(makeSamples(1U, 1U), 1U), "overflow push 1") && + result; + result = expect(buffer.push(makeSamples(2U, 1U), 2U), "overflow push 2") && + result; + result = expect(buffer.push(makeSamples(3U, 1U), 3U), "overflow push 3") && + result; + result = expect(buffer.popLatest(&frame), "pop after overflow") && result; + result = expect(frame.samples.channelOne[0] == 3U, "newest retained") && + result; + result = expect(buffer.getDroppedFrameCount() == 1U, "drop counter") && + result; + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testResetClearsBufferAndDropCounter */ +static bool testResetClearsBufferAndDropCounter() { + WaveformRingBuffer buffer(1U); + SWaveformFrame frame; + bool result = true; + + buffer.push(makeSamples(1U, 1U), 1U); + buffer.push(makeSamples(2U, 1U), 2U); + buffer.reset(); + result = expect(buffer.getDroppedFrameCount() == 0U, "reset drop counter") + && result; + result = expect(!buffer.popLatest(&frame), "reset clears old frames") && + result; + result = expect(buffer.push(makeSamples(3U, 1U), 3U), "push after reset") + && result; + result = expect(buffer.popLatest(&frame), "pop after reset") && result; + result = expect(frame.samples.channelOne[0] == 3U, "reset frame value") && + result; + + return result; +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testEmptyBufferPopLatestFails */ +static bool testEmptyBufferPopLatestFails() { + WaveformRingBuffer buffer(2U); + SWaveformFrame frame; + + return expect(!buffer.popLatest(&frame), "empty buffer pop latest"); +} + +/*----------------------------------------------------------------------------*/ + +/** @fn testConcurrentProducerConsumer */ +static bool testConcurrentProducerConsumer() { + static const uint8_t kFrameTotal = 64U; + WaveformRingBuffer buffer(4U); + std::atomic maxObserved{0U}; + std::atomic stopConsumer{false}; + std::thread consumer([&buffer, &maxObserved, &stopConsumer]() { + SWaveformFrame frame; + + while (!stopConsumer.load()) { + if (buffer.popLatest(&frame)) { + if (frame.samples.channelOne[0] > maxObserved.load()) { + maxObserved.store(frame.samples.channelOne[0]); + } + } + } + }); + uint8_t marker = 0U; + bool sawFinalFrame = false; + SWaveformFrame frame; + bool result = true; + + for (marker = 0U; marker < kFrameTotal; ++marker) { + buffer.push(makeSamples(marker, marker), marker); + } + while (!sawFinalFrame) { + if ( + buffer.popLatest(&frame) && + (frame.samples.channelOne[0] == (kFrameTotal - 1U)) + ) { + sawFinalFrame = true; + } + else if (maxObserved.load() == (kFrameTotal - 1U)) { + sawFinalFrame = true; + } + else { + std::this_thread::yield(); + } + } + stopConsumer.store(true); + consumer.join(); + result = expect(sawFinalFrame, "final frame observed by some consumer") && + result; + + return result; +} + +} // namespace + +/********************* Application Programming Interface *********************/ + +/** @fn main */ +int main() { + bool passed = true; + int result = 1; + + passed = testPopLatestReturnsNewestWithoutOverflow() && passed; + passed = testOverflowDropsOldestAndCountsDrop() && passed; + passed = testResetClearsBufferAndDropCounter() && passed; + passed = testEmptyBufferPopLatestFails() && passed; + passed = testConcurrentProducerConsumer() && passed; + if (passed) { + result = 0; + } + + return result; +} +/******************************************************************************/