STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
CaptureWindow.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5
6namespace fsw {
7namespace calibration {
8
9namespace {
10
11constexpr uint64_t NS_PER_MS = 1000000ULL;
12
13/** Saturating difference — clocks are unsigned and callers may pass a `now` that predates
14 * a sample (a reordered packet, or a test driving times by hand). Wrapping here would
15 * turn "41 ms old" into "584 million years old" and reject a healthy capture. */
16uint64_t ms_between(uint64_t later_ns, uint64_t earlier_ns) {
17 return later_ns > earlier_ns ? (later_ns - earlier_ns) / NS_PER_MS : 0;
18}
19
20double mean_of(const std::vector<double>& v, size_t from, size_t to) {
21 double sum = 0.0;
22 for (size_t i = from; i < to; i++)
23 sum += v[i];
24 return sum / static_cast<double>(to - from);
25}
26
27} // namespace
28
29void CaptureWindow::push(uint16_t uid, int32_t adc, uint64_t ts_ns, uint64_t rx_ns) {
30 auto& ring = rings_[uid];
31 ring.push_back(AdcSample{ts_ns, rx_ns, adc});
32
33 // Evict by span first: that is what makes one ring serve a 10 Hz actuator and a
34 // 250 Hz transducer without either being sized for the other.
35 const uint64_t span_limit_ns = kRingSpanMs * NS_PER_MS;
36 while (ring.size() > 1 && ring.back().ts_ns > ring.front().ts_ns &&
37 ring.back().ts_ns - ring.front().ts_ns > span_limit_ns) {
38 ring.pop_front();
39 }
40 // Then by count, so a fast board cannot grow it without bound.
41 while (ring.size() > kRingMax)
42 ring.pop_front();
43}
44
45CaptureResult CaptureWindow::capture(uint16_t uid, uint64_t now_rx_ns) const {
47
48 auto it = rings_.find(uid);
49 if (it == rings_.end() || it->second.empty()) {
50 r.reason = kReasonNoSamples;
51 return r;
52 }
53 const std::deque<AdcSample>& ring = it->second;
54 const AdcSample& newest = ring.back();
55
56 // This sensor's own sample period, from the ring's mean spacing (sample clock). A fast
57 // PT gives a few ms and both bounds below stay at their floors, exactly as before; a
58 // ~1 Hz load cell gets a window that holds kMinCaptureSamples of its samples and a
59 // staleness limit it can actually meet. Mean, not median: a batched board delivers
60 // bursts of near-identical stamps, and only the mean still averages to the true rate.
61 uint64_t period_ms = 0;
62 if (ring.size() >= 2)
63 period_ms = ms_between(newest.ts_ns, ring.front().ts_ns) / (ring.size() - 1);
64 r.window_ms =
65 std::clamp<uint64_t>(kMinCaptureSamples * period_ms, kCaptureWindowMs, kMaxCaptureWindowMs);
66 r.max_age_ms =
67 std::clamp<uint64_t>(kStaleSamplePeriods * period_ms, kMaxSampleAgeMs, kMaxCaptureWindowMs);
68
69 // Liveness, on OUR clock. A sensor that stopped publishing still "has" samples; that
70 // is exactly how a capture on a dead channel used to succeed and record a stale code.
71 r.age_ms = ms_between(now_rx_ns, newest.rx_ns);
72 if (r.age_ms > r.max_age_ms) {
73 r.reason = kReasonStale;
74 return r;
75 }
76
77 // Membership, anchored on the newest SAMPLE time rather than on now, so a constant
78 // board-to-host epoch offset cancels instead of emptying the window.
79 const uint64_t window_ns = r.window_ms * NS_PER_MS;
80 const uint64_t floor_ns = newest.ts_ns > window_ns ? newest.ts_ns - window_ns : 0;
81
82 std::vector<double> vals;
83 vals.reserve(ring.size());
84 uint64_t oldest_ts = newest.ts_ns;
85 for (const AdcSample& s : ring) {
86 if (s.ts_ns < floor_ns)
87 continue;
88 vals.push_back(static_cast<double>(s.adc));
89 oldest_ts = std::min(oldest_ts, s.ts_ns);
90 }
91
92 r.n = vals.size();
93 r.span_ms = ms_between(newest.ts_ns, oldest_ts);
94
95 if (r.n < kMinCaptureSamples) {
96 r.reason = kReasonTooFew;
97 return r;
98 }
99
100 r.adc_avg = mean_of(vals, 0, vals.size());
101 const auto mm = std::minmax_element(vals.begin(), vals.end());
102 r.spread = *mm.second - *mm.first;
103
104 // Settling: a step or ramp inside the window shows up as a difference between the
105 // two half-means that plain noise cannot produce. Normalising by the window's own
106 // noise is what lets one threshold cover every sensor type on the rig.
107 const size_t half = vals.size() / 2;
108 const double m1 = mean_of(vals, 0, half);
109 const double m2 = mean_of(vals, half, vals.size());
110 r.drift = std::fabs(m2 - m1);
111
112 // Pool the noise WITHIN each half, never across the whole window. Measuring scatter
113 // about the window mean would fold the step itself into the denominator: a 50,000-count
114 // jump makes sd ~25,000 and drags z down to ~3.7, so the larger the step the better it
115 // hides. Residuals about each half's own mean leave only the noise, and z rises with the
116 // step as it should. (This is the two-sample pooled standard error.)
117 double ss = 0.0;
118 for (size_t i = 0; i < half; i++)
119 ss += (vals[i] - m1) * (vals[i] - m1);
120 for (size_t i = half; i < vals.size(); i++)
121 ss += (vals[i] - m2) * (vals[i] - m2);
122 const size_t n1 = half, n2 = vals.size() - half;
123 const double dof = static_cast<double>(n1 + n2) - 2.0;
124 const double sd_pooled = dof > 0.0 ? std::sqrt(ss / dof) : 0.0;
125 // Floor at one ADC count: a perfectly quiet signal (the simulator, or a stuck channel)
126 // has sd 0, and dividing by it would call any drift infinitely significant.
127 const double se = std::max(
128 sd_pooled * std::sqrt(1.0 / static_cast<double>(n1) + 1.0 / static_cast<double>(n2)), 1.0);
129 r.drift_z = r.drift / se;
130 r.settled = r.drift_z <= kSettleZ;
131
132 r.ok = true;
133 r.reason = kReasonOk;
134 return r;
135}
136
138 rings_.clear();
139}
140
141void CaptureWindow::clear(uint16_t uid) {
142 rings_.erase(uid);
143}
144
145std::vector<uint16_t> CaptureWindow::uids() const {
146 std::vector<uint16_t> out;
147 out.reserve(rings_.size());
148 for (const auto& [uid, ring] : rings_) {
149 if (!ring.empty())
150 out.push_back(uid);
151 }
152 std::sort(out.begin(), out.end());
153 return out;
154}
155
156size_t CaptureWindow::size(uint16_t uid) const {
157 auto it = rings_.find(uid);
158 return it == rings_.end() ? 0 : it->second.size();
159}
160
161} // namespace calibration
162} // namespace fsw
The ADC a calibration capture records: a bounded, recent, checked mean.
static uint8_t ring[RING_SIZE]
Definition board_uart9.c:10
std::unordered_map< uint16_t, std::deque< AdcSample > > rings_
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).
static STAR_MCP3201 adc(MCP3201_CS_PIN, &mcp_spi)
double mean_of(const std::vector< double > &v, size_t from, size_t to)
uint64_t ms_between(uint64_t later_ns, uint64_t earlier_ns)
Saturating difference — clocks are unsigned and callers may pass a now that predates a sample (a reor...
Output smoothing for calibrated PT pressure, per sensor uid (ESP32 and LabJack T7 alike).
constexpr const char * kReasonTooFew
constexpr const char * kReasonNoSamples
constexpr uint64_t kCaptureWindowMs
How far back a capture averages.
constexpr const char * kReasonOk
Why a capture was refused.
constexpr const char * kReasonStale
constexpr uint64_t kStaleSamplePeriods
Liveness allowance for a slow sensor, in its own sample periods: one late sample is jitter,...
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.
Motorized ball valve position: raw ADC counts -> % open.
uint64_t ts_ns
sample time, board-derived (spacing is what matters)
uint64_t rx_ns
arrival time on OUR monotonic clock
static boot_report_t r