feat: Add ALERT command for temp/heap threshold monitoring

ALERT TEMP <celsius> and ALERT HEAP <bytes> emit EVENT packets
when thresholds are crossed (60s holdoff). NVS-persisted, shown
in STATUS and CONFIG. Temp alerts require SOC_TEMP_SENSOR_SUPPORTED.
This commit is contained in:
user
2026-02-14 17:40:12 +01:00
parent 5d37bde414
commit ce5205eb29

View File

@@ -211,6 +211,15 @@ static int64_t s_chanscan_last = 0;
#define PROBE_DEDUP_DEFAULT_US 10000000LL #define PROBE_DEDUP_DEFAULT_US 10000000LL
static int64_t s_probe_dedup_us = PROBE_DEDUP_DEFAULT_US; 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 --- */ /* --- NVS helpers --- */
static void config_load_nvs(void) 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) { if (nvs_get_i8(h, "csi_enabled", &csi_en) == ESP_OK) {
s_csi_enabled = (csi_en != 0); 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); 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_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_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 { } else {
ESP_LOGI(TAG, "NVS: no saved config, using defaults"); 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; if (!s_adaptive || s_energy_idx < WANDER_WINDOW) continue;
/* Compute mean */ /* 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" " 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" " 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" " 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" " nvs_used=%lu nvs_free=%lu nvs_total=%lu part_size=%lu"
" built=%s_%s idf=%s chip=%sr%dc%d", " built=%s_%s idf=%s chip=%sr%dc%d",
uptime_str, (long long)up, (unsigned long)heap, rssi, channel, (int)s_tx_power_dbm, 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_powersave ? "on" : "off",
s_presence_enabled ? "on" : "off", s_pr_last_score, s_presence_enabled ? "on" : "off", s_pr_last_score,
s_chanscan_enabled ? "on" : "off", s_led_quiet ? "quiet" : "auto", 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.used_entries,
(unsigned long)nvs_stats.free_entries, (unsigned long)nvs_stats.free_entries,
(unsigned long)nvs_stats.total_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); 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 <c>|HEAP <bytes>|OFF");
return strlen(reply);
}
/* HELP */ /* HELP */
if (strcmp(cmd, "HELP") == 0) { if (strcmp(cmd, "HELP") == 0) {
int pos = 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" "CALIBRATE [STATUS|CLEAR|N] PRESENCE [ON|OFF|THRESHOLD]\n"
"CHANSCAN [ON|OFF|NOW|INTERVAL] LED [QUIET|AUTO]\n" "CHANSCAN [ON|OFF|NOW|INTERVAL] LED [QUIET|AUTO]\n"
"POWERSAVE [ON|OFF] AUTH [secret|OFF] FLOODTHRESH <n> [win]\n" "POWERSAVE [ON|OFF] AUTH [secret|OFF] FLOODTHRESH <n> [win]\n"
"LOG <NONE|ERROR|WARN|INFO|DEBUG|VERBOSE> RSSI RESET\n" "ALERT [TEMP <c>|HEAP <bytes>|OFF] LOG <level> RSSI RESET\n"
"OTA <url> POWERTEST [dwell] IDENTIFY REBOOT FACTORY"); "OTA <url> POWERTEST [dwell] IDENTIFY REBOOT FACTORY");
return pos; return pos;
} }
@@ -2254,6 +2363,8 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
"powersave=%s\n" "powersave=%s\n"
"auth=%s\n" "auth=%s\n"
"flood_thresh=%d/%ds\n" "flood_thresh=%d/%ds\n"
"alert_temp=%.1f\n"
"alert_heap=%lu\n"
"boots=%lu", "boots=%lu",
s_hostname, s_hostname,
s_send_frequency, 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_powersave ? "on" : "off",
s_auth_secret[0] ? "on" : "off", s_auth_secret[0] ? "on" : "off",
s_flood_thresh, s_flood_window_s, s_flood_thresh, s_flood_window_s,
s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh,
(unsigned long)s_boot_count); (unsigned long)s_boot_count);
return pos; return pos;
} }