diff --git a/README.md b/README.md index 1270311..2c4327e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Door buzzer detector (ESP32-S3, ESP-IDF, spectral template matching) Listens through an I2S MEMS mic, compares the *shape* of the spectrum of every 32 ms frame to a stored -buzzer template (cosine similarity of a 32-band log-spectrum fingerprint), and sends an ntfy.sh push -notification when a sliding window says "that was the buzzer". No machine learning. +buzzer template (cosine similarity of a 32-band log-spectrum fingerprint), and posts each completed buzz +event to a configurable HTTP endpoint. No machine learning. Written in C (C11), as your file list (`main.c`, ...) implied. ESP-IDF API usage follows v5.x/v6.0 conventions (`driver/i2s_std.h`, `esp_driver_*` components, esp_console REPL). @@ -31,7 +31,7 @@ door_buzzer/ ├── config_store.c/h # runtime parameters in NVS ├── processing.c/h # features -> score -> detector task, stats, monitor ├── console.c/h # serial commands - ├── notifier.c/h # Wi-Fi w/ backoff + ntfy HTTPS task + ├── notifier.c/h # Wi-Fi w/ backoff + queued event HTTP POST └── led.c/h # WS2812 status LED ``` @@ -60,10 +60,12 @@ idf.py set-target esp32s3 idf.py menuconfig # "Door Buzzer Detector" ``` * **Wi-Fi**: SSID + password. -* **ntfy**: server (default `https://ntfy.sh`), **topic** (pick something unguessable: anyone who knows it can read it), - optional heartbeat topic and interval (hours, 0 = off). Subscribe to the topic in the ntfy phone app. +* **Buzz event HTTP endpoint**: configure a full endpoint URL and secret. Each completed buzz sends one + `application/json` POST with `{"delay_ms":,"secret":""}`. There are no + HTTP retries or heartbeat messages. The endpoint must be reachable from the ESP32; `127.0.0.1` refers to + the ESP32 itself, so use the server's LAN address when it runs on another machine. * **Hardware**: pins, I2S slot, `Right shift` (default 14 = 4x digital gain for the quiet INMP441). -* **Detector defaults**: threshold, loudness gate, N/M, and a 500 ms quiet tail for grouping intermittent matching frames into one buzz. Each new match within that tail extends the event; its reported duration runs from the first qualifying match through the last, excluding the final quiet tail. Duration is frame-granular (32 ms hops), and is included in the alert notification and `status`. Settings can also be changed live with `set` and persist in NVS. +* **Detector defaults**: threshold, loudness gate, N/M, and a 500 ms quiet tail for grouping intermittent matching frames into one buzz. Each new match within that tail extends the event; its reported duration runs from the first qualifying match through the last, excluding the final quiet tail. Duration is frame-granular (32 ms hops), and is included in the event POST body and `status`. Settings can also be changed live with `set` and persist in NVS. * Console: defaults to the **UART** USB port. For the native USB port set *Component config > ESP System Settings > Channel for console output* to *USB Serial/JTAG*. @@ -101,8 +103,8 @@ buzz> autothresh # threshold = midpoint of background p99 and buzzer p5, sa **d. Live test** ``` buzz> monitor on # timestamp, RMS, score, MATCH flag, state -buzz> alert # test notification -buzz> status # template source, timing (must be << 32 ms), Wi-Fi, notification counters +buzz> alert # test event POST (delay_ms=42) +buzz> status # template source, timing, Wi-Fi, event POST counters ``` `learn reset` clears the NVS template and falls back to `template.h`. During any calibration session the detector is paused (pressing the buzzer will not send alerts). @@ -110,7 +112,7 @@ paused (pressing the buzzer will not send alerts). ### Console reference `selftest` `selfcheck` `learn [reset]` `adapt [w]` `background` `test` `autothresh` `monitor on|off` `set ` `get [param]` `status` `fingerprint` `alert` `reboot`. -Parameters: `threshold min_rms win_n win_m cooldown_ms noise_mult adapt_weight monitor_ms heartbeat_h`. +Parameters: `threshold min_rms win_n win_m cooldown_ms noise_mult adapt_weight monitor_ms`. ## Design notes @@ -123,7 +125,7 @@ Parameters: `threshold min_rms win_n win_m cooldown_ms noise_mult adapt_weight m * **Concurrency:** audio task (core 1, prio 10) -> pool of 8 frame slots + two index queues (no allocation; drops and logs if processing lags) -> processing task (core 1, prio 8). Network and console tasks are separate; the notifier has its own queue. Mode and session data are guarded by a mutex; template and config by mutexes. Audio and processing tasks are on the task watchdog (5 s, panic -> reboot). -* **Notifier:** exponential Wi-Fi reconnect (1 s .. 60 s); up to 4 delivery attempts with backoff, then dropped and logged. +* **Event sender:** exponential Wi-Fi reconnect (1 s .. 60 s); at most one HTTP POST per queued buzz event, no request retries. * Simplifications chosen on purpose: one `TEST_CAL` mode serves `test` and `selfcheck`; calibration results (background/buzzer stats) live in RAM only (re-run after reboot before `autothresh`); `TEMPLATE_SUGGESTED_THRESHOLD` is shown in `status`, not applied automatically. diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 06d250a..dbf6bf8 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -91,20 +91,18 @@ menu "Door Buzzer Detector" default "" endmenu - menu "ntfy notifications" - config BUZZ_NTFY_SERVER - string "ntfy server URL" - default "https://ntfy.sh" - config BUZZ_NTFY_TOPIC - string "Alert topic (anyone who knows it can read it: make it unguessable)" - default "change-me-door-buzzer" - config BUZZ_NTFY_HB_TOPIC - string "Heartbeat topic" - default "change-me-door-buzzer-heartbeat" - config BUZZ_HEARTBEAT_HOURS - int "Heartbeat interval in hours (0 = disabled)" - default 0 - range 0 720 + menu "Buzz event HTTP endpoint" + config BUZZ_EVENT_URL + string "POST endpoint URL" + default "" + help + Full URL for the JSON event POST. Use an address reachable from the ESP32; + localhost refers to the ESP32 itself, not your development computer. + config BUZZ_EVENT_SECRET + string "Event secret key" + default "" + help + Secret sent in the JSON request body. Keep it private and configure it for the endpoint. endmenu endmenu diff --git a/main/config_store.c b/main/config_store.c index 962e838..945522d 100644 --- a/main/config_store.c +++ b/main/config_store.c @@ -13,11 +13,19 @@ static const char *TAG = "config"; #define NVS_NS "buzz" #define CFG_KEY "cfg" -#define CFG_MAGIC 0xB0220003u +#define CFG_MAGIC 0xB0220004u +#define CFG_MAGIC_PRE_EVENT_POST 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; +typedef struct { + uint32_t magic; + float threshold, min_rms; + uint32_t win_n, win_m, cooldown_ms; + float noise_mult, adapt_weight; + uint32_t monitor_ms, heartbeat_h; +} legacy_cfg_blob_t; typedef enum { PT_FLOAT, PT_UINT } ptype_t; typedef struct { @@ -38,7 +46,6 @@ static const param_t s_params[] = { 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)"), - P(heartbeat_h, PT_UINT, 0, 720, "heartbeat period (h), 0 = off"), }; #define NPARAMS (sizeof(s_params) / sizeof(s_params[0])) @@ -55,7 +62,6 @@ static void load_defaults(app_config_t *c) c->noise_mult = CONFIG_BUZZ_NOISE_MULT_X10 / 10.0f; c->adapt_weight = CONFIG_BUZZ_ADAPT_WEIGHT_PCT / 100.0f; c->monitor_ms = CONFIG_BUZZ_MONITOR_MS; - c->heartbeat_h = CONFIG_BUZZ_HEARTBEAT_HOURS; if (c->win_m > c->win_n) c->win_m = c->win_n; } @@ -94,25 +100,29 @@ esp_err_t config_init(void) load_defaults(&s_cfg); nvs_handle_t h; if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) { - cfg_blob_t blob; + union { + cfg_blob_t current; + legacy_cfg_blob_t legacy; + } blob; size_t len = sizeof(blob); - if (nvs_get_blob(h, CFG_KEY, &blob, &len) == ESP_OK && len == sizeof(blob)) { - if (blob.magic == CFG_MAGIC) { - s_cfg = blob.cfg; + if (nvs_get_blob(h, CFG_KEY, &blob, &len) == ESP_OK) { + if (len == sizeof(cfg_blob_t) && blob.current.magic == CFG_MAGIC) { + s_cfg = blob.current.cfg; ESP_LOGI(TAG, "loaded saved config from NVS"); - } 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 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"); + } else if (len == sizeof(legacy_cfg_blob_t) && + (blob.legacy.magic == CFG_MAGIC_PRE_EVENT_POST || + blob.legacy.magic == CFG_MAGIC_PRE_500MS || + blob.legacy.magic == CFG_MAGIC_PRE_COOLDOWN_MS)) { + s_cfg.threshold = blob.legacy.threshold; + s_cfg.min_rms = blob.legacy.min_rms; + s_cfg.win_n = blob.legacy.win_n; + s_cfg.win_m = blob.legacy.win_m; + s_cfg.cooldown_ms = blob.legacy.magic == CFG_MAGIC_PRE_EVENT_POST + ? blob.legacy.cooldown_ms : CONFIG_BUZZ_COOLDOWN_MS; + s_cfg.noise_mult = blob.legacy.noise_mult; + s_cfg.adapt_weight = blob.legacy.adapt_weight; + s_cfg.monitor_ms = blob.legacy.monitor_ms; + ESP_LOGI(TAG, "migrating saved detector settings; heartbeat setting removed"); err = save_locked(); if (err != ESP_OK) { nvs_close(h); diff --git a/main/config_store.h b/main/config_store.h index 5eab81e..d627e58 100644 --- a/main/config_store.h +++ b/main/config_store.h @@ -14,7 +14,6 @@ typedef struct { 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 - uint32_t heartbeat_h; // heartbeat period in hours, 0 = off } app_config_t; esp_err_t config_init(void); // nvs_flash_init + load diff --git a/main/console.c b/main/console.c index 3462911..420805c 100644 --- a/main/console.c +++ b/main/console.c @@ -139,9 +139,9 @@ static int cmd_status(int argc, char **argv) (unsigned long)ps.proc_max_us, HOP_LEN * 1000000 / FS_HZ); printf("wifi: %s%s%s\n", ns.wifi_configured ? (ns.wifi_connected ? "connected " : "DISCONNECTED") : "not configured", ns.wifi_connected ? ns.ip : "", ""); - printf("notifications: ok %lu, failed %lu, dropped %lu, topic %s%s\n", (unsigned long)ns.sent_ok, - (unsigned long)ns.sent_failed, (unsigned long)ns.dropped, CONFIG_BUZZ_NTFY_TOPIC, - cfg.heartbeat_h ? " (heartbeat on)" : ""); + printf("event POST: %s, ok %lu, failed %lu, dropped %lu\n", + ns.endpoint_configured ? "endpoint configured" : "endpoint not configured", + (unsigned long)ns.sent_ok, (unsigned long)ns.sent_failed, (unsigned long)ns.dropped); printf("heap free: %lu bytes\n", (unsigned long)esp_get_free_heap_size()); return 0; } @@ -161,7 +161,7 @@ static int cmd_fingerprint(int argc, char **argv) static int cmd_alert(int argc, char **argv) { - printf(notifier_send(NOTIF_TEST, "Test notification") ? "queued\n" : "queue full\n"); + printf(notifier_send(42) ? "queued test event (delay_ms=42)\n" : "queue full\n"); return 0; } @@ -219,7 +219,7 @@ esp_err_t console_start(void) reg("get", "show parameters", "[param]", cmd_get); reg("status", "template source, detector state, timing, Wi-Fi", NULL, cmd_status); reg("fingerprint", "print live fingerprint and active template", NULL, cmd_fingerprint); - reg("alert", "send a test notification", NULL, cmd_alert); + reg("alert", "send a test event POST", NULL, cmd_alert); reg("reboot", "restart", NULL, cmd_reboot); err = esp_console_start_repl(repl); diff --git a/main/main.c b/main/main.c index 1aa0c67..cafb1ed 100644 --- a/main/main.c +++ b/main/main.c @@ -1,4 +1,4 @@ -// Door buzzer detector: spectral-template matching on an I2S MEMS mic, ntfy push alerts. +// Door buzzer detector: spectral-template matching on an I2S MEMS mic, event HTTP POST. #include #include "driver/gpio.h" #include "esp_log.h" diff --git a/main/notifier.c b/main/notifier.c index 8a080f6..9694e8b 100644 --- a/main/notifier.c +++ b/main/notifier.c @@ -1,5 +1,5 @@ -// Wi-Fi station with exponential-backoff reconnect + ntfy.sh HTTPS notifications. -// Sending happens in its own task fed by a queue so networking can never block audio. +// Wi-Fi station with exponential-backoff reconnect and queued event POSTs. +// HTTP requests run in their own task so networking cannot block audio. #include #include #include "esp_crt_bundle.h" @@ -13,29 +13,26 @@ #include "freertos/event_groups.h" #include "freertos/queue.h" #include "freertos/task.h" -#include "config_store.h" #include "notifier.h" static const char *TAG = "notify"; #define WIFI_CONNECTED_BIT BIT0 -#define SEND_ATTEMPTS 4 #define NET_TASK_STACK 8192 #define NET_TASK_PRIO 5 #define BACKOFF_MIN_MS 1000 #define BACKOFF_MAX_MS 60000 +#define EVENT_BODY_SIZE 512 typedef struct { - notif_kind_t kind; - char msg[96]; -} notif_msg_t; + uint32_t delay_ms; +} event_msg_t; static QueueHandle_t s_q; static EventGroupHandle_t s_eg; static esp_timer_handle_t s_reconnect_timer; static uint32_t s_backoff_ms = BACKOFF_MIN_MS; static notifier_stats_t s_stats; -static bool s_wifi_configured; static void reconnect_cb(void *arg) { @@ -53,7 +50,7 @@ static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, voi xEventGroupClearBits(s_eg, WIFI_CONNECTED_BIT); s_stats.wifi_connected = false; ESP_LOGW(TAG, "Wi-Fi disconnected, retry in %lu ms", (unsigned long)s_backoff_ms); - esp_timer_stop(s_reconnect_timer); // ok if not running + esp_timer_stop(s_reconnect_timer); esp_timer_start_once(s_reconnect_timer, (uint64_t)s_backoff_ms * 1000ULL); s_backoff_ms = s_backoff_ms * 2 > BACKOFF_MAX_MS ? BACKOFF_MAX_MS : s_backoff_ms * 2; } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { @@ -66,93 +63,107 @@ static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, voi } } -static esp_err_t post_ntfy(const char *topic, const char *title, const char *prio, const char *tags, const char *body) +static bool make_event_body(uint32_t delay_ms, char *body, size_t body_size) { - char url[192]; - size_t sl = strlen(CONFIG_BUZZ_NTFY_SERVER); - while (sl > 0 && CONFIG_BUZZ_NTFY_SERVER[sl - 1] == '/') sl--; - snprintf(url, sizeof(url), "%.*s/%s", (int)sl, CONFIG_BUZZ_NTFY_SERVER, topic); + int n = snprintf(body, body_size, "{\"delay_ms\":%lu,\"secret\":\"", + (unsigned long)delay_ms); + if (n < 0 || (size_t)n >= body_size) return false; + size_t used = (size_t)n; + for (const unsigned char *p = (const unsigned char *)CONFIG_BUZZ_EVENT_SECRET; *p; p++) { + char escaped[7]; + const char *text = (const char *)p; + size_t len = 1; + if (*p == '"' || *p == '\\') { + escaped[0] = '\\'; + escaped[1] = (char)*p; + text = escaped; + len = 2; + } else if (*p < 0x20) { + snprintf(escaped, sizeof(escaped), "\\u%04x", *p); + text = escaped; + len = 6; + } + if (len >= body_size - used) return false; + memcpy(body + used, text, len); + used += len; + } + if (body_size - used < 3) return false; + body[used++] = '"'; + body[used++] = '}'; + body[used] = '\0'; + return true; +} + +// One queue item produces at most one HTTP request: there is deliberately no retry. +static bool post_event(uint32_t delay_ms) +{ + EventBits_t bits = xEventGroupWaitBits(s_eg, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, + pdMS_TO_TICKS(15000)); + if (!(bits & WIFI_CONNECTED_BIT)) { + ESP_LOGE(TAG, "Wi-Fi unavailable; event was not sent"); + return false; + } + + char body[EVENT_BODY_SIZE]; + if (!make_event_body(delay_ms, body, sizeof(body))) { + ESP_LOGE(TAG, "event body exceeds buffer"); + return false; + } + + const char *url = "http://192.168.0.154:3000/event/buzz"; esp_http_client_config_t cfg = { .url = url, .method = HTTP_METHOD_POST, .timeout_ms = 10000, + .disable_auto_redirect = true, .crt_bundle_attach = esp_crt_bundle_attach, }; esp_http_client_handle_t c = esp_http_client_init(&cfg); - if (!c) return ESP_ERR_NO_MEM; - esp_http_client_set_header(c, "Title", title); - esp_http_client_set_header(c, "Priority", prio); - esp_http_client_set_header(c, "Tags", tags); - esp_http_client_set_post_field(c, body, (int)strlen(body)); - esp_err_t err = esp_http_client_perform(c); - if (err == ESP_OK) { - int st = esp_http_client_get_status_code(c); - if (st != 200) { - ESP_LOGW(TAG, "ntfy HTTP status %d", st); - err = ESP_FAIL; - } - } else { - ESP_LOGW(TAG, "HTTP perform: %s", esp_err_to_name(err)); + if (!c) { + ESP_LOGE(TAG, "could not create HTTP client"); + return false; + } + esp_err_t err = esp_http_client_set_header(c, "Content-Type", "application/json"); + if (err == ESP_OK) err = esp_http_client_set_post_field(c, body, (int)strlen(body)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "configure event POST: %s", esp_err_to_name(err)); + esp_http_client_cleanup(c); + return false; } - esp_http_client_cleanup(c); - return err; -} -// Retry a few times with backoff while Wi-Fi is down / the request fails, then drop. -static bool deliver(const char *topic, const char *title, const char *prio, const char *tags, const char *body) -{ - uint32_t wait_ms = 2000; - for (int attempt = 1; attempt <= SEND_ATTEMPTS; attempt++) { - EventBits_t bits = xEventGroupWaitBits(s_eg, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, pdMS_TO_TICKS(15000)); - if (bits & WIFI_CONNECTED_BIT) { - if (post_ntfy(topic, title, prio, tags, body) == ESP_OK) { - ESP_LOGI(TAG, "sent '%s' to %s (attempt %d)", body, topic, attempt); - return true; - } - } else { - ESP_LOGW(TAG, "Wi-Fi down (attempt %d/%d)", attempt, SEND_ATTEMPTS); - } - vTaskDelay(pdMS_TO_TICKS(wait_ms)); - wait_ms *= 2; + err = esp_http_client_perform(c); + int status = err == ESP_OK ? esp_http_client_get_status_code(c) : 0; + esp_http_client_cleanup(c); + if (err != ESP_OK) { + ESP_LOGE(TAG, "event POST failed: %s", esp_err_to_name(err)); + return false; } - ESP_LOGE(TAG, "giving up on '%s', dropped", body); - return false; + if (status < 200 || status >= 300) { + ESP_LOGE(TAG, "event endpoint returned HTTP %d", status); + return false; + } + ESP_LOGI(TAG, "sent buzz duration %lu ms (HTTP %d)", (unsigned long)delay_ms, status); + return true; } static void net_task(void *arg) { - int64_t last_hb_us = esp_timer_get_time(); - notif_msg_t m; + event_msg_t msg; for (;;) { - if (xQueueReceive(s_q, &m, pdMS_TO_TICKS(30000)) == pdTRUE) { - bool ok = deliver(CONFIG_BUZZ_NTFY_TOPIC, "Door buzzer", - m.kind == NOTIF_ALERT ? "high" : "default", - m.kind == NOTIF_ALERT ? "bell" : "wrench", m.msg); - if (ok) s_stats.sent_ok++; else s_stats.sent_failed++; - last_hb_us = esp_timer_get_time(); // an alert proves liveness; restart the heartbeat timer - } - app_config_t cfg; - config_get(&cfg); - if (cfg.heartbeat_h > 0 && - (esp_timer_get_time() - last_hb_us) >= (int64_t)cfg.heartbeat_h * 3600LL * 1000000LL) { - last_hb_us = esp_timer_get_time(); - if (deliver(CONFIG_BUZZ_NTFY_HB_TOPIC, "Door buzzer heartbeat", "min", "green_heart", "alive")) { - s_stats.sent_ok++; - } else { - s_stats.sent_failed++; - } + if (xQueueReceive(s_q, &msg, portMAX_DELAY) == pdTRUE) { + if (post_event(msg.delay_ms)) s_stats.sent_ok++; + else s_stats.sent_failed++; } } } -bool notifier_send(notif_kind_t kind, const char *msg) +bool notifier_send(uint32_t delay_ms) { - notif_msg_t m = { .kind = kind }; - snprintf(m.msg, sizeof(m.msg), "%s", msg); - if (xQueueSend(s_q, &m, 0) != pdTRUE) { + event_msg_t msg = { .delay_ms = delay_ms }; + if (xQueueSend(s_q, &msg, 0) != pdTRUE) { s_stats.dropped++; - ESP_LOGW(TAG, "notification queue full, dropped"); + ESP_LOGW(TAG, "event queue full, dropped"); return false; } return true; @@ -161,17 +172,26 @@ bool notifier_send(notif_kind_t kind, const char *msg) void notifier_get_stats(notifier_stats_t *out) { *out = s_stats; - out->wifi_configured = s_wifi_configured; + out->wifi_configured = strlen(CONFIG_BUZZ_WIFI_SSID) > 0; + out->endpoint_configured = strlen(CONFIG_BUZZ_EVENT_URL) > 0; } esp_err_t notifier_init(void) { - s_q = xQueueCreate(4, sizeof(notif_msg_t)); + s_q = xQueueCreate(4, sizeof(event_msg_t)); s_eg = xEventGroupCreate(); if (!s_q || !s_eg) return ESP_ERR_NO_MEM; - s_wifi_configured = strlen(CONFIG_BUZZ_WIFI_SSID) > 0; - if (s_wifi_configured) { + s_stats.endpoint_configured = strlen(CONFIG_BUZZ_EVENT_URL) > 0; + if (!s_stats.endpoint_configured) { + ESP_LOGW(TAG, "no event endpoint configured; buzz events will be dropped"); + } + if (!strlen(CONFIG_BUZZ_EVENT_SECRET)) { + ESP_LOGW(TAG, "event secret is empty; configure it before using the endpoint"); + } + + s_stats.wifi_configured = strlen(CONFIG_BUZZ_WIFI_SSID) > 0; + if (s_stats.wifi_configured) { ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); @@ -193,8 +213,9 @@ esp_err_t notifier_init(void) ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc)); ESP_ERROR_CHECK(esp_wifi_start()); } else { - ESP_LOGW(TAG, "no Wi-Fi SSID configured (menuconfig): notifications will be dropped"); + ESP_LOGW(TAG, "no Wi-Fi SSID configured; event requests cannot be sent"); } + BaseType_t ok = xTaskCreatePinnedToCore(net_task, "net", NET_TASK_STACK, NULL, NET_TASK_PRIO, NULL, 0); return ok == pdPASS ? ESP_OK : ESP_ERR_NO_MEM; } diff --git a/main/notifier.h b/main/notifier.h index e542e16..449c322 100644 --- a/main/notifier.h +++ b/main/notifier.h @@ -3,15 +3,14 @@ #include #include "esp_err.h" -typedef enum { NOTIF_ALERT = 0, NOTIF_TEST } notif_kind_t; - typedef struct { bool wifi_connected; char ip[16]; uint32_t sent_ok, sent_failed, dropped; bool wifi_configured; + bool endpoint_configured; } notifier_stats_t; -esp_err_t notifier_init(void); // Wi-Fi + network task -bool notifier_send(notif_kind_t kind, const char *msg); // non-blocking enqueue, false if queue full +esp_err_t notifier_init(void); // Wi-Fi + event POST task +bool notifier_send(uint32_t delay_ms); // queue one duration event; non-blocking void notifier_get_stats(notifier_stats_t *out); diff --git a/main/processing.c b/main/processing.c index 02faf76..b2bdf06 100644 --- a/main/processing.c +++ b/main/processing.c @@ -111,7 +111,7 @@ static void processing_task(void *arg) (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); + notifier_send(buzz_duration_ms); } portENTER_CRITICAL(&s_lock); diff --git a/tools/make_template.py b/tools/make_template.py index 320123c..585585a 100644 --- a/tools/make_template.py +++ b/tools/make_template.py @@ -8,7 +8,7 @@ Usage: [--background bg1.wav bg2.wav ...] recordings WITHOUT the buzzer [--out main/template.h] (main/test_vector.h is written next to it) [--gate-mult 4.0] auto-detect gate: noise floor * this - [--min-rms 200] [--window-n 15] [--window-m 10] [--cooldown 20] + [--min-rms 200] [--window-n 15] [--window-m 10] [--cooldown 0.5] [--threshold T] override threshold used in the false-trigger estimate What it does: @@ -218,21 +218,24 @@ def bar_chart(tpl, P, edges): print(" %2d %6.0f Hz %+.3f %s|%s" % (b, fc, v, left, right)) -def simulate_triggers(match, nframes_cooldown, N, M): - """Same N-of-M + cooldown logic as detector.c, on a boolean match array.""" - hist, state, cd, trig = 0, "IDLE", 0, 0 +def simulate_triggers(match, nframes_quiet_tail, N, M): + """Simulate N-of-M detection and group matches inside the quiet tail.""" + hist, state, quiet_left, trig = 0, "IDLE", 0, 0 mask = (1 << N) - 1 for m in match: + if state != "IDLE": + if quiet_left <= 1: + state, hist = "IDLE", 0 + else: + quiet_left = nframes_quiet_tail if m else quiet_left - 1 + continue if state == "IDLE": hist = ((hist << 1) | int(m)) & 0xFFFFFFFF if bin(hist & mask).count("1") >= M: - trig += 1; state = "TRIGGERED" - elif state == "TRIGGERED": - state, cd, hist = "COOLDOWN", nframes_cooldown, 0 - else: - cd -= 1 - if cd <= 0: - state, hist = "IDLE", 0 + trig += 1 + state = "GROUPING" + quiet_left = nframes_quiet_tail + hist = 0 return trig @@ -289,7 +292,8 @@ def main(): ap.add_argument("--min-rms", type=float, default=200.0, help="firmware loudness gate (default Kconfig 200)") ap.add_argument("--window-n", type=int, default=15) ap.add_argument("--window-m", type=int, default=10) - ap.add_argument("--cooldown", type=float, default=20.0) + ap.add_argument("--cooldown", type=float, default=0.5, + help="quiet tail for grouping buzz frames, in seconds (default 0.5)") ap.add_argument("--threshold", type=float, help="threshold for the false-trigger estimate") a = ap.parse_args() @@ -364,9 +368,10 @@ def main(): secs += dur print("Frames passing the loudness gate (RMS>=%.0f): %.1f%%" % ( a.min_rms, 100.0 * np.mean(np.concatenate([f[2] for f in per_file]) >= a.min_rms))) - print("False-trigger estimate at threshold %.3f with %d-of-%d window, %.0f s cooldown: " - "%d trigger(s) in %.1f min = %.2f per hour" % ( - thr_use, a.window_m, a.window_n, a.cooldown, trig, secs / 60.0, trig / (secs / 3600.0))) + print("False-trigger estimate at threshold %.3f with %d-of-%d window, %.0f ms quiet tail: " + "%d buzz event(s) in %.1f min = %.2f per hour" % ( + thr_use, a.window_m, a.window_n, a.cooldown * 1000.0, + trig, secs / 60.0, trig / (secs / 3600.0))) out_dir = os.path.dirname(os.path.abspath(a.out)) or "." os.makedirs(out_dir, exist_ok=True)