ESPectre SDK 2.8.0-280-gac7af68
Wi-Fi CSI motion sensing for ESP32 firmware
Loading...
Searching...
No Matches
lightweight_detector.h
Go to the documentation of this file.
1/*
2 * ESPectre - Lightweight Detector
3 *
4 * Vote-free weighted fusion of turbulence autocorrelation and aggregated
5 * turbulence IQR. Mirrors
6 * src/python/micro_espectre/lightweight_detector.py.
7 *
8 * Author: Francesco Pace <francesco.pace@gmail.com>
9 * SPDX-License-Identifier: GPL-3.0-only
10 * Commercial licensing available under separate agreement; see LICENSING.md.
11 */
12#pragma once
13
14#include "base_detector.h"
15#include "csi_format.h"
16#include "csi_features.h"
18
19#include <cstddef>
20#include <cstdint>
21#include <vector>
22
23namespace espectre {
24
25constexpr float LIGHTWEIGHT_AUTOCORR_CENTER = 0.3919344866784947f;
26constexpr float LIGHTWEIGHT_AUTOCORR_SCALE = 0.3798648330757351f;
27constexpr float LIGHTWEIGHT_AUTOCORR_WEIGHT = 5.083034533668216f;
28constexpr float LIGHTWEIGHT_TURB_IQR_OVER_MEAN_AGGR_CENTER = 0.24612139211074338f;
29constexpr float LIGHTWEIGHT_TURB_IQR_OVER_MEAN_AGGR_SCALE = 0.20056599613462603f;
30constexpr float LIGHTWEIGHT_TURB_IQR_OVER_MEAN_AGGR_WEIGHT = 4.997501915217463f;
31constexpr float LIGHTWEIGHT_INTERCEPT = 1.0776769868761f;
32
33constexpr float LIGHTWEIGHT_TRAIN_IDLE_Q95_LOGIT = -2.253902812716911f;
34constexpr float LIGHTWEIGHT_STARTUP_QUANTILE = 0.95f;
35constexpr float LIGHTWEIGHT_STARTUP_STRENGTH = 0.5f;
36constexpr uint8_t LIGHTWEIGHT_STARTUP_SAMPLE_LIMIT = 64U;
37
38// Settled-level rule: how long the stream has to stay quiet before the startup
39// threshold is allowed to come down, and by how much margin above the level it
40// settled at. 12 blocks of 20 evaluations is 60 s at the nominal cadence. The
41// margin is in logit units; 2.7 is the conservative temporal-window operating
42// point that clears the weak-link recall floor without changing the measured
43// normal-link or quiet-room FP tails.
44constexpr uint8_t LIGHTWEIGHT_SETTLE_BLOCKS = 12U;
46constexpr float LIGHTWEIGHT_SETTLE_MARGIN_LOGITS = 2.7f;
47
48/**
49 * The default detector: self-calibrating, no training data required.
50 *
51 * Fuses turbulence autocorrelation with robust spread from a five-bin
52 * aggregated turbulence stream, and adapts its threshold to the room
53 * during startup calibration. After that, a long quiet stretch can still
54 * lower the live threshold when the opening was noisier than the rest of
55 * the session. The full runtime emits `IRuntimeListener::on_threshold_changed()`
56 * for that drop; a core-only integration must re-read `get_threshold()` after
57 * `update_state()`. Prefer it unless you have a reason to run
58 * `HighAccuracyDetector`.
59 *
60 * Most integrations never construct one: `RuntimeConfig::detection_algorithm`
61 * selects it and the runtime owns the lifecycle. Drive it directly only on the
62 * core-only path, where your firmware already captures CSI:
63 *
64 * @code
65 * espectre::LightweightDetector detector;
66 * // per packet, from your capture callback:
67 * detector.process_packet(csi, csi_len, espectre::DEFAULT_SUBCARRIERS,
68 * espectre::HT20_SELECTED_BAND_SIZE, rssi_dbm);
69 * // on your evaluation cadence:
70 * detector.update_state();
71 * if (detector.is_ready() && detector.get_state() == espectre::MotionState::MOTION) { ... }
72 * @endcode
73 *
74 * `is_ready()` is false until the window fills; results before that are not
75 * meaningful. See `runtime/esp_idf/csi_pipeline.cpp` for the reference
76 * normalization, cadence, and hit filtering around these calls, and
77 * `docs/ALGORITHMS.md` for the algorithm itself.
78 *
79 * @par Threading
80 * Not thread-safe. `process_packet()` and `update_state()` must not run
81 * concurrently.
82 */
84 public:
85 /**
86 * @param window_size Detector window in packets
87 * @param threshold Motion probability threshold
88 * @param autocorr_lag Turbulence autocorrelation distance in packets
89 *
90 * Production uses the nominal-rate default. Alternate lags are exposed for
91 * replay experiments only: changing the feature offset requires validating
92 * the fitted coefficients before deployment. See detector_timing.h.
93 */
95 float threshold = LIGHTWEIGHT_DEFAULT_THRESHOLD,
96 uint16_t autocorr_lag = 1U);
97
98 ~LightweightDetector() override = default;
99 LightweightDetector(LightweightDetector&& other) noexcept = default;
103
104 void process_packet(const int8_t* csi_data, size_t csi_len,
105 const uint8_t* selected_subcarriers = nullptr,
106 uint8_t num_subcarriers = 0,
107 int8_t rssi_dbm = INT8_MIN) override;
108 void advance_missing_slots(uint32_t count) override;
109 void update_state() override;
110 void reset() override;
111 void clear_buffer() override;
112 void configure_hampel(bool enabled,
113 uint8_t window_size = HAMPEL_TURBULENCE_WINDOW_DEFAULT,
114 float threshold = HAMPEL_TURBULENCE_THRESHOLD_DEFAULT) override;
115 void configure_lowpass(bool enabled,
116 float cutoff_hz = LOWPASS_CUTOFF_DEFAULT) override;
117 bool is_ready() const override;
118 bool set_threshold(float threshold) override;
119 bool set_adaptive_threshold(float threshold) override;
120 float get_threshold() const override { return threshold_; }
121 const char* get_name() const override { return "Lightweight"; }
122 float get_startup_threshold_factor() const override {
124 }
125 bool startup_gate_enabled() const override { return true; }
128
129 float get_turb_autocorr() const { return current_turb_autocorr_; }
130 float get_turb_iqr_over_mean_aggr() const { return current_turb_iqr_over_mean_aggr_; }
131 float get_logit() const { return current_logit_; }
132
133 private:
134 float calculate_turb_autocorr_() const;
135 float calculate_turb_iqr_over_mean_aggr_() const;
136 float calculate_logit_(float turb_autocorr, float turb_iqr_over_mean_aggr) const;
137 void add_aggregated_turbulence_(float turbulence);
138 static float sigmoid_(float value);
139 static float quantile_(const float* values, uint8_t count, float quantile);
140 float startup_quantile_() const;
141 void observe_settled_level_();
142 void reset_settled_level_();
143 void clear_fusion_inputs_();
144
145 float threshold_;
146 float current_logit_;
147 float current_turb_autocorr_;
148 float current_turb_iqr_over_mean_aggr_;
149 float startup_logits_[LIGHTWEIGHT_STARTUP_SAMPLE_LIMIT]{};
150 uint8_t startup_logit_count_;
151 float adapted_threshold_;
152 bool adapted_threshold_ready_;
153 uint16_t autocorr_lag_;
154 float settle_blocks_[LIGHTWEIGHT_SETTLE_BLOCKS]{};
155 float settle_block_max_;
156 uint8_t settle_block_evaluations_;
157 uint8_t settle_block_count_;
158 uint8_t settle_block_index_;
159 std::vector<float> aggregated_turbulence_buffer_;
160 FilteredTurbulenceRing aggregated_turbulence_;
161};
162
163} // namespace espectre
BaseDetector(uint16_t window_size=DETECTOR_DEFAULT_WINDOW_SIZE)
Constructor.
bool set_threshold(float threshold) override
Set detection threshold.
bool startup_gate_enabled() const override
Whether startup calibration uses the consistency gate (threshold.h).
LightweightDetector(uint16_t window_size=DETECTOR_DEFAULT_WINDOW_SIZE, float threshold=LIGHTWEIGHT_DEFAULT_THRESHOLD, uint16_t autocorr_lag=1U)
void update_state() override
Update state machine (call at publish interval).
~LightweightDetector() override=default
void on_startup_calibration_complete() override
Hook called when startup calibration completes successfully.
void configure_lowpass(bool enabled, float cutoff_hz=LOWPASS_CUTOFF_DEFAULT) override
Configure low-pass filter.
LightweightDetector(LightweightDetector &&other) noexcept=default
void configure_hampel(bool enabled, uint8_t window_size=HAMPEL_TURBULENCE_WINDOW_DEFAULT, float threshold=HAMPEL_TURBULENCE_THRESHOLD_DEFAULT) override
Configure Hampel filter.
void on_startup_calibration_begin() override
Hook called immediately before startup calibration begins.
float get_threshold() const override
Get current threshold.
float get_startup_threshold_factor() const override
Get the detector-specific automatic startup multiplier.
LightweightDetector(const LightweightDetector &)=delete
const char * get_name() const override
Get detector name for logging.
void advance_missing_slots(uint32_t count) override
Advance packet-indexed feature rings for absent temporal slots.
void reset() override
Reset detector state.
bool set_adaptive_threshold(float threshold) override
Apply a detector-specific startup-calibrated threshold.
LightweightDetector & operator=(LightweightDetector &&other) noexcept=default
void process_packet(const int8_t *csi_data, size_t csi_len, const uint8_t *selected_subcarriers=nullptr, uint8_t num_subcarriers=0, int8_t rssi_dbm=INT8_MIN) override
Process a CSI packet and update internal state.
LightweightDetector & operator=(const LightweightDetector &)=delete
void clear_buffer() override
Clear turbulence buffer (cold restart).
bool is_ready() const override
Check if detector is ready (buffer filled).
constexpr uint8_t LIGHTWEIGHT_SETTLE_BLOCKS
constexpr float LIGHTWEIGHT_DEFAULT_THRESHOLD
constexpr float LIGHTWEIGHT_INTERCEPT
constexpr float LIGHTWEIGHT_TURB_IQR_OVER_MEAN_AGGR_SCALE
constexpr uint16_t DETECTOR_DEFAULT_WINDOW_SIZE
constexpr uint8_t LIGHTWEIGHT_STARTUP_SAMPLE_LIMIT
constexpr float LIGHTWEIGHT_STARTUP_STRENGTH
constexpr float LIGHTWEIGHT_TURB_IQR_OVER_MEAN_AGGR_WEIGHT
constexpr float LIGHTWEIGHT_SETTLE_MARGIN_LOGITS
constexpr float LIGHTWEIGHT_AUTOCORR_WEIGHT
constexpr uint8_t HAMPEL_TURBULENCE_WINDOW_DEFAULT
constexpr float LIGHTWEIGHT_STARTUP_THRESHOLD_FACTOR
constexpr float LIGHTWEIGHT_STARTUP_QUANTILE
constexpr float HAMPEL_TURBULENCE_THRESHOLD_DEFAULT
constexpr float LIGHTWEIGHT_TURB_IQR_OVER_MEAN_AGGR_CENTER
constexpr uint8_t LIGHTWEIGHT_SETTLE_BLOCK_EVALUATIONS
constexpr float LIGHTWEIGHT_TRAIN_IDLE_Q95_LOGIT
constexpr float LOWPASS_CUTOFF_DEFAULT
constexpr float LIGHTWEIGHT_AUTOCORR_SCALE
constexpr float LIGHTWEIGHT_AUTOCORR_CENTER