92 gate_enabled_ = gate_enabled;
94 ready_packet_count_ = 0;
96 max_motion_metric_ = 0.0f;
97 gate_accepted_ =
false;
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;
128 void observe(
bool detector_ready,
float motion_metric, uint16_t packet_weight = 1U) {
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);
139 const uint32_t initial_remaining = remaining;
140 packet_count_ += weight;
141 if (!detector_ready) {
144 ready_packet_count_ += weight;
145 if (!has_value_ || motion_metric > max_motion_metric_) {
146 max_motion_metric_ = motion_metric;
149 if (gate_enabled_ && !gate_accepted_) {
150 observe_gate_metric_(motion_metric, weight, initial_remaining);
152 if (gate_enabled_ && !motion_accepted_ && packet_count_ <= target_packets_) {
153 observe_motion_chunk_(motion_metric, weight);
155 if (gate_enabled_ && !gate_accepted_ && packet_count_ >= target_packets_ &&
163 return motion_accepted_ || packet_count_ >= target_packets_;
174 if (motion_accepted_) {
175 return motion_threshold_metric_();
177 if (!gate_enabled_ || ring_count_ == 0) {
178 return has_value_ ? max_motion_metric_ : 0.0f;
180 if (gate_accepted_) {
181 float metric = ring_max_();
182 if (has_discarded_chunk_ &&
184 metric = std::max(metric, discarded_chunk_max_);
188 if (quiet_anchor_ready_ && motion_level_count_ > 0) {
189 const float quiet_ceiling = quiet_ceiling_();
191 return std::max(quiet_ceiling, std::min(ring_median_(), anchored_cap));
193 if (!motion_confirmed_) {
196 return ring_median_();
201 if (motion_accepted_) {
202 return "motion gap midpoint";
204 if (!gate_enabled_ || ring_count_ == 0) {
207 if (gate_accepted_ || !motion_confirmed_) {
208 if (!gate_accepted_ && quiet_anchor_ready_ && motion_level_count_ > 0) {
209 return "quiet anchor";
213 if (quiet_anchor_ready_ && motion_level_count_ > 0) {
214 return "quiet anchor";
216 return "gated median";
220 if (motion_accepted_) {
223 if (packet_count_ >= target_packets_) {
227 case Phase::SEEK_MOTION:
228 return "SEEK_MOTION";
229 case Phase::SEEK_POST_MOTION_QUIET:
230 return "SEEK_POST_QUIET";
232 return "CALIBRATING";
239 SEEK_POST_MOTION_QUIET,
248 void observe_gate_metric_(
float metric, uint32_t weight, uint32_t initial_remaining) {
249 if (chunk_size_ == 0) {
253 uint32_t remaining_weight = weight;
254 while (remaining_weight > 0U && !gate_accepted_) {
255 if (chunk_count_ == 0 || metric > chunk_max_) {
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_) {
268 void close_gate_chunk_() {
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;
276 ring_[ring_next_] = chunk_max_;
281 if (ring_count_ == 1 || chunk_max_ < min_chunk_max_) {
282 min_chunk_max_ = chunk_max_;
288 gate_accepted_ =
true;
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;
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;
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;
318 void consume_closed_motion_chunk_(
float level,
float peak) {
319 if (!quiet_anchor_ready_) {
321 bootstrap_levels_[0] = bootstrap_levels_[1];
324 bootstrap_levels_[bootstrap_count_] = level;
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];
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;
342 if (!motion_confirmed_) {
345 append_motion_level_(level);
346 consecutive_motion_chunks_++;
348 motion_confirmed_ =
true;
349 phase_ = Phase::SEEK_POST_MOTION_QUIET;
350 consecutive_post_quiet_chunks_ = 0;
351 post_quiet_level_count_ = 0;
357 append_quiet_level_(level);
359 consecutive_motion_chunks_ = 0;
364 append_post_quiet_level_(level);
365 consecutive_post_quiet_chunks_++;
368 motion_accepted_ =
true;
369 phase_ = Phase::COMPLETE;
376 append_motion_level_(level);
377 consecutive_post_quiet_chunks_ = 0;
378 phase_ = Phase::SEEK_POST_MOTION_QUIET;
382 consecutive_post_quiet_chunks_ = 0;
385 bool levels_are_stable_(
const float* values, uint8_t count)
const {
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]);
396 return high <= 1e-9f;
401 float quiet_reference_()
const {
402 if (quiet_level_count_ == 0) {
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];
411 return 0.5f * (ordered[quiet_level_count_ / 2 - 1] + ordered[quiet_level_count_ / 2]);
414 float motion_floor_()
const {
415 if (motion_level_count_ == 0) {
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];
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];
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];
438 return has_quiet ? quiet_ceiling : 0.0f;
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) {
447 return 0.5f * (motion_floor + quiet_ceiling);
450 bool motion_gap_ok_()
const {
451 if (motion_level_count_ == 0) {
454 const float quiet_ceiling = quiet_ceiling_();
455 if (quiet_ceiling <= 0.0f) {
461 void append_quiet_level_(
float value) {
463 quiet_levels_[quiet_level_count_++] = value;
467 quiet_levels_[i - 1] = quiet_levels_[i];
472 void append_motion_level_(
float value) {
474 motion_levels_[motion_level_count_++] = value;
478 motion_levels_[i - 1] = motion_levels_[i];
483 void append_post_quiet_level_(
float value) {
485 post_quiet_levels_[post_quiet_level_count_++] = value;
489 post_quiet_levels_[i - 1] = post_quiet_levels_[i];
494 bool gate_ok_()
const {
495 const float ring_max = ring_max_();
496 const float ring_median = ring_median_();
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]);
514 float ring_median_()
const {
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];
521 return 0.5f * (ordered[ring_count_ / 2 - 1] + ordered[ring_count_ / 2]);
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};
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};
544 uint8_t bootstrap_count_{0};
546 uint8_t quiet_level_count_{0};
548 uint8_t motion_level_count_{0};
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};