Enhance buzzer detection logic to group intermittent matches with a 500 ms quiet tail, updating relevant configurations and documentation.

This commit is contained in:
2026-10-19 15:28:21 -05:00
parent 7862a09858
commit 63d9995f59
10 changed files with 131 additions and 50 deletions
+2 -2
View File
@@ -65,8 +65,8 @@ menu "Door Buzzer Detector"
default 10
range 1 32
config BUZZ_COOLDOWN_MS
int "Cooldown after an alert (milliseconds)"
default 5
int "Quiet tail to group buzz frames into one event (milliseconds)"
default 500
range 0 86400000
config BUZZ_NOISE_MULT_X10
int "Learn/adapt gate: noise floor multiplier x10"
+13 -3
View File
@@ -13,7 +13,8 @@
static const char *TAG = "config";
#define NVS_NS "buzz"
#define CFG_KEY "cfg"
#define CFG_MAGIC 0xB0220002u
#define CFG_MAGIC 0xB0220003u
#define CFG_MAGIC_PRE_500MS 0xB0220002u
#define CFG_MAGIC_PRE_COOLDOWN_MS 0xB0220001u
typedef struct { uint32_t magic; app_config_t cfg; } cfg_blob_t;
@@ -33,7 +34,7 @@ static const param_t s_params[] = {
P(min_rms, PT_FLOAT, 0.0, 32767.0, "loudness gate (int16 RMS)"),
P(win_n, PT_UINT, 1, 32, "sliding window length N (frames)"),
P(win_m, PT_UINT, 1, 32, "matches M needed in window (<= N)"),
P(cooldown_ms, PT_UINT, 0, 86400000, "cooldown after alert (ms)"),
P(cooldown_ms, PT_UINT, 0, 86400000, "quiet tail used to group buzzer frames (ms)"),
P(noise_mult, PT_FLOAT, 1.0, 100.0, "learn/adapt gate = noise floor x this"),
P(adapt_weight, PT_FLOAT, 0.0, 1.0, "weight of new template in adapt"),
P(monitor_ms, PT_UINT, 10, 60000, "min interval between monitor lines (ms)"),
@@ -102,7 +103,16 @@ esp_err_t config_init(void)
} else if (blob.magic == CFG_MAGIC_PRE_COOLDOWN_MS) {
s_cfg = blob.cfg;
s_cfg.cooldown_ms = CONFIG_BUZZ_COOLDOWN_MS;
ESP_LOGI(TAG, "migrating saved config to millisecond cooldown");
ESP_LOGI(TAG, "migrating saved config to event-grouping cooldown");
err = save_locked();
if (err != ESP_OK) {
nvs_close(h);
return err;
}
} else if (blob.magic == CFG_MAGIC_PRE_500MS) {
s_cfg = blob.cfg;
s_cfg.cooldown_ms = CONFIG_BUZZ_COOLDOWN_MS;
ESP_LOGI(TAG, "updating saved cooldown for buzz-event grouping");
err = save_locked();
if (err != ESP_OK) {
nvs_close(h);
+1 -1
View File
@@ -10,7 +10,7 @@ typedef struct {
float min_rms; // loudness gate (int16 RMS); quieter frames never match
uint32_t win_n; // sliding window length (frames), 1..32
uint32_t win_m; // matches needed inside the window, 1..win_n
uint32_t cooldown_ms; // quiet period after an alert
uint32_t cooldown_ms; // quiet tail that groups matches into one buzz event
float noise_mult; // learn/adapt gate = noise floor * this
float adapt_weight; // weight of the NEW template in `adapt` (0..1)
uint32_t monitor_ms; // min interval between `monitor` lines
+2 -1
View File
@@ -128,10 +128,11 @@ static int cmd_status(int argc, char **argv)
printf("template: %s, %lu frames%s\n", matcher_source_name(), (unsigned long)matcher_frame_count(),
matcher_has_template() ? "" : " <-- run tools/make_template.py + rebuild, or `learn`");
if (matcher_suggested_threshold(&sug)) printf(" template.h suggests threshold %.3f\n", sug);
printf("detector: threshold %.3f, min_rms %.0f, window %lu/%lu, cooldown %lu ms\n", cfg.threshold, cfg.min_rms,
printf("detector: threshold %.3f, min_rms %.0f, window %lu/%lu, buzz quiet tail %lu ms\n", cfg.threshold, cfg.min_rms,
(unsigned long)cfg.win_m, (unsigned long)cfg.win_n, (unsigned long)cfg.cooldown_ms);
printf("last frame: rms %.1f, score %+.3f, match %d\n", ps.last_rms, ps.last_score, ps.last_match);
printf("triggers: %lu (last at uptime %lu s)\n", (unsigned long)ps.triggers, (unsigned long)ps.last_trigger_s);
printf("last buzz: %lu ms (final quiet tail excluded)\n", (unsigned long)ps.last_buzz_ms);
printf("frames: processed %lu, captured %lu, dropped %lu\n", (unsigned long)ps.frames,
(unsigned long)audio_in_frames_captured(), (unsigned long)audio_in_frames_dropped());
printf("proc time: avg %lu us, max %lu us (budget %d us per frame)\n", (unsigned long)ps.proc_avg_us,
+49 -17
View File
@@ -1,39 +1,71 @@
#include <string.h>
#include "detector.h"
#include "feature_params.h"
void detector_init(detector_t *d) { memset(d, 0, sizeof(*d)); d->state = DET_IDLE; }
void detector_reset(detector_t *d) { d->state = DET_IDLE; d->hist = 0; }
void detector_reset(detector_t *d)
{
d->state = DET_IDLE;
d->hist = 0;
d->frame_index = 0;
}
bool detector_is_match(float rms, float score, float min_rms, float threshold)
{
return rms >= min_rms && score >= threshold;
}
bool detector_update(detector_t *d, bool match, uint32_t now_ms, const detector_cfg_t *cfg)
static void update_idle(detector_t *d, bool match, uint32_t now_ms,
uint32_t n, uint32_t m)
{
uint32_t slot = d->frame_index % 32;
d->match_time_ms[slot] = now_ms;
d->hist = (d->hist << 1) | (match ? 1u : 0u);
uint32_t mask = (n == 32) ? 0xFFFFFFFFu : ((1u << n) - 1u);
if ((uint32_t)__builtin_popcount(d->hist & mask) >= m) {
uint32_t matches_seen = 0;
for (uint32_t offset = 0; offset < n; offset++) {
if ((d->hist & (1u << offset)) && ++matches_seen == m) {
uint32_t first_slot = (d->frame_index - offset) % 32;
d->buzz_start_ms = d->match_time_ms[first_slot];
break;
}
}
d->last_match_ms = now_ms;
d->state = DET_TRIGGERED;
d->triggers++;
}
d->frame_index++;
}
bool detector_update(detector_t *d, bool match, uint32_t now_ms, const detector_cfg_t *cfg,
uint32_t *buzz_duration_ms)
{
uint32_t n = cfg->win_n < 1 ? 1 : (cfg->win_n > 32 ? 32 : cfg->win_n);
uint32_t m = cfg->win_m < 1 ? 1 : (cfg->win_m > n ? n : cfg->win_m);
switch (d->state) {
case DET_IDLE: {
d->hist = (d->hist << 1) | (match ? 1u : 0u);
uint32_t mask = (n == 32) ? 0xFFFFFFFFu : ((1u << n) - 1u);
if ((uint32_t)__builtin_popcount(d->hist & mask) >= m) {
d->state = DET_TRIGGERED;
d->triggers++;
return true;
}
break;
}
case DET_IDLE:
update_idle(d, match, now_ms, n, m);
return false;
case DET_TRIGGERED:
d->state = DET_COOLDOWN;
d->cooldown_start_ms = now_ms;
d->hist = 0;
break;
case DET_COOLDOWN:
if ((uint32_t)(now_ms - d->cooldown_start_ms) >= cfg->cooldown_ms) {
if (match && (uint32_t)(now_ms - d->last_match_ms) < cfg->cooldown_ms) {
d->last_match_ms = now_ms;
d->state = DET_COOLDOWN;
} else {
d->state = DET_COOLDOWN;
if ((uint32_t)(now_ms - d->last_match_ms) < cfg->cooldown_ms) break;
d->last_completed_start_ms = d->buzz_start_ms;
if (buzz_duration_ms) {
*buzz_duration_ms = (uint32_t)(d->last_match_ms - d->buzz_start_ms) +
(1000u * HOP_LEN / FS_HZ);
}
d->state = DET_IDLE;
d->hist = 0;
if (match) update_idle(d, true, now_ms, n, m);
return true;
}
break;
}
+10 -5
View File
@@ -14,7 +14,11 @@ typedef struct {
typedef struct {
det_state_t state;
uint32_t hist; // bit 0 = newest frame, 1 = matched
uint32_t cooldown_start_ms;
uint32_t frame_index;
uint32_t match_time_ms[32];
uint32_t buzz_start_ms;
uint32_t last_match_ms;
uint32_t last_completed_start_ms;
uint32_t triggers;
} detector_t;
@@ -24,8 +28,9 @@ void detector_reset(detector_t *d); // back to IDLE with empty history
// Loudness gate AND score >= threshold.
bool detector_is_match(float rms, float score, float min_rms, float threshold);
// Feed one frame. Returns true exactly once per trigger (IDLE -> TRIGGERED).
// TRIGGERED lasts one frame, then COOLDOWN until cooldown_ms elapsed, then IDLE
// with a cleared history (the buzzer's tail cannot re-trigger).
bool detector_update(detector_t *d, bool match, uint32_t now_ms, const detector_cfg_t *cfg);
// Feed one frame. Returns true when a buzz event ends and writes its duration.
// Matches within cooldown_ms extend the same event; the final quiet tail is
// excluded from its duration.
bool detector_update(detector_t *d, bool match, uint32_t now_ms, const detector_cfg_t *cfg,
uint32_t *buzz_duration_ms);
const char *detector_state_name(det_state_t s);
+15 -6
View File
@@ -92,11 +92,12 @@ static void processing_task(void *arg)
const app_mode_t mode = calibration_get_mode();
const bool match = has_tpl && detector_is_match(rms, score, cfg.min_rms, cfg.threshold);
bool trig = false;
bool buzz_ended = false;
uint32_t buzz_duration_ms = 0;
if (mode == MODE_NORMAL) {
if (has_tpl) {
detector_cfg_t dc = { cfg.win_n, cfg.win_m, cfg.cooldown_ms };
trig = detector_update(&det, match, now_ms, &dc);
buzz_ended = detector_update(&det, match, now_ms, &dc, &buzz_duration_ms);
} else {
detector_reset(&det);
}
@@ -104,9 +105,13 @@ static void processing_task(void *arg)
calibration_on_frame(fp, valid, rms, score); // detector paused: no alerts
detector_reset(&det);
}
if (trig) {
ESP_LOGW(TAG, "BUZZER DETECTED (rms %.0f, score %.3f)", rms, score);
notifier_send(NOTIF_ALERT, "Buzzer detected");
if (buzz_ended) {
char message[96];
snprintf(message, sizeof(message), "Buzzer detected: %lu ms",
(unsigned long)buzz_duration_ms);
ESP_LOGW(TAG, "BUZZER EVENT ended, detected duration %lu ms",
(unsigned long)buzz_duration_ms);
notifier_send(NOTIF_ALERT, message);
}
portENTER_CRITICAL(&s_lock);
@@ -118,7 +123,11 @@ static void processing_task(void *arg)
s_stats.last_match = match;
s_stats.state = det.state;
s_stats.triggers = det.triggers;
if (trig) s_stats.last_trigger_s = now_ms / 1000 ? now_ms / 1000 : 1;
if (buzz_ended) {
s_stats.last_trigger_s = det.last_completed_start_ms / 1000;
if (s_stats.last_trigger_s == 0) s_stats.last_trigger_s = 1;
s_stats.last_buzz_ms = buzz_duration_ms;
}
memcpy(s_last_fp, fp, sizeof(s_last_fp));
portEXIT_CRITICAL(&s_lock);
+1
View File
@@ -14,6 +14,7 @@ typedef struct {
det_state_t state;
uint32_t triggers;
uint32_t last_trigger_s; // uptime seconds of the last trigger, 0 = never
uint32_t last_buzz_ms; // detected duration, excluding final quiet tail
} proc_stats_t;
esp_err_t processing_start(void);