/** * @file SignalBuffer.h * @brief Per-signal ring buffer with LTTB decimation. * * Header-only. Uses STL (this is not a MARTe2 component). */ #pragma once #include #include #include #include namespace StreamHubClient { /** * @brief Thread-safe circular buffer of (time, value) float64 pairs. */ struct SignalBuffer { explicit SignalBuffer(size_t cap = 20000) : capacity(cap), head(0), count(0) { t.resize(cap); v.resize(cap); } void setCapacity(size_t cap) { t.assign(cap, 0.0); v.assign(cap, 0.0); capacity = cap; head = 0; count = 0; } /** Append a (time, value) pair; overwrites oldest when full. */ void push(double time, double val) { t[head] = time; v[head] = val; head = (head + 1) % capacity; if (count < capacity) { count++; } } /** * @brief Copy the last `n` points into tOut/vOut (oldest → newest order). * @return Number of points written. */ size_t readLast(size_t n, std::vector& tOut, std::vector& vOut) const { size_t actual = std::min(n, count); if (actual == 0) { return 0; } tOut.resize(actual); vOut.resize(actual); size_t startIdx = (head + capacity - actual) % capacity; for (size_t i = 0; i < actual; i++) { size_t idx = (startIdx + i) % capacity; tOut[i] = t[idx]; vOut[i] = v[idx]; } return actual; } /** * @brief Read all points in [t0, t1] into tOut/vOut. * * Assumes points were pushed in non-decreasing time order (true for the * StreamHub live stream) and binary-searches the logical [0,count) index * space so the cost is O(log count + window) rather than O(count) — this * keeps per-frame rendering cheap even with a million-point ring. * @return Number of points written. */ size_t readRange(double t0, double t1, std::vector& tOut, std::vector& vOut) const { tOut.clear(); vOut.clear(); if (count == 0) { return 0; } const size_t startIdx = (head + capacity - count) % capacity; /* time at logical index i (0 = oldest, count-1 = newest) */ // first index with t >= t0 size_t lo = 0, hi = count; while (lo < hi) { size_t mid = lo + (hi - lo) / 2; if (t[(startIdx + mid) % capacity] < t0) { lo = mid + 1; } else { hi = mid; } } const size_t begin = lo; // first index with t > t1 lo = begin; hi = count; while (lo < hi) { size_t mid = lo + (hi - lo) / 2; if (t[(startIdx + mid) % capacity] <= t1) { lo = mid + 1; } else { hi = mid; } } const size_t end = lo; if (end <= begin) { return 0; } tOut.reserve(end - begin); vOut.reserve(end - begin); for (size_t i = begin; i < end; i++) { size_t idx = (startIdx + i) % capacity; tOut.push_back(t[idx]); vOut.push_back(v[idx]); } return tOut.size(); } size_t size() const { return count; } void clear() { head = 0; count = 0; } size_t capacity; std::vector t; std::vector v; size_t head; size_t count; }; /*---------------------------------------------------------------------------*/ /* LTTB — Largest Triangle Three Buckets */ /*---------------------------------------------------------------------------*/ /** * @brief Decimate tIn/vIn to at most `threshold` points using LTTB. * * Always preserves first and last points. * @return Number of output points. */ inline size_t LTTBDecimate( const std::vector& tIn, const std::vector& vIn, std::vector& tOut, std::vector& vOut, size_t threshold) { const size_t nIn = tIn.size(); if (nIn <= threshold || threshold < 2) { tOut = tIn; vOut = vIn; return nIn; } tOut.resize(threshold); vOut.resize(threshold); tOut[0] = tIn[0]; vOut[0] = vIn[0]; tOut[threshold - 1] = tIn[nIn - 1]; vOut[threshold - 1] = vIn[nIn - 1]; /* Bucket size (middle threshold-2 buckets cover points 1..nIn-2) */ double bucketSize = static_cast(nIn - 2) / static_cast(threshold - 2); size_t a = 0; /* index of last selected point */ for (size_t i = 0; i < threshold - 2; i++) { /* Calculate average of next bucket */ size_t avgRangeStart = static_cast((i + 1) * bucketSize) + 1; size_t avgRangeEnd = static_cast((i + 2) * bucketSize) + 1; if (avgRangeEnd >= nIn) { avgRangeEnd = nIn - 1; } double avgT = 0.0, avgV = 0.0; size_t avgLen = avgRangeEnd - avgRangeStart; for (size_t j = avgRangeStart; j < avgRangeEnd; j++) { avgT += tIn[j]; avgV += vIn[j]; } if (avgLen > 0) { avgT /= static_cast(avgLen); avgV /= static_cast(avgLen); } /* Select point in current bucket with max triangle area */ size_t rangeStart = static_cast(i * bucketSize) + 1; size_t rangeEnd = static_cast((i + 1) * bucketSize) + 1; if (rangeEnd >= nIn) { rangeEnd = nIn - 1; } double maxArea = -1.0; size_t maxIdx = rangeStart; for (size_t j = rangeStart; j < rangeEnd; j++) { double area = std::fabs( (tIn[a] - avgT) * (vIn[j] - vIn[a]) - (tIn[a] - tIn[j]) * (avgV - vIn[a]) ) * 0.5; if (area > maxArea) { maxArea = area; maxIdx = j; } } tOut[i + 1] = tIn[maxIdx]; vOut[i + 1] = vIn[maxIdx]; a = maxIdx; } return threshold; } } /* namespace StreamHubClient */