STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
CaptureWindow.hpp File Reference

The ADC a calibration capture records: a bounded, recent, checked mean. More...

#include <cstddef>
#include <cstdint>
#include <deque>
#include <unordered_map>
#include <vector>
Include dependency graph for CaptureWindow.hpp:
This graph shows which files directly or indirectly include this file:

Go to the source code of this file.

Classes

struct  fsw::calibration::AdcSample
struct  fsw::calibration::CaptureResult
class  fsw::calibration::CaptureWindow
 Per-sensor ring of recent samples, and the capture decision over it. More...

Namespaces

namespace  fsw
 Motorized ball valve position: raw ADC counts -> % open.
namespace  fsw::calibration
 Output smoothing for calibrated PT pressure, per sensor uid (ESP32 and LabJack T7 alike).

Variables

constexpr uint64_t fsw::calibration::kCaptureWindowMs = 1000
 How far back a capture averages.
constexpr uint64_t fsw::calibration::kMaxSampleAgeMs = 1500
 Newest sample older than this (in OUR clock) means the sensor is not live.
constexpr size_t fsw::calibration::kMinCaptureSamples = 3
 Below this a "mean" is not one, and the settle check has nothing to say.
constexpr uint64_t fsw::calibration::kStaleSamplePeriods = 2
 Liveness allowance for a slow sensor, in its own sample periods: one late sample is jitter, two missing in a row is a sensor that stopped.
constexpr uint64_t fsw::calibration::kMaxCaptureWindowMs = 5000
 Upper bound on the adaptive window and staleness allowance.
constexpr uint64_t fsw::calibration::kRingSpanMs = 8000
 Ring eviction: by span so it self-sizes across 1-250 Hz, by count so memory is bounded if a board ever runs fast.
constexpr size_t fsw::calibration::kRingMax = 1024
constexpr double fsw::calibration::kSettleZ = 6.0
 Half-mean difference, in standard errors, above which the window is not settled.
constexpr const char * fsw::calibration::kReasonOk = ""
 Why a capture was refused.
constexpr const char * fsw::calibration::kReasonNoSamples = "no_samples"
constexpr const char * fsw::calibration::kReasonStale = "stale"
constexpr const char * fsw::calibration::kReasonTooFew = "too_few"

Detailed Description

The ADC a calibration capture records: a bounded, recent, checked mean.

A capture must answer "what is this sensor reading RIGHT NOW", and the answer has to mean the same thing on a 250 Hz pressure transducer and on a 13 Hz load cell.

It did not. The calibration service kept a plain deque<int32_t> of the last 128 samples per sensor and averaged the whole thing, with this comment:

constexpr size_t kPtAdcRingMax = 128;   // ~0.5 s of samples at ~250 Hz

No board on the rig runs at 250 Hz. Measured pre-downsample ingest: PT 61-68 Hz, RTD 26 Hz, actuators 10 Hz, and a load cell 13.7 Hz. So the same 128 samples is 2 s on a PT and 9.3 s on a load cell, and every capture averaged the last nine seconds of the scale. On 2026-09-13 an operator added weight and captured 13 s later; roughly a quarter of the window still held the previous load, and the point landed 26% short of the line (steps of 52241, 53737, then 39238 counts per equal 2.268 kg). Three separate ZERO-load captures spanned 29,087 counts while the live signal's sigma was 798 — 36 sigma, impossible as noise, ordinary as a moving average over nine seconds.

So the window is bounded in TIME, not in samples, and two different clocks answer two different questions:

  • MEMBERSHIP uses the sample timestamp ts_ns (the bridge's reconstructed host-epoch time), measured relative to the NEWEST sample in this sensor's ring rather than to wall-now. Anchoring on the newest sample cancels any constant offset between the board's epoch and ours, so transport lag longer than the window can never starve every capture.
  • LIVENESS uses rx_ns, our own monotonic arrival stamp, against the capture's now_rx_ns. Clock-domain-pure, so it is immune to board clock skew — and it is what rejects a dead sensor, or a reconnect burst of samples that are old in our terms.

SETTLING. A window can be recent and still wrong, if the load changed inside it. The check compares the mean of the window's first half against its second half, normalised by the window's own noise, so a ramp or step reads as drift while plain noise does not. Normalising is what lets one threshold serve a 4-20 mA PT (codes ~1e6) and a load cell (~5e4) with nothing to tune per sensor. An unsettled capture is reported, never silently dropped: the caller records the point and flags it.

Pure and deterministic: no clock is read here — times are inputs, so the tests drive every edge case (see lib/test/test_capture_window.cpp).

NOTE on [time_sync] mode = "arrival" (BoardClockSync.hpp): that revert switch flattens a whole packet onto one stamp, which coarsens ts_ns to per-packet resolution. The newest-sample anchoring keeps membership from failing closed, and kMinCaptureSamples plus the drift check still catch the pathological case — but expect fewer, chunkier samples per window in that mode.

Definition in file CaptureWindow.hpp.