30#define CHECK(cond, ...) \
34 std::printf(" ❌ [%s:%d] ", __func__, __LINE__); \
35 std::printf(__VA_ARGS__); \
42constexpr uint64_t
NS_MS = 1000000ULL;
43constexpr uint64_t
EPOCH = 1800000000000000000ULL;
44constexpr uint16_t
UID = 4101;
53 uint64_t
s = 0x2026091300000001ULL;
54 int32_t
next(int32_t amplitude) {
55 s =
s * 6364136223846793005ULL + 1442695040888963407ULL;
56 return static_cast<int32_t
>((
s >> 33) % (2 * amplitude + 1)) - amplitude;
62 Lcg* noise =
nullptr, int32_t amplitude = 0) {
63 for (
size_t i = 0; i <
count; i++) {
64 const int32_t v = noise ?
value + noise->next(amplitude) :
value;
83 constexpr int32_t kBefore = 100000;
84 constexpr int32_t kAfter = 150000;
91 CHECK(
r.ok,
"capture should succeed, reason=%s",
r.reason);
92 CHECK(std::fabs(
r.adc_avg - kAfter) < 0.01 * (kAfter - kBefore),
93 "bounded mean %.1f should be within 1%% of the step of %d",
r.adc_avg, kAfter);
94 CHECK(
r.settled,
"a step that finished 13 s ago must read as settled (z=%.1f)",
r.drift_z);
96 "span %" PRIu64
" ms should not exceed the window",
r.span_ms);
105 constexpr size_t kOldRingMax = 128;
106 std::vector<int32_t> hist;
108 for (
size_t i = 0; i < 411; i++)
109 hist.push_back(kBefore + n2.
next(800));
110 for (
size_t i = 0; i < 41; i++)
111 hist.push_back(kAfter + n2.
next(800));
113 double old_sum = 0.0;
114 for (
size_t i = hist.size() - kOldRingMax; i < hist.size(); i++)
116 const double old_step = old_sum / kOldRingMax - kBefore;
117 const double true_step = kAfter - kBefore;
118 CHECK(old_step < 0.8 * true_step,
119 "the old 128-sample mean should fall >20%% short (got %.0f of %.0f)", old_step,
125 for (
size_t i = 0; i < hist.size(); i++) {
127 bounded.
push(
UID, hist[i], ns, ns);
131 CHECK(fixed.
ok && std::fabs(fixed.
adc_avg - kAfter) < 0.01 * true_step,
132 "bounded mean %.1f should still be the new load", fixed.
adc_avg);
143 CHECK(
r.ok,
"capture should succeed, reason=%s",
r.reason);
144 CHECK(std::fabs(
r.adc_avg - 30000.0) < 1.0,
"mean %.1f should be the post-step value",
147 "span %" PRIu64
" ms should be the window, not the ring",
r.span_ms);
149 CHECK(
r.n > 200 &&
r.n < 300,
"expected ~250 samples in the window, got %zu",
r.n);
158 CHECK(!
r.ok,
"a 60 s old sensor must not yield a capture");
159 CHECK(std::string(
r.reason) ==
"stale",
"reason should be stale, got %s",
r.reason);
160 CHECK(
r.age_ms >= 60000,
"age %" PRIu64
" ms should reflect the gap",
r.age_ms);
168 CHECK(!none.
ok && std::string(none.
reason) ==
"no_samples",
"unknown uid → no_samples, got %s",
173 CHECK(!few.
ok && std::string(few.
reason) ==
"too_few",
"2 samples → too_few, got %s",
185 CHECK(!
r.ok && std::string(
r.reason) ==
"no_samples",
186 "after clear() the pre-drop samples must be gone, got %s",
r.reason);
187 CHECK(w.
uids().empty(),
"clear() should empty uids()");
197 CHECK(
r.ok &&
r.settled,
"steady noisy signal should be settled (z=%.2f)",
r.drift_z);
205 CHECK(
r.ok,
"a mid-step capture is still recorded");
206 CHECK(!
r.settled,
"a step inside the window must read unsettled (z=%.2f)",
r.drift_z);
216 CHECK(
r.ok &&
r.settled,
"a step 3 s ago should be settled (z=%.2f)",
r.drift_z);
217 CHECK(std::fabs(
r.adc_avg - 150000.0) < 500.0,
"mean %.1f should be the new load",
224 CHECK(
r.ok &&
r.settled,
"a constant signal is settled (z=%.2f)",
r.drift_z);
225 CHECK(std::isfinite(
r.drift_z),
"drift_z must stay finite when sd is 0");
226 CHECK(
r.spread == 0.0,
"spread should be 0, got %.1f",
r.spread);
243 for (
size_t i = 0; i < 60; i++, t += 1000) {
245 slow.push(
UID, 100000, ns, ns);
248 "1 Hz ring should hold ~8 s, got %zu", slow.size(
UID));
250 "1 Hz ring %zu must outlast the largest window", slow.size(
UID));
260 for (uint64_t period : {800ULL, 1300ULL}) {
263 uint64_t t =
feed(w, 0, 10, period, 100000, &noise, 800);
266 CHECK(
r.ok,
"%" PRIu64
" ms load cell should capture, reason=%s", period,
r.reason);
268 CHECK(
r.window_ms == 3 * period,
"window %" PRIu64
" should be 3 periods",
r.window_ms);
269 CHECK(std::fabs(
r.adc_avg - 100000.0) < 800.0,
"mean %.1f",
r.adc_avg);
274 "%" PRIu64
" ms sensor silent 3 periods must be stale, got %s", period, dead.
reason);
281 uint64_t t =
feed(w, 0, 500, 15, 20000);
284 "PT window %" PRIu64
" should stay at the floor",
r.window_ms);
286 "PT staleness %" PRIu64
" should stay at the floor",
r.max_age_ms);
289 "a PT silent 1.6 s is stale as before, got %s", late.
reason);
295 const uint64_t ns =
EPOCH;
296 w.
push(2101, 10, ns, ns);
297 w.
push(4101, 20, ns, ns);
298 w.
push(2201, 30, ns, ns);
299 const std::vector<uint16_t> ids = w.
uids();
300 CHECK(ids.size() == 3,
"expected 3 uids, got %zu", ids.size());
301 CHECK(ids[0] == 2101 && ids[1] == 2201 && ids[2] == 4101,
"uids should come back sorted");
304 CHECK(w.
uids().size() == 2,
"clear(uid) should drop just that one");
310 std::printf(
"CaptureWindow tests\n");
323 std::printf(
" ✅ all passed\n");
The ADC a calibration capture records: a bounded, recent, checked mean.
Per-sensor ring of recent samples, and the capture decision over it.
CaptureResult capture(uint16_t uid, uint64_t now_rx_ns) const
What this sensor reads now, or why that cannot be answered.
void clear()
Forget everything.
void push(uint16_t uid, int32_t adc, uint64_t ts_ns, uint64_t rx_ns)
Record one sample.
std::vector< uint16_t > uids() const
Every uid with at least one sample.
size_t size(uint16_t uid) const
Samples currently held for a uid (diagnostics and tests).
size_type count(const key_type &key) const
void fast_sensor_bounds_unchanged()
void low_rate_step_after_settle()
uint64_t now_ns(uint64_t t_ms)
constexpr uint64_t PT_PERIOD_MS
A fast transducer, for contrast.
void clear_on_reconnect()
void window_bound_high_rate()
void uids_reports_live_sensors()
constexpr uint64_t LC_PERIOD_MS
The measured load-cell cadence: 13.7 Hz is one sample every ~73 ms.
void slow_load_cell_captures()
uint64_t feed(CaptureWindow &w, uint64_t t_ms, size_t count, uint64_t period_ms, int32_t value, Lcg *noise=nullptr, int32_t amplitude=0)
Feed count samples at period_ms, returning the time just after the last one.
void stale_sensor_rejected()
constexpr uint64_t kCaptureWindowMs
How far back a capture averages.
constexpr size_t kMinCaptureSamples
Below this a "mean" is not one, and the settle check has nothing to say.
constexpr uint64_t kMaxCaptureWindowMs
Upper bound on the adaptive window and staleness allowance.
constexpr size_t kRingMax
constexpr double kSettleZ
Half-mean difference, in standard errors, above which the window is not settled.
constexpr uint64_t kRingSpanMs
Ring eviction: by span so it self-sizes across 1-250 Hz, by count so memory is bounded if a board eve...
constexpr uint64_t kMaxSampleAgeMs
Newest sample older than this (in OUR clock) means the sensor is not live.
Deterministic noise, same shape as test_board_clock_sync.cpp's Lcg.
int32_t next(int32_t amplitude)
bool ok
false → the caller must NOT create a point