From ce5205eb29a9258770677d96c010b5dd81a6bb3b Mon Sep 17 00:00:00 2001 From: user Date: Sat, 14 Feb 2026 17:40:12 +0100 Subject: [PATCH] feat: Add ALERT command for temp/heap threshold monitoring ALERT TEMP and ALERT HEAP emit EVENT packets when thresholds are crossed (60s holdoff). NVS-persisted, shown in STATUS and CONFIG. Temp alerts require SOC_TEMP_SENSOR_SUPPORTED. --- get-started/csi_recv_router/main/app_main.c | 118 +++++++++++++++++++- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/get-started/csi_recv_router/main/app_main.c b/get-started/csi_recv_router/main/app_main.c index 79426e4..2189720 100644 --- a/get-started/csi_recv_router/main/app_main.c +++ b/get-started/csi_recv_router/main/app_main.c @@ -211,6 +211,15 @@ static int64_t s_chanscan_last = 0; #define PROBE_DEDUP_DEFAULT_US 10000000LL static int64_t s_probe_dedup_us = PROBE_DEDUP_DEFAULT_US; +/* Alert thresholds (0 = disabled) */ +#define ALERT_HOLDOFF_US 60000000LL /* 60s debounce between alerts */ +static float s_alert_temp_thresh = 0.0f; /* celsius, e.g. 70.0 */ +static uint32_t s_alert_heap_thresh = 0; /* bytes, e.g. 30000 */ +#if SOC_TEMP_SENSOR_SUPPORTED +static int64_t s_alert_temp_last = 0; +#endif +static int64_t s_alert_heap_last = 0; + /* --- NVS helpers --- */ static void config_load_nvs(void) @@ -313,11 +322,20 @@ static void config_load_nvs(void) if (nvs_get_i8(h, "csi_enabled", &csi_en) == ESP_OK) { s_csi_enabled = (csi_en != 0); } + int32_t alert_temp; + if (nvs_get_i32(h, "alert_temp", &alert_temp) == ESP_OK && alert_temp > 0) { + s_alert_temp_thresh = (float)alert_temp / 10.0f; /* stored as 10ths of degree */ + } + int32_t alert_heap; + if (nvs_get_i32(h, "alert_heap", &alert_heap) == ESP_OK && alert_heap > 0) { + s_alert_heap_thresh = (uint32_t)alert_heap; + } nvs_close(h); - ESP_LOGI(TAG, "NVS loaded: hostname=%s rate=%d tx_power=%d adaptive=%d threshold=%.6f ble=%d target=%s:%d csi_mode=%d hybrid_n=%d powersave=%d presence=%d pr_thresh=%.4f baseline_nsub=%d led_quiet=%d csi=%d", + ESP_LOGI(TAG, "NVS loaded: hostname=%s rate=%d tx_power=%d adaptive=%d threshold=%.6f ble=%d target=%s:%d csi_mode=%d hybrid_n=%d powersave=%d presence=%d pr_thresh=%.4f baseline_nsub=%d led_quiet=%d csi=%d alert_temp=%.1f alert_heap=%lu", s_hostname, s_send_frequency, s_tx_power_dbm, s_adaptive, s_motion_threshold, s_ble_enabled, s_target_ip, s_target_port, (int)s_csi_mode, s_hybrid_interval, s_powersave, - s_presence_enabled, s_pr_threshold, s_baseline_nsub, s_led_quiet, s_csi_enabled); + s_presence_enabled, s_pr_threshold, s_baseline_nsub, s_led_quiet, s_csi_enabled, + s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh); } else { ESP_LOGI(TAG, "NVS: no saved config, using defaults"); } @@ -992,6 +1010,53 @@ static void adaptive_task(void *arg) } } + /* Alert threshold checks */ + { + int64_t now_alert = esp_timer_get_time(); + + /* Heap alert */ + if (s_alert_heap_thresh > 0) { + uint32_t free_heap = esp_get_free_heap_size(); + if (free_heap < s_alert_heap_thresh && + (now_alert - s_alert_heap_last) > ALERT_HOLDOFF_US) { + s_alert_heap_last = now_alert; + char event[128]; + int len = snprintf(event, sizeof(event), + "EVENT,%s,alert=heap free=%lu thresh=%lu", + s_hostname, (unsigned long)free_heap, + (unsigned long)s_alert_heap_thresh); + if (s_udp_socket >= 0) { + sendto(s_udp_socket, event, len, 0, + (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr)); + } + ESP_LOGW(TAG, "ALERT: heap low (%lu < %lu)", + (unsigned long)free_heap, (unsigned long)s_alert_heap_thresh); + } + } + + /* Temperature alert */ +#if SOC_TEMP_SENSOR_SUPPORTED + if (s_alert_temp_thresh > 0.0f && s_temp_handle) { + float temp = 0.0f; + if (temperature_sensor_get_celsius(s_temp_handle, &temp) == ESP_OK && + temp > s_alert_temp_thresh && + (now_alert - s_alert_temp_last) > ALERT_HOLDOFF_US) { + s_alert_temp_last = now_alert; + char event[128]; + int len = snprintf(event, sizeof(event), + "EVENT,%s,alert=temp value=%.1f thresh=%.1f", + s_hostname, temp, s_alert_temp_thresh); + if (s_udp_socket >= 0) { + sendto(s_udp_socket, event, len, 0, + (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr)); + } + ESP_LOGW(TAG, "ALERT: temp high (%.1f > %.1f)", + temp, s_alert_temp_thresh); + } + } +#endif + } + if (!s_adaptive || s_energy_idx < WANDER_WINDOW) continue; /* Compute mean */ @@ -1602,6 +1667,7 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) " temp=%.1f csi_count=%lu boots=%lu rssi_min=%d rssi_max=%d" " csi=%s csi_mode=%s hybrid_n=%d auth=%s flood_thresh=%d/%d powersave=%s" " presence=%s pr_score=%.4f chanscan=%s led=%s" + " alert_temp=%.1f alert_heap=%lu" " nvs_used=%lu nvs_free=%lu nvs_total=%lu part_size=%lu" " built=%s_%s idf=%s chip=%sr%dc%d", uptime_str, (long long)up, (unsigned long)heap, rssi, channel, (int)s_tx_power_dbm, @@ -1617,6 +1683,7 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) s_powersave ? "on" : "off", s_presence_enabled ? "on" : "off", s_pr_last_score, s_chanscan_enabled ? "on" : "off", s_led_quiet ? "quiet" : "auto", + s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh, (unsigned long)nvs_stats.used_entries, (unsigned long)nvs_stats.free_entries, (unsigned long)nvs_stats.total_entries, @@ -2194,6 +2261,48 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) return strlen(reply); } + /* ALERT — set temp/heap alert thresholds */ + if (strcmp(cmd, "ALERT") == 0) { + snprintf(reply, reply_size, "OK ALERT temp=%.1f heap=%lu (0=off)", + s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh); + return strlen(reply); + } + if (strncmp(cmd, "ALERT ", 6) == 0) { + const char *arg = cmd + 6; + if (strncmp(arg, "TEMP ", 5) == 0) { + float val = strtof(arg + 5, NULL); + if (val < 0 || val > 125) { + snprintf(reply, reply_size, "ERR ALERT TEMP range 0-125 (0=off)"); + return strlen(reply); + } + s_alert_temp_thresh = val; + config_save_i32("alert_temp", (int32_t)(val * 10.0f)); + snprintf(reply, reply_size, "OK ALERT TEMP %.1f", val); + return strlen(reply); + } + if (strncmp(arg, "HEAP ", 5) == 0) { + int val = atoi(arg + 5); + if (val < 0 || val > 300000) { + snprintf(reply, reply_size, "ERR ALERT HEAP range 0-300000 (0=off)"); + return strlen(reply); + } + s_alert_heap_thresh = (uint32_t)val; + config_save_i32("alert_heap", val); + snprintf(reply, reply_size, "OK ALERT HEAP %d", val); + return strlen(reply); + } + if (strcmp(arg, "OFF") == 0) { + s_alert_temp_thresh = 0.0f; + s_alert_heap_thresh = 0; + config_save_i32("alert_temp", 0); + config_save_i32("alert_heap", 0); + snprintf(reply, reply_size, "OK ALERT all disabled"); + return strlen(reply); + } + snprintf(reply, reply_size, "ERR ALERT TEMP |HEAP |OFF"); + return strlen(reply); + } + /* HELP */ if (strcmp(cmd, "HELP") == 0) { int pos = 0; @@ -2207,7 +2316,7 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) "CALIBRATE [STATUS|CLEAR|N] PRESENCE [ON|OFF|THRESHOLD]\n" "CHANSCAN [ON|OFF|NOW|INTERVAL] LED [QUIET|AUTO]\n" "POWERSAVE [ON|OFF] AUTH [secret|OFF] FLOODTHRESH [win]\n" - "LOG RSSI RESET\n" + "ALERT [TEMP |HEAP |OFF] LOG RSSI RESET\n" "OTA POWERTEST [dwell] IDENTIFY REBOOT FACTORY"); return pos; } @@ -2254,6 +2363,8 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) "powersave=%s\n" "auth=%s\n" "flood_thresh=%d/%ds\n" + "alert_temp=%.1f\n" + "alert_heap=%lu\n" "boots=%lu", s_hostname, s_send_frequency, @@ -2276,6 +2387,7 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) s_powersave ? "on" : "off", s_auth_secret[0] ? "on" : "off", s_flood_thresh, s_flood_window_s, + s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh, (unsigned long)s_boot_count); return pos; }