ESPectre SDK 2.8.0-280-gac7af68
Wi-Fi CSI motion sensing for ESP32 firmware
Loading...
Searching...
No Matches
base_detector.h
Go to the documentation of this file.
1/*
2 * ESPectre - Base Detector
3 *
4 * Abstract base class for motion detection algorithms.
5 * Provides shared turbulence buffer management and filtering.
6 *
7 * Author: Francesco Pace <francesco.pace@gmail.com>
8 * SPDX-License-Identifier: GPL-3.0-only
9 * Commercial licensing available under separate agreement; see LICENSING.md.
10 */
11#pragma once
12
13#include <cstddef>
14#include <cstdint>
15#include <algorithm>
16#include "detector_types.h"
17#include "detector_limits.h"
18#include "filters.h"
19#include "utils.h"
20
21namespace espectre {
22
23// ============================================================================
24// BASE DETECTOR CLASS
25// ============================================================================
26
27/**
28 * Abstract base class for motion detection algorithms
29 *
30 * Provides shared functionality:
31 * - Turbulence buffer management (circular buffer)
32 * - Hampel and low-pass filtering
33 * - CSI processing and spatial turbulence calculation
34 *
35 * Subclasses must implement:
36 * - update_state(): detection algorithm logic
37 * - get_motion_metric(): primary detection metric
38 * - get_threshold() / set_threshold(): threshold management
39 * - get_name(): detector name for logging
40 */
42public:
43 /**
44 * Constructor
45 *
46 * @param window_size Buffer window size (10-200 packets)
47 */
48 explicit BaseDetector(uint16_t window_size = DETECTOR_DEFAULT_WINDOW_SIZE);
49
50 virtual ~BaseDetector();
51
52 // Move semantics (Rule of Five - we manage raw pointer)
53 BaseDetector(BaseDetector&& other) noexcept;
55
56 // Disable copy (raw pointer ownership)
57 BaseDetector(const BaseDetector&) = delete;
59
60 // ========================================================================
61 // VIRTUAL INTERFACE (implemented in base)
62 // ========================================================================
63
64 /**
65 * Process a CSI packet and update internal state
66 *
67 * Calculates spatial turbulence from CSI data, applies filtering,
68 * and stores in circular buffer.
69 *
70 * @param csi_data Raw CSI data (I/Q interleaved)
71 * @param csi_len Length of CSI data
72 * @param selected_subcarriers Array of subcarrier indices
73 * @param num_subcarriers Number of selected subcarriers
74 * @param rssi_dbm Link RSSI for this packet, or INT8_MIN when unknown
75 */
76 virtual void process_packet(const int8_t* csi_data, size_t csi_len,
77 const uint8_t* selected_subcarriers = nullptr,
78 uint8_t num_subcarriers = 0,
79 int8_t rssi_dbm = INT8_MIN);
80
81 /** Supply the monotonic arrival timestamp consumed by time-binned features. */
82 void set_packet_timestamp_us(uint64_t timestamp_us) {
83 packet_timestamp_us_ = timestamp_us;
85 }
86
87 /**
88 * Reset detector state
89 *
90 * Resets state machine but preserves buffer ("warm" restart).
91 */
92 virtual void reset();
93
94 /**
95 * Get current motion state
96 */
97 virtual MotionState get_state() const { return state_; }
98
99 /**
100 * Check if detector is ready (buffer filled)
101 */
102 virtual bool is_ready() const {
103 return buffer_count_ >= window_size_ &&
105 }
106
107 /** Advance packet-indexed feature rings for absent temporal slots. */
108 virtual void advance_missing_slots(uint32_t count);
109
110 /** Set the valid-slot occupancy floor used by `is_ready()`. */
111 void set_minimum_valid_samples(uint16_t count) {
112 minimum_valid_samples_ = std::max<uint16_t>(
113 1U, std::min<uint16_t>(count, window_size_));
114 }
115
116 /**
117 * Get total packets processed
118 */
119 virtual uint32_t get_total_packets() const { return total_packets_; }
120
121 // ========================================================================
122 // PURE VIRTUAL INTERFACE (must be implemented by subclasses)
123 // ========================================================================
124
125 /**
126 * Update state machine (call at publish interval)
127 *
128 * Subclasses implement their detection algorithm here.
129 */
130 virtual void update_state() = 0;
131
132 /**
133 * Get current motion metric value
134 *
135 * Not virtual: every detector reported the same member through an identical
136 * accessor, and the two copies drifted on when they cleared it. Subclasses
137 * assign `current_metric_` at the end of their `update_state()` instead.
138 *
139 * @return Primary metric (classic motion metric, ML probability, etc.)
140 */
141 float get_motion_metric() const { return current_metric_; }
142
143 /**
144 * Set detection threshold
145 *
146 * @param threshold New threshold value
147 * @return true if value was accepted
148 */
149 virtual bool set_threshold(float threshold) = 0;
150
151 /** Apply a detector-specific startup-calibrated threshold. */
152 virtual bool set_adaptive_threshold(float threshold) { return set_threshold(threshold); }
153
154 /**
155 * Get current threshold
156 */
157 virtual float get_threshold() const = 0;
158
159 /**
160 * Get detector name for logging
161 */
162 virtual const char* get_name() const = 0;
163
164 /**
165 * Get the detector-specific automatic startup multiplier.
166 *
167 * threshold = threshold_metric x factor. Matches the Python
168 * runtime's detector STARTUP_THRESHOLD_FACTOR convention, where
169 * `threshold_metric` comes from the shared startup calibrator.
170 */
171 virtual float get_startup_threshold_factor() const { return 1.3f; }
172
173 /**
174 * Whether startup calibration uses the consistency gate (threshold.h)
175 *
176 * Enabled only for detectors with a tight quiet floor (l1_delta).
177 * Matches the Python runtime's detector STARTUP_GATE convention.
178 */
179 virtual bool startup_gate_enabled() const { return false; }
180
181 /** Hook called immediately before startup calibration begins. */
183
184 /**
185 * Hook called when startup calibration completes successfully.
186 *
187 * Detectors can freeze session-specific state here before the runtime
188 * performs its warm clear between calibration and steady-state detection.
189 */
191
192 // ========================================================================
193 // FILTER CONFIGURATION
194 // ========================================================================
195
196 /**
197 * Configure low-pass filter
198 *
199 * @param enabled Whether to enable the filter
200 * @param cutoff_hz Cutoff frequency (5.0-20.0 Hz)
201 */
202 virtual void configure_lowpass(
203 bool enabled, float cutoff_hz = LOWPASS_CUTOFF_DEFAULT);
204
205 /**
206 * Configure Hampel filter
207 *
208 * @param enabled Whether to enable the filter
209 * @param window_size Window size (3-11)
210 * @param threshold MAD multiplier threshold
211 */
212 virtual void configure_hampel(
213 bool enabled, uint8_t window_size = HAMPEL_TURBULENCE_WINDOW_DEFAULT,
214 float threshold = HAMPEL_TURBULENCE_THRESHOLD_DEFAULT);
215
216 /**
217 * Clear turbulence buffer (cold restart)
218 *
219 * Virtual so detectors with additional state (e.g. L1-Delta profile
220 * rings) can extend the cold clear.
221 */
222 virtual void clear_buffer();
223
224 // ========================================================================
225 // BUFFER ACCESSORS (for subclasses and feature extraction)
226 // ========================================================================
227
228 /**
229 * Get turbulence buffer pointer
230 */
231 const float* get_turbulence_buffer() const { return turbulence_buffer_; }
232
233 /**
234 * Get number of valid samples in buffer
235 */
236 uint16_t get_buffer_count() const { return buffer_count_; }
237 uint16_t get_valid_buffer_count() const { return valid_buffer_count_; }
238
239 /**
240 * Get configured window size
241 */
242 uint16_t get_window_size() const { return window_size_; }
243
244 /**
245 * Get last turbulence value
246 */
247 float get_last_turbulence() const;
248
249 /**
250 * Check if low-pass filter is enabled
251 */
252 bool is_lowpass_enabled() const { return lowpass_state_.enabled; }
253
254 /**
255 * Check if Hampel filter is enabled
256 */
257 bool is_hampel_enabled() const { return hampel_state_.enabled; }
258
259protected:
260 /**
261 * Drop the last evaluation result.
262 *
263 * Anything that invalidates the window must also invalidate what was
264 * derived from it, or the next publish ships a metric computed from
265 * samples the detector no longer holds. Owned here so a detector cannot
266 * clear one half and forget the other.
267 */
272
273 void process_amplitudes(const float* amplitudes, uint8_t count);
274
275 uint64_t packet_timestamp_us_or(uint64_t fallback) const {
277 }
278
279 /**
280 * Add turbulence value to buffer (with filtering)
281 */
282 void add_turbulence_to_buffer(float turbulence);
283
284 /**
285 * Allocate a zeroed float buffer on the heap.
286 *
287 * Shared by the detectors so no feature helper puts a window-sized
288 * array on the CSI callback stack.
289 *
290 * @return nullptr when count is 0 or the allocation fails
291 */
292 static float* alloc_zeroed_floats(uint16_t count);
293
294 /**
295 * View the turbulence ring in chronological order.
296 *
297 * Returns the ring itself while it is still filling (already in order),
298 * and the base-owned reorder buffer once it wraps. Returns nullptr when
299 * there is nothing to read or the reorder buffer could not be allocated.
300 *
301 * @param count Receives the number of valid samples
302 */
303 const float* ordered_turbulence(uint16_t& count) const;
304
305 // Buffer state
312 uint16_t window_size_;
313
314 // Motion state. `current_metric_` is what get_motion_metric() reports and
315 // what the runtime publishes; clear_evaluation_state_() owns dropping both.
322
323 // Filters
326
327};
328
329} // namespace espectre
uint16_t get_buffer_count() const
Get number of valid samples in buffer.
virtual 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)
Process a CSI packet and update internal state.
static float * alloc_zeroed_floats(uint16_t count)
Allocate a zeroed float buffer on the heap.
virtual void on_startup_calibration_begin()
Hook called immediately before startup calibration begins.
BaseDetector & operator=(const BaseDetector &)=delete
BaseDetector(const BaseDetector &)=delete
virtual void update_state()=0
Update state machine (call at publish interval).
const float * ordered_turbulence(uint16_t &count) const
View the turbulence ring in chronological order.
virtual uint32_t get_total_packets() const
Get total packets processed.
BaseDetector & operator=(BaseDetector &&other) noexcept
BaseDetector(BaseDetector &&other) noexcept
virtual void advance_missing_slots(uint32_t count)
Advance packet-indexed feature rings for absent temporal slots.
virtual void configure_lowpass(bool enabled, float cutoff_hz=LOWPASS_CUTOFF_DEFAULT)
Configure low-pass filter.
virtual bool set_adaptive_threshold(float threshold)
Apply a detector-specific startup-calibrated threshold.
uint16_t get_window_size() const
Get configured window size.
virtual void clear_buffer()
Clear turbulence buffer (cold restart).
lowpass_filter_state_t lowpass_state_
uint16_t get_valid_buffer_count() const
virtual MotionState get_state() const
Get current motion state.
uint64_t packet_timestamp_us_or(uint64_t fallback) const
virtual bool is_ready() const
Check if detector is ready (buffer filled).
void add_turbulence_to_buffer(float turbulence)
Add turbulence value to buffer (with filtering).
virtual bool set_threshold(float threshold)=0
Set detection threshold.
void set_minimum_valid_samples(uint16_t count)
Set the valid-slot occupancy floor used by is_ready().
virtual void configure_hampel(bool enabled, uint8_t window_size=HAMPEL_TURBULENCE_WINDOW_DEFAULT, float threshold=HAMPEL_TURBULENCE_THRESHOLD_DEFAULT)
Configure Hampel filter.
bool is_lowpass_enabled() const
Check if low-pass filter is enabled.
const float * get_turbulence_buffer() const
Get turbulence buffer pointer.
virtual float get_threshold() const =0
Get current threshold.
virtual float get_startup_threshold_factor() const
Get the detector-specific automatic startup multiplier.
float get_motion_metric() const
Get current motion metric value.
bool is_hampel_enabled() const
Check if Hampel filter is enabled.
BaseDetector(uint16_t window_size=DETECTOR_DEFAULT_WINDOW_SIZE)
Constructor.
void process_amplitudes(const float *amplitudes, uint8_t count)
void set_packet_timestamp_us(uint64_t timestamp_us)
Supply the monotonic arrival timestamp consumed by time-binned features.
virtual bool startup_gate_enabled() const
Whether startup calibration uses the consistency gate (threshold.h).
void clear_evaluation_state_()
Drop the last evaluation result.
hampel_filter_state_t hampel_state_
float get_last_turbulence() const
Get last turbulence value.
virtual void on_startup_calibration_complete()
Hook called when startup calibration completes successfully.
virtual const char * get_name() const =0
Get detector name for logging.
virtual void reset()
Reset detector state.
constexpr uint16_t DETECTOR_DEFAULT_WINDOW_SIZE
constexpr uint8_t HAMPEL_TURBULENCE_WINDOW_DEFAULT
MotionState
Debounced detector state.
constexpr float HAMPEL_TURBULENCE_THRESHOLD_DEFAULT
hampel_turbulence_state_t hampel_filter_state_t
Definition filters.h:52
constexpr float LOWPASS_CUTOFF_DEFAULT