ESPectre SDK 2.8.0-280-gac7af68
Wi-Fi CSI motion sensing for ESP32 firmware
Loading...
Searching...
No Matches
threshold.h
Go to the documentation of this file.
1/*
2 * ESPectre - Adaptive Threshold Calculator
3 *
4 * Calculates adaptive threshold from calibration baseline values.
5 * Called after calibration to compute the detection threshold.
6 *
7 * Startup threshold calibration is automatic. Detectors may apply their own
8 * session adaptation to the shared calibration metric.
9 *
10 * The default Lightweight path is motion-first with an internal quiet-first
11 * fallback. Successful motion-first calibration can finish before the nominal
12 * budget; otherwise the calibrator falls back to the quiet-first gate on the
13 * same observed metrics and still completes within the configured packet
14 * budget. Keep the semantics aligned with src/python/micro_espectre/threshold.py.
15 *
16 * Author: Francesco Pace <francesco.pace@gmail.com>
17 * SPDX-License-Identifier: GPL-3.0-only
18 * Commercial licensing available under separate agreement; see LICENSING.md.
19 */
20#pragma once
21
22#include <algorithm>
23#include <cmath>
24#include <cstdint>
25
26namespace espectre {
27
28// =============================================================================
29// Segmentation Threshold Constants
30// =============================================================================
31
32// Note: Window size constants are defined in base_detector.h:
33
34constexpr float SEGMENTATION_DEFAULT_THRESHOLD = 1.0f;
35// Min threshold lowered to support CV normalization (std/mean produces smaller values)
36constexpr float SEGMENTATION_MIN_THRESHOLD = 1e-9f;
37constexpr float SEGMENTATION_MAX_THRESHOLD = 10.0f;
38
39/**
40 * Validate threshold value against finite/range constraints.
41 */
42inline bool is_valid_threshold(float threshold, float min_threshold, float max_threshold) {
43 return std::isfinite(threshold) &&
44 threshold >= min_threshold &&
45 threshold <= max_threshold;
46}
47
48/**
49 * Clamp threshold to [min, max] and recover non-finite values.
50 */
51inline float clamp_threshold(float threshold, float min_threshold, float max_threshold) {
52 if (!std::isfinite(threshold)) {
53 return min_threshold;
54 }
55 if (threshold < min_threshold) {
56 return min_threshold;
57 }
58 if (threshold > max_threshold) {
59 return max_threshold;
60 }
61 return threshold;
62}
63
64// Default startup multiplier for detectors that use the shared metric.
65constexpr float DEFAULT_ADAPTIVE_FACTOR = 1.3f;
66
67// Startup calibration consistency gate (benchmark-tuned on the paired
68// datasets; keep aligned with src/python/micro_espectre/threshold.py).
69constexpr uint8_t STARTUP_GATE_CHUNKS = 6;
70constexpr float STARTUP_GATE_SPREAD_RATIO = 1.10f;
71constexpr float STARTUP_GATE_ANCHOR_RATIO = 1.5f;
72constexpr uint8_t STARTUP_MOTION_CHUNK_SIZE = 25;
73constexpr uint8_t STARTUP_MOTION_MIN_QUIET_CHUNKS = 2;
74constexpr uint8_t STARTUP_MOTION_CONFIRM_CHUNKS = 2;
76constexpr float STARTUP_QUIET_STABILITY_RATIO = 1.20f;
77constexpr float STARTUP_MOTION_TRIGGER_RATIO = 1.80f;
78constexpr float STARTUP_QUIET_RETURN_RATIO = 1.25f;
79constexpr float STARTUP_MOTION_GAP_RATIO = 1.35f;
80constexpr float STARTUP_NO_MOTION_FALLBACK_MARGIN = 1.03f;
81constexpr uint8_t STARTUP_MOTION_MAX_LEVELS = 40;
82
83/**
84 * Startup threshold calibrator with a motion-first primary path and an
85 * internal quiet-first fallback. The fallback keeps the existing gated ring of
86 * chunk maxima, but completion never exceeds the configured budget.
87 */
89 public:
90 void begin(uint16_t target_packets, bool gate_enabled) {
91 target_packets_ = target_packets > 0 ? target_packets : 1;
92 gate_enabled_ = gate_enabled;
93 packet_count_ = 0;
94 ready_packet_count_ = 0;
95 has_value_ = false;
96 max_motion_metric_ = 0.0f;
97 gate_accepted_ = false;
98 chunk_size_ = 0;
99 chunk_count_ = 0;
100 chunk_max_ = 0.0f;
101 ring_count_ = 0;
102 ring_next_ = 0;
103 min_chunk_max_ = 0.0f;
104 discarded_chunk_max_ = 0.0f;
105 has_discarded_chunk_ = false;
106 motion_chunk_sum_ = 0.0f;
107 motion_chunk_max_ = 0.0f;
108 motion_chunk_count_ = 0;
109 bootstrap_count_ = 0;
110 quiet_level_count_ = 0;
111 motion_level_count_ = 0;
112 post_quiet_level_count_ = 0;
113 quiet_anchor_ready_ = false;
114 motion_confirmed_ = false;
115 motion_accepted_ = false;
116 phase_ = Phase::SEEK_MOTION;
117 consecutive_motion_chunks_ = 0;
118 consecutive_post_quiet_chunks_ = 0;
119 }
120
121 /**
122 * Consume one evaluated detector step representing one or more packets.
123 *
124 * @param detector_ready Whether the detector metric window is full
125 * @param motion_metric Current detector motion metric
126 * @param packet_weight Number of processed packets represented by the metric
127 */
128 void observe(bool detector_ready, float motion_metric, uint16_t packet_weight = 1U) {
129 // The weight is folded in one step rather than replayed packet by packet.
130 // Replaying it re-evaluated the chunk guards on every repetition, so a
131 // chunk boundary landing inside one evaluation split differently here than
132 // in the Python calibrator and the two produced different thresholds. See
133 // threshold.py, which this mirrors.
134 const uint32_t remaining = target_packets_ > packet_count_ ? target_packets_ - packet_count_ : 0U;
135 const uint32_t weight = std::min<uint32_t>(std::max<uint16_t>(packet_weight, 1U), remaining);
136 if (weight == 0U) {
137 return;
138 }
139 const uint32_t initial_remaining = remaining;
140 packet_count_ += weight;
141 if (!detector_ready) {
142 return;
143 }
144 ready_packet_count_ += weight;
145 if (!has_value_ || motion_metric > max_motion_metric_) {
146 max_motion_metric_ = motion_metric;
147 }
148 has_value_ = true;
149 if (gate_enabled_ && !gate_accepted_) {
150 observe_gate_metric_(motion_metric, weight, initial_remaining);
151 }
152 if (gate_enabled_ && !motion_accepted_ && packet_count_ <= target_packets_) {
153 observe_motion_chunk_(motion_metric, weight);
154 }
155 if (gate_enabled_ && !gate_accepted_ && packet_count_ >= target_packets_ &&
156 chunk_count_ > 0 && ring_count_ < STARTUP_GATE_CHUNKS) {
157 close_gate_chunk_();
158 }
159 }
160
161 /// True once motion-first succeeds early or the startup budget is spent.
162 bool is_complete() const {
163 return motion_accepted_ || packet_count_ >= target_packets_;
164 }
165
166 bool is_successful() const { return motion_accepted_ || has_value_; }
167 bool gate_accepted() const { return gate_accepted_; }
168 uint32_t packet_count() const { return packet_count_; }
169 uint16_t target_packets() const { return target_packets_; }
170 uint32_t ready_packet_count() const { return ready_packet_count_; }
171
172 /// Metric the threshold formula (x factor) is applied to.
173 float threshold_metric() const {
174 if (motion_accepted_) {
175 return motion_threshold_metric_();
176 }
177 if (!gate_enabled_ || ring_count_ == 0) {
178 return has_value_ ? max_motion_metric_ : 0.0f;
179 }
180 if (gate_accepted_) {
181 float metric = ring_max_();
182 if (has_discarded_chunk_ &&
183 discarded_chunk_max_ <= STARTUP_GATE_ANCHOR_RATIO * ring_median_()) {
184 metric = std::max(metric, discarded_chunk_max_);
185 }
186 return metric;
187 }
188 if (quiet_anchor_ready_ && motion_level_count_ > 0) {
189 const float quiet_ceiling = quiet_ceiling_();
190 const float anchored_cap = STARTUP_GATE_ANCHOR_RATIO * quiet_ceiling;
191 return std::max(quiet_ceiling, std::min(ring_median_(), anchored_cap));
192 }
193 if (!motion_confirmed_) {
194 return STARTUP_NO_MOTION_FALLBACK_MARGIN * ring_max_();
195 }
196 return ring_median_();
197 }
198
199 /// Statistic name for threshold logging.
200 const char* statistic_name() const {
201 if (motion_accepted_) {
202 return "motion gap midpoint";
203 }
204 if (!gate_enabled_ || ring_count_ == 0) {
205 return "max";
206 }
207 if (gate_accepted_ || !motion_confirmed_) {
208 if (!gate_accepted_ && quiet_anchor_ready_ && motion_level_count_ > 0) {
209 return "quiet anchor";
210 }
211 return "gated max";
212 }
213 if (quiet_anchor_ready_ && motion_level_count_ > 0) {
214 return "quiet anchor";
215 }
216 return "gated median";
217 }
218
219 const char* phase_label() const {
220 if (motion_accepted_) {
221 return "COMPLETE";
222 }
223 if (packet_count_ >= target_packets_) {
224 return "FALLBACK";
225 }
226 switch (phase_) {
227 case Phase::SEEK_MOTION:
228 return "SEEK_MOTION";
229 case Phase::SEEK_POST_MOTION_QUIET:
230 return "SEEK_POST_QUIET";
231 default:
232 return "CALIBRATING";
233 }
234 }
235
236 private:
237 enum class Phase {
238 SEEK_MOTION,
239 SEEK_POST_MOTION_QUIET,
240 COMPLETE,
241 };
242
243 /**
244 * @param initial_remaining Budget left before this observation was counted,
245 * which is what sizes the chunks. Measuring it after the weight was
246 * added would shrink every chunk by the weight.
247 */
248 void observe_gate_metric_(float metric, uint32_t weight, uint32_t initial_remaining) {
249 if (chunk_size_ == 0) {
250 chunk_size_ = std::max<uint32_t>(1U, initial_remaining / STARTUP_GATE_CHUNKS);
251 }
252
253 uint32_t remaining_weight = weight;
254 while (remaining_weight > 0U && !gate_accepted_) {
255 if (chunk_count_ == 0 || metric > chunk_max_) {
256 chunk_max_ = metric;
257 }
258 const uint32_t available = chunk_size_ > chunk_count_ ? chunk_size_ - chunk_count_ : 0U;
259 const uint32_t take = std::min(remaining_weight, available);
260 chunk_count_ += take;
261 remaining_weight -= take;
262 if (chunk_count_ >= chunk_size_) {
263 close_gate_chunk_();
264 }
265 }
266 }
267
268 void close_gate_chunk_() {
269 if (ring_count_ == STARTUP_GATE_CHUNKS) {
270 const float discarded = ring_[ring_next_];
271 if (!has_discarded_chunk_ || discarded > discarded_chunk_max_) {
272 discarded_chunk_max_ = discarded;
273 has_discarded_chunk_ = true;
274 }
275 }
276 ring_[ring_next_] = chunk_max_;
277 ring_next_ = (ring_next_ + 1) % STARTUP_GATE_CHUNKS;
278 if (ring_count_ < STARTUP_GATE_CHUNKS) {
279 ring_count_++;
280 }
281 if (ring_count_ == 1 || chunk_max_ < min_chunk_max_) {
282 min_chunk_max_ = chunk_max_;
283 }
284 chunk_count_ = 0;
285 chunk_max_ = 0.0f;
286
287 if (ring_count_ >= STARTUP_GATE_CHUNKS && gate_ok_()) {
288 gate_accepted_ = true;
289 }
290 }
291
292 void observe_motion_chunk_(float metric, uint32_t weight) {
293 uint32_t remaining_weight = weight;
294 while (remaining_weight > 0U && !motion_accepted_) {
295 if (motion_chunk_count_ == 0 || metric > motion_chunk_max_) {
296 motion_chunk_max_ = metric;
297 }
298 const uint32_t available = STARTUP_MOTION_CHUNK_SIZE > motion_chunk_count_
299 ? STARTUP_MOTION_CHUNK_SIZE - motion_chunk_count_
300 : 0U;
301 const uint32_t take = std::min(remaining_weight, available);
302 motion_chunk_sum_ += metric * static_cast<float>(take);
303 motion_chunk_count_ = static_cast<uint8_t>(motion_chunk_count_ + take);
304 remaining_weight -= take;
305 if (motion_chunk_count_ < STARTUP_MOTION_CHUNK_SIZE) {
306 continue;
307 }
308
309 const float level = motion_chunk_sum_ / static_cast<float>(motion_chunk_count_);
310 const float peak = motion_chunk_max_;
311 consume_closed_motion_chunk_(level, peak);
312 motion_chunk_sum_ = 0.0f;
313 motion_chunk_max_ = 0.0f;
314 motion_chunk_count_ = 0;
315 }
316 }
317
318 void consume_closed_motion_chunk_(float level, float peak) {
319 if (!quiet_anchor_ready_) {
320 if (bootstrap_count_ == STARTUP_MOTION_MIN_QUIET_CHUNKS) {
321 bootstrap_levels_[0] = bootstrap_levels_[1];
322 bootstrap_count_--;
323 }
324 bootstrap_levels_[bootstrap_count_] = level;
325 bootstrap_count_++;
326
327 if (bootstrap_count_ >= STARTUP_MOTION_MIN_QUIET_CHUNKS &&
328 levels_are_stable_(bootstrap_levels_, bootstrap_count_)) {
329 quiet_anchor_ready_ = true;
330 quiet_level_count_ = bootstrap_count_;
331 for (uint8_t i = 0; i < bootstrap_count_; i++) {
332 quiet_levels_[i] = bootstrap_levels_[i];
333 }
334 }
335 return;
336 }
337
338 const float quiet_ref = std::max(quiet_reference_(), 1e-9f);
339 const float motion_ratio = level / quiet_ref;
340 const float peak_ratio = peak / quiet_ref;
341
342 if (!motion_confirmed_) {
343 if (motion_ratio >= STARTUP_MOTION_TRIGGER_RATIO &&
344 peak_ratio >= STARTUP_MOTION_TRIGGER_RATIO) {
345 append_motion_level_(level);
346 consecutive_motion_chunks_++;
347 if (consecutive_motion_chunks_ >= STARTUP_MOTION_CONFIRM_CHUNKS) {
348 motion_confirmed_ = true;
349 phase_ = Phase::SEEK_POST_MOTION_QUIET;
350 consecutive_post_quiet_chunks_ = 0;
351 post_quiet_level_count_ = 0;
352 }
353 return;
354 }
355
356 if (motion_ratio <= STARTUP_QUIET_RETURN_RATIO) {
357 append_quiet_level_(level);
358 }
359 consecutive_motion_chunks_ = 0;
360 return;
361 }
362
363 if (motion_ratio <= STARTUP_QUIET_RETURN_RATIO) {
364 append_post_quiet_level_(level);
365 consecutive_post_quiet_chunks_++;
366 if (consecutive_post_quiet_chunks_ >= STARTUP_POST_MOTION_QUIET_CHUNKS &&
367 motion_gap_ok_()) {
368 motion_accepted_ = true;
369 phase_ = Phase::COMPLETE;
370 }
371 return;
372 }
373
374 if (motion_ratio >= STARTUP_MOTION_TRIGGER_RATIO &&
375 peak_ratio >= STARTUP_MOTION_TRIGGER_RATIO) {
376 append_motion_level_(level);
377 consecutive_post_quiet_chunks_ = 0;
378 phase_ = Phase::SEEK_POST_MOTION_QUIET;
379 return;
380 }
381
382 consecutive_post_quiet_chunks_ = 0;
383 }
384
385 bool levels_are_stable_(const float* values, uint8_t count) const {
386 if (count == 0) {
387 return false;
388 }
389 float low = values[0];
390 float high = values[0];
391 for (uint8_t i = 1; i < count; i++) {
392 low = std::min(low, values[i]);
393 high = std::max(high, values[i]);
394 }
395 if (low <= 0.0f) {
396 return high <= 1e-9f;
397 }
398 return high <= STARTUP_QUIET_STABILITY_RATIO * low;
399 }
400
401 float quiet_reference_() const {
402 if (quiet_level_count_ == 0) {
403 return 0.0f;
404 }
405 float ordered[STARTUP_GATE_CHUNKS];
406 std::copy(quiet_levels_, quiet_levels_ + quiet_level_count_, ordered);
407 std::sort(ordered, ordered + quiet_level_count_);
408 if (quiet_level_count_ % 2 != 0) {
409 return ordered[quiet_level_count_ / 2];
410 }
411 return 0.5f * (ordered[quiet_level_count_ / 2 - 1] + ordered[quiet_level_count_ / 2]);
412 }
413
414 float motion_floor_() const {
415 if (motion_level_count_ == 0) {
416 return 0.0f;
417 }
418 float ordered[STARTUP_MOTION_MAX_LEVELS];
419 std::copy(motion_levels_, motion_levels_ + motion_level_count_, ordered);
420 std::sort(ordered, ordered + motion_level_count_);
421 const uint8_t index = std::min<uint8_t>(
422 motion_level_count_ - 1,
423 static_cast<uint8_t>(0.10f * static_cast<float>(motion_level_count_)));
424 return ordered[index];
425 }
426
427 float quiet_ceiling_() const {
428 float quiet_ceiling = 0.0f;
429 bool has_quiet = false;
430 for (uint8_t i = 0; i < quiet_level_count_; i++) {
431 quiet_ceiling = has_quiet ? std::max(quiet_ceiling, quiet_levels_[i]) : quiet_levels_[i];
432 has_quiet = true;
433 }
434 for (uint8_t i = 0; i < post_quiet_level_count_; i++) {
435 quiet_ceiling = has_quiet ? std::max(quiet_ceiling, post_quiet_levels_[i]) : post_quiet_levels_[i];
436 has_quiet = true;
437 }
438 return has_quiet ? quiet_ceiling : 0.0f;
439 }
440
441 float motion_threshold_metric_() const {
442 const float motion_floor = motion_floor_();
443 const float quiet_ceiling = quiet_ceiling_();
444 if (motion_floor <= quiet_ceiling) {
445 return motion_floor;
446 }
447 return 0.5f * (motion_floor + quiet_ceiling);
448 }
449
450 bool motion_gap_ok_() const {
451 if (motion_level_count_ == 0) {
452 return false;
453 }
454 const float quiet_ceiling = quiet_ceiling_();
455 if (quiet_ceiling <= 0.0f) {
456 return false;
457 }
458 return motion_floor_() > STARTUP_MOTION_GAP_RATIO * quiet_ceiling;
459 }
460
461 void append_quiet_level_(float value) {
462 if (quiet_level_count_ < STARTUP_GATE_CHUNKS) {
463 quiet_levels_[quiet_level_count_++] = value;
464 return;
465 }
466 for (uint8_t i = 1; i < STARTUP_GATE_CHUNKS; i++) {
467 quiet_levels_[i - 1] = quiet_levels_[i];
468 }
469 quiet_levels_[STARTUP_GATE_CHUNKS - 1] = value;
470 }
471
472 void append_motion_level_(float value) {
473 if (motion_level_count_ < STARTUP_MOTION_MAX_LEVELS) {
474 motion_levels_[motion_level_count_++] = value;
475 return;
476 }
477 for (uint8_t i = 1; i < STARTUP_MOTION_MAX_LEVELS; i++) {
478 motion_levels_[i - 1] = motion_levels_[i];
479 }
480 motion_levels_[STARTUP_MOTION_MAX_LEVELS - 1] = value;
481 }
482
483 void append_post_quiet_level_(float value) {
484 if (post_quiet_level_count_ < STARTUP_GATE_CHUNKS) {
485 post_quiet_levels_[post_quiet_level_count_++] = value;
486 return;
487 }
488 for (uint8_t i = 1; i < STARTUP_GATE_CHUNKS; i++) {
489 post_quiet_levels_[i - 1] = post_quiet_levels_[i];
490 }
491 post_quiet_levels_[STARTUP_GATE_CHUNKS - 1] = value;
492 }
493
494 bool gate_ok_() const {
495 const float ring_max = ring_max_();
496 const float ring_median = ring_median_();
497 if (ring_max > STARTUP_GATE_SPREAD_RATIO * ring_median) {
498 return false;
499 }
500 if (ring_median > STARTUP_GATE_ANCHOR_RATIO * min_chunk_max_) {
501 return false;
502 }
503 return true;
504 }
505
506 float ring_max_() const {
507 float value = ring_[0];
508 for (uint8_t i = 1; i < ring_count_; i++) {
509 value = std::max(value, ring_[i]);
510 }
511 return value;
512 }
513
514 float ring_median_() const {
515 float ordered[STARTUP_GATE_CHUNKS];
516 std::copy(ring_, ring_ + ring_count_, ordered);
517 std::sort(ordered, ordered + ring_count_);
518 if (ring_count_ % 2 != 0) {
519 return ordered[ring_count_ / 2];
520 }
521 return 0.5f * (ordered[ring_count_ / 2 - 1] + ordered[ring_count_ / 2]);
522 }
523
524 uint16_t target_packets_{1};
525 bool gate_enabled_{false};
526 uint32_t packet_count_{0};
527 uint32_t ready_packet_count_{0};
528 bool has_value_{false};
529 float max_motion_metric_{0.0f};
530 bool gate_accepted_{false};
531 uint32_t chunk_size_{0};
532 uint32_t chunk_count_{0};
533 float chunk_max_{0.0f};
534 float ring_[STARTUP_GATE_CHUNKS] = {};
535 uint8_t ring_count_{0};
536 uint8_t ring_next_{0};
537 float min_chunk_max_{0.0f};
538 float discarded_chunk_max_{0.0f};
539 bool has_discarded_chunk_{false};
540 float motion_chunk_sum_{0.0f};
541 float motion_chunk_max_{0.0f};
542 uint8_t motion_chunk_count_{0};
543 float bootstrap_levels_[STARTUP_MOTION_MIN_QUIET_CHUNKS] = {};
544 uint8_t bootstrap_count_{0};
545 float quiet_levels_[STARTUP_GATE_CHUNKS] = {};
546 uint8_t quiet_level_count_{0};
547 float motion_levels_[STARTUP_MOTION_MAX_LEVELS] = {};
548 uint8_t motion_level_count_{0};
549 float post_quiet_levels_[STARTUP_GATE_CHUNKS] = {};
550 uint8_t post_quiet_level_count_{0};
551 bool quiet_anchor_ready_{false};
552 bool motion_confirmed_{false};
553 bool motion_accepted_{false};
554 Phase phase_{Phase::SEEK_MOTION};
555 uint8_t consecutive_motion_chunks_{0};
556 uint8_t consecutive_post_quiet_chunks_{0};
557};
558
559} // namespace espectre
Startup threshold calibrator with a motion-first primary path and an internal quiet-first fallback.
Definition threshold.h:88
void begin(uint16_t target_packets, bool gate_enabled)
Definition threshold.h:90
const char * statistic_name() const
Statistic name for threshold logging.
Definition threshold.h:200
void observe(bool detector_ready, float motion_metric, uint16_t packet_weight=1U)
Consume one evaluated detector step representing one or more packets.
Definition threshold.h:128
bool is_complete() const
True once motion-first succeeds early or the startup budget is spent.
Definition threshold.h:162
float threshold_metric() const
Metric the threshold formula (x factor) is applied to.
Definition threshold.h:173
const char * phase_label() const
Definition threshold.h:219
constexpr float STARTUP_MOTION_TRIGGER_RATIO
Definition threshold.h:77
constexpr float STARTUP_QUIET_STABILITY_RATIO
Definition threshold.h:76
constexpr uint8_t STARTUP_POST_MOTION_QUIET_CHUNKS
Definition threshold.h:75
constexpr float DEFAULT_ADAPTIVE_FACTOR
Definition threshold.h:65
constexpr float SEGMENTATION_MIN_THRESHOLD
Definition threshold.h:36
constexpr float SEGMENTATION_DEFAULT_THRESHOLD
Definition threshold.h:34
constexpr uint8_t STARTUP_MOTION_MIN_QUIET_CHUNKS
Definition threshold.h:73
constexpr float STARTUP_GATE_SPREAD_RATIO
Definition threshold.h:70
constexpr float SEGMENTATION_MAX_THRESHOLD
Definition threshold.h:37
constexpr uint8_t STARTUP_GATE_CHUNKS
Definition threshold.h:69
constexpr float STARTUP_NO_MOTION_FALLBACK_MARGIN
Definition threshold.h:80
constexpr float STARTUP_MOTION_GAP_RATIO
Definition threshold.h:79
constexpr uint8_t STARTUP_MOTION_CONFIRM_CHUNKS
Definition threshold.h:74
constexpr uint8_t STARTUP_MOTION_MAX_LEVELS
Definition threshold.h:81
constexpr uint8_t STARTUP_MOTION_CHUNK_SIZE
Definition threshold.h:72
constexpr float STARTUP_QUIET_RETURN_RATIO
Definition threshold.h:78
float clamp_threshold(float threshold, float min_threshold, float max_threshold)
Clamp threshold to [min, max] and recover non-finite values.
Definition threshold.h:51
constexpr float STARTUP_GATE_ANCHOR_RATIO
Definition threshold.h:71
bool is_valid_threshold(float threshold, float min_threshold, float max_threshold)
Validate threshold value against finite/range constraints.
Definition threshold.h:42