feat: Add baseline calibration & presence detection (v1.7)
CALIBRATE command captures per-subcarrier CSI amplitudes over a timed window and stores the averaged baseline in NVS. PRESENCE command enables real-time scoring via normalized Euclidean distance against the baseline, with rolling window averaging and 10s holdoff on state transitions. New commands: CALIBRATE [3-60|STATUS|CLEAR], PRESENCE [ON|OFF|THRESHOLD] New NVS keys: bl_amps (blob), bl_nsub, presence, pr_thresh New STATUS fields: presence=, pr_score= New events: calibrate=done, presence=0|1
This commit is contained in:
13
ROADMAP.md
13
ROADMAP.md
@@ -120,6 +120,19 @@ Note: Promiscuous mode (probe/deauth capture) disabled on original ESP32 — bre
|
||||
- [ ] Deep sleep mode with wake-on-CSI-motion
|
||||
- [ ] Battery-optimized duty cycling
|
||||
|
||||
## v1.7 - Baseline Calibration & Presence Detection [DONE]
|
||||
- [x] CALIBRATE command (capture N seconds of CSI with room empty, average per-subcarrier amplitudes, store in NVS)
|
||||
- [x] CALIBRATE STATUS / CALIBRATE CLEAR subcommands
|
||||
- [x] Presence scoring (normalized Euclidean distance of live CSI vs baseline, rolling window)
|
||||
- [x] PRESENCE ON/OFF command (NVS persisted, requires valid baseline)
|
||||
- [x] PRESENCE THRESHOLD command (tunable 0.001-1.0, NVS persisted)
|
||||
- [x] Presence events (`EVENT,<hostname>,presence=<0|1> score=<float>`) with 10s holdoff
|
||||
- [x] Calibration done event (`EVENT,<hostname>,calibrate=done packets=<n> nsub=<n>`)
|
||||
- [x] presence= and pr_score= fields in STATUS reply
|
||||
- [x] NVS persistence for baseline (bl_amps blob, bl_nsub) and presence config
|
||||
- [ ] Tune presence threshold per room with real-world testing
|
||||
- [ ] Pi-side presence event handling in watch daemon
|
||||
|
||||
## v2.0 - Hardware Upgrade (ESP32-S3/C6)
|
||||
|
||||
Requires replacing current ESP32 (original) DevKitC V1 boards with ESP32-S3
|
||||
|
||||
22
TASKS.md
22
TASKS.md
@@ -2,9 +2,11 @@
|
||||
|
||||
**Last Updated:** 2026-02-04
|
||||
|
||||
## Current Sprint: v1.6+ — Validation & Measurement
|
||||
## Current Sprint: v1.7+ — Presence Tuning & Integration
|
||||
|
||||
### P2 - Normal
|
||||
- [ ] Tune presence threshold per room with real-world testing
|
||||
- [ ] Pi-side presence event handling in watch daemon
|
||||
- [ ] Power consumption measurements using POWERTEST + external meter
|
||||
- [ ] Test OTA rollback (flash bad firmware, verify auto-revert)
|
||||
- [ ] Create HA webhook automations for deauth_flood / unknown_probe
|
||||
@@ -16,6 +18,20 @@
|
||||
- [ ] Document esp-radar console features
|
||||
- [ ] Pin mapping for ESP32-DevKitC V1
|
||||
|
||||
## Completed: v1.7 - Baseline Calibration & Presence Detection
|
||||
|
||||
- [x] CALIBRATE command (capture N seconds of CSI, average per-subcarrier amplitudes)
|
||||
- [x] CALIBRATE STATUS / CALIBRATE CLEAR subcommands
|
||||
- [x] Presence scoring (normalized Euclidean distance vs baseline, rolling window of 50)
|
||||
- [x] PRESENCE ON/OFF command (NVS persisted, requires valid baseline)
|
||||
- [x] PRESENCE THRESHOLD command (0.001-1.0, NVS persisted, default 0.05)
|
||||
- [x] Presence events (`EVENT,<hostname>,presence=<0|1> score=<float>`) with 10s holdoff
|
||||
- [x] Calibration done event (`EVENT,<hostname>,calibrate=done packets=<n> nsub=<n>`)
|
||||
- [x] presence= and pr_score= fields in STATUS reply
|
||||
- [x] NVS persistence: bl_amps (blob), bl_nsub (i8), presence (i8), pr_thresh (i32)
|
||||
- [x] config_save_blob / config_erase_key NVS helpers
|
||||
- [x] n_sub field in csi_features_t, amps_out parameter in csi_extract_features
|
||||
|
||||
## Completed: v1.6 - Power Management
|
||||
|
||||
- [x] ESP-IDF power management framework (DFS 240/80 MHz + light sleep)
|
||||
@@ -151,10 +167,10 @@
|
||||
## Notes
|
||||
|
||||
- Adaptive threshold varies by environment; 0.001-0.01 is a good starting range
|
||||
- NVS keys: `send_rate`, `tx_power`, `adaptive`, `threshold`, `ble_scan`, `target_ip`, `target_port`, `hostname`, `boot_count`, `csi_mode`, `hybrid_n`, `auth_secret`, `flood_thresh`, `flood_window`, `scan_rate`, `probe_rate`, `powersave`
|
||||
- NVS keys: `send_rate`, `tx_power`, `adaptive`, `threshold`, `ble_scan`, `target_ip`, `target_port`, `hostname`, `boot_count`, `csi_mode`, `hybrid_n`, `auth_secret`, `flood_thresh`, `flood_window`, `scan_rate`, `probe_rate`, `powersave`, `presence`, `pr_thresh`, `bl_nsub`, `bl_amps`
|
||||
- EVENT packets include sensor hostname: `EVENT,<hostname>,motion=... rate=... wander=...`
|
||||
- ALERT_DATA format: `ALERT_DATA,<hostname>,<deauth|disassoc>,<sender_mac>,<target_mac>,<rssi>` or `ALERT_DATA,<hostname>,deauth_flood,<count>,<window_s>`
|
||||
- STATUS fields: `uptime=`, `uptime_s=`, `heap=`, `rssi=`, `channel=`, `tx_power=`, `rate=`, `csi_rate=`, `hostname=`, `version=`, `adaptive=`, `motion=`, `ble=`, `target=`, `temp=`, `csi_count=`, `boots=`, `rssi_min=`, `rssi_max=`, `csi_mode=`, `hybrid_n=`, `auth=`, `flood_thresh=`, `powersave=`
|
||||
- STATUS fields: `uptime=`, `uptime_s=`, `heap=`, `rssi=`, `channel=`, `tx_power=`, `rate=`, `csi_rate=`, `hostname=`, `version=`, `adaptive=`, `motion=`, `ble=`, `target=`, `temp=`, `csi_count=`, `boots=`, `rssi_min=`, `rssi_max=`, `csi_mode=`, `hybrid_n=`, `auth=`, `flood_thresh=`, `powersave=`, `presence=`, `pr_score=`
|
||||
- PROBE_DATA format: `PROBE_DATA,<hostname>,<mac>,<rssi>,<ssid>`
|
||||
- Probe requests deduped per MAC (default 10s cooldown, tunable via PROBERATE)
|
||||
- mDNS service: `_esp-csi._udp` on data port (for sensor discovery)
|
||||
|
||||
@@ -55,6 +55,14 @@ esp-cmd <host> AUTH mysecret123 # Enable HMAC auth (8-64 char secret)
|
||||
esp-cmd <host> AUTH OFF # Disable auth
|
||||
esp-cmd <host> FLOODTHRESH # Query deauth flood threshold (5/10s)
|
||||
esp-cmd <host> FLOODTHRESH 10 30 # Set: 10 deauths in 30s = flood
|
||||
esp-cmd <host> CALIBRATE # Start baseline capture (default 10s, room must be empty)
|
||||
esp-cmd <host> CALIBRATE 20 # Calibrate for 20 seconds
|
||||
esp-cmd <host> CALIBRATE STATUS # Show baseline info (valid/invalid, nsub)
|
||||
esp-cmd <host> CALIBRATE CLEAR # Delete baseline, disable presence
|
||||
esp-cmd <host> PRESENCE # Query: on/off, baseline, threshold, score
|
||||
esp-cmd <host> PRESENCE ON # Enable presence detection (needs baseline)
|
||||
esp-cmd <host> PRESENCE OFF # Disable presence detection
|
||||
esp-cmd <host> PRESENCE THRESHOLD 0.08 # Set presence threshold (0.001-1.0)
|
||||
esp-cmd <host> REBOOT # Restart device
|
||||
```
|
||||
|
||||
@@ -142,6 +150,38 @@ esp-cmd amber-maple.local THRESHOLD 0.005 # Tune sensitivity
|
||||
# Good starting range: 0.001 - 0.01
|
||||
```
|
||||
|
||||
## Presence Detection
|
||||
|
||||
CSI-based presence detection compares live per-subcarrier amplitudes against
|
||||
a baseline captured with the room empty.
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# 1. Ensure room is empty, CSI is flowing (200+ packets)
|
||||
esp-cmd amber-maple.local CALIBRATE 10 # Capture 10s baseline
|
||||
# Wait for: EVENT,amber-maple,calibrate=done packets=1000 nsub=52
|
||||
|
||||
# 2. Verify baseline
|
||||
esp-cmd amber-maple.local CALIBRATE STATUS # Should show "valid nsub=52"
|
||||
|
||||
# 3. Enable presence detection
|
||||
esp-cmd amber-maple.local PRESENCE ON # Starts scoring
|
||||
|
||||
# 4. Tune threshold (optional)
|
||||
esp-cmd amber-maple.local PRESENCE THRESHOLD 0.08 # Higher = less sensitive
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
- **Baseline:** Average per-subcarrier amplitude over N seconds (stored in NVS)
|
||||
- **Scoring:** Normalized Euclidean distance: `sqrt(sum((live-baseline)^2) / sum(baseline^2))`
|
||||
- **Window:** Rolling average of 50 scores
|
||||
- **Threshold:** Default 0.05 (5% normalized distance)
|
||||
- **Holdoff:** 10s debounce between state transitions
|
||||
- **Events:** `EVENT,<hostname>,presence=1 score=0.0832` / `EVENT,<hostname>,presence=0 score=0.0234`
|
||||
- **Memory:** ~1 KB total (baseline + accumulators + score buffer)
|
||||
|
||||
### LED States
|
||||
|
||||
| LED | Meaning |
|
||||
@@ -250,6 +290,8 @@ CSI_DATA,<hostname>,seq,mac,rssi,rate,...,len,first_word,"[I,Q,...]" # RAW
|
||||
CSI_DATA,<hostname>,seq,mac,rssi,rate,...,len,first_word,"F:rms,std,max,idx,energy" # COMPACT mode
|
||||
BLE_DATA,<hostname>,mac,rssi,pub|rnd,name
|
||||
EVENT,<hostname>,motion=0|1 rate=<hz> wander=<value>
|
||||
EVENT,<hostname>,calibrate=done packets=<n> nsub=<n>
|
||||
EVENT,<hostname>,presence=0|1 score=<float>
|
||||
ALERT_DATA,<hostname>,deauth|disassoc,sender_mac,target_mac,rssi
|
||||
ALERT_DATA,<hostname>,deauth_flood,<count>,<window_s>
|
||||
PROBE_DATA,<hostname>,mac,rssi,ssid
|
||||
@@ -288,6 +330,9 @@ only generated on ESP32-C6 and newer chips.
|
||||
| hybrid_n | 10 | Raw packet interval (hybrid mode) |
|
||||
| auth | on/off | HMAC command authentication |
|
||||
| flood_thresh | 5/10 | Deauth flood: count/window_seconds |
|
||||
| powersave | on/off | WiFi modem sleep |
|
||||
| presence | on/off | Presence detection |
|
||||
| pr_score | 0.0432 | Current presence score (0 = no change from baseline) |
|
||||
|
||||
## PROFILE Sections
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ typedef struct {
|
||||
float amp_std;
|
||||
float amp_max;
|
||||
uint8_t amp_max_idx;
|
||||
uint8_t n_sub;
|
||||
uint32_t energy;
|
||||
} csi_features_t;
|
||||
|
||||
@@ -167,6 +168,31 @@ static int s_deauth_ring_count = 0;
|
||||
static volatile bool s_powertest_running = false;
|
||||
static bool s_powersave = false;
|
||||
|
||||
/* Baseline calibration & presence detection */
|
||||
#define BASELINE_MAX_SUBS 64
|
||||
#define PRESENCE_WINDOW 50
|
||||
#define PRESENCE_HOLDOFF_US 10000000LL /* 10s debounce */
|
||||
#define DEFAULT_PR_THRESH 0.05f /* 5% normalized distance */
|
||||
|
||||
static float s_baseline_amps[BASELINE_MAX_SUBS];
|
||||
static int s_baseline_nsub = 0; /* 0 = no baseline */
|
||||
static bool s_presence_enabled = false;
|
||||
static float s_pr_threshold = DEFAULT_PR_THRESH;
|
||||
static volatile float s_pr_last_score = 0.0f;
|
||||
static volatile bool s_presence_detected = false;
|
||||
static int64_t s_last_presence_time = 0;
|
||||
|
||||
/* Calibration state (transient) */
|
||||
static volatile bool s_calibrating = false;
|
||||
static float s_calib_accum[BASELINE_MAX_SUBS];
|
||||
static volatile uint32_t s_calib_count = 0;
|
||||
static int s_calib_target = 0;
|
||||
static int s_calib_nsub = 0;
|
||||
|
||||
/* Presence scoring buffer */
|
||||
static float s_pr_scores[PRESENCE_WINDOW];
|
||||
static uint32_t s_pr_score_idx = 0;
|
||||
|
||||
/* Probe dedup rate (moved before config_load_nvs for NVS access) */
|
||||
#define PROBE_DEDUP_DEFAULT_US 10000000LL
|
||||
static int64_t s_probe_dedup_us = PROBE_DEDUP_DEFAULT_US;
|
||||
@@ -242,10 +268,26 @@ static void config_load_nvs(void)
|
||||
if (nvs_get_i8(h, "powersave", &powersave) == ESP_OK) {
|
||||
s_powersave = (powersave != 0);
|
||||
}
|
||||
int8_t presence;
|
||||
if (nvs_get_i8(h, "presence", &presence) == ESP_OK) {
|
||||
s_presence_enabled = (presence != 0);
|
||||
}
|
||||
int32_t pr_thresh;
|
||||
if (nvs_get_i32(h, "pr_thresh", &pr_thresh) == ESP_OK && pr_thresh > 0) {
|
||||
s_pr_threshold = (float)pr_thresh / 1000000.0f;
|
||||
}
|
||||
int8_t bl_nsub;
|
||||
if (nvs_get_i8(h, "bl_nsub", &bl_nsub) == ESP_OK && bl_nsub > 0 && bl_nsub <= BASELINE_MAX_SUBS) {
|
||||
size_t bl_len = (size_t)bl_nsub * sizeof(float);
|
||||
if (nvs_get_blob(h, "bl_amps", s_baseline_amps, &bl_len) == ESP_OK) {
|
||||
s_baseline_nsub = (int)bl_nsub;
|
||||
}
|
||||
}
|
||||
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",
|
||||
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",
|
||||
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);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "NVS: no saved config, using defaults");
|
||||
}
|
||||
@@ -296,6 +338,28 @@ static esp_err_t config_save_str(const char *key, const char *value)
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t config_save_blob(const char *key, const void *data, size_t len)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
err = nvs_set_blob(h, key, data, len);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t config_erase_key(const char *key)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
err = nvs_erase_key(h, key);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* --- LED --- */
|
||||
|
||||
static void led_gpio_init(void)
|
||||
@@ -372,7 +436,7 @@ static void led_task(void *arg)
|
||||
|
||||
/* --- CSI feature extraction --- */
|
||||
|
||||
static void csi_extract_features(const int8_t *buf, int len, float gain, csi_features_t *out)
|
||||
static void csi_extract_features(const int8_t *buf, int len, float gain, csi_features_t *out, float *amps_out)
|
||||
{
|
||||
int n_pairs = len / 2;
|
||||
if (n_pairs > 64) n_pairs = 64;
|
||||
@@ -399,6 +463,10 @@ static void csi_extract_features(const int8_t *buf, int len, float gain, csi_fea
|
||||
}
|
||||
}
|
||||
|
||||
if (amps_out && n_pairs > 0) {
|
||||
memcpy(amps_out, amps, n_pairs * sizeof(float));
|
||||
}
|
||||
|
||||
float mean_amp = (n_pairs > 0) ? sum_amp / n_pairs : 0.0f;
|
||||
|
||||
/* Pass 2: compute variance */
|
||||
@@ -413,6 +481,7 @@ static void csi_extract_features(const int8_t *buf, int len, float gain, csi_fea
|
||||
out->amp_std = sqrtf(var);
|
||||
out->amp_max = max_amp;
|
||||
out->amp_max_idx = max_idx;
|
||||
out->n_sub = (uint8_t)n_pairs;
|
||||
out->energy = energy;
|
||||
}
|
||||
|
||||
@@ -457,10 +526,41 @@ static void wifi_csi_rx_cb(void *ctx, wifi_csi_info_t *info)
|
||||
ESP_LOGD(TAG, "compensate_gain %f, agc_gain %d, fft_gain %d", compensate_gain, agc_gain, fft_gain);
|
||||
#endif
|
||||
|
||||
/* Extract features (used for compact mode and adaptive sampling) */
|
||||
/* Extract features (used for compact mode, adaptive sampling, calibration, presence) */
|
||||
csi_features_t features = {0};
|
||||
if (s_csi_mode != CSI_MODE_RAW || s_adaptive) {
|
||||
csi_extract_features(info->buf, info->len, compensate_gain, &features);
|
||||
bool need_amps = s_calibrating || (s_presence_enabled && s_baseline_nsub > 0);
|
||||
static float s_live_amps[64];
|
||||
if (s_csi_mode != CSI_MODE_RAW || s_adaptive || need_amps) {
|
||||
csi_extract_features(info->buf, info->len, compensate_gain, &features,
|
||||
need_amps ? s_live_amps : NULL);
|
||||
}
|
||||
|
||||
/* Calibration: accumulate per-subcarrier amplitudes */
|
||||
if (s_calibrating && features.n_sub > 0) {
|
||||
if (s_calib_count == 0) {
|
||||
s_calib_nsub = features.n_sub;
|
||||
memset(s_calib_accum, 0, sizeof(s_calib_accum));
|
||||
}
|
||||
int nsub = (features.n_sub < s_calib_nsub) ? features.n_sub : s_calib_nsub;
|
||||
for (int i = 0; i < nsub; i++) {
|
||||
s_calib_accum[i] += s_live_amps[i];
|
||||
}
|
||||
s_calib_count++;
|
||||
}
|
||||
|
||||
/* Presence scoring: normalized Euclidean distance vs baseline */
|
||||
if (!s_calibrating && s_presence_enabled && s_baseline_nsub > 0 && features.n_sub > 0) {
|
||||
int nsub = (features.n_sub < s_baseline_nsub) ? features.n_sub : s_baseline_nsub;
|
||||
float sum_diff_sq = 0.0f;
|
||||
float sum_base_sq = 0.0f;
|
||||
for (int i = 0; i < nsub; i++) {
|
||||
float d = s_live_amps[i] - s_baseline_amps[i];
|
||||
sum_diff_sq += d * d;
|
||||
sum_base_sq += s_baseline_amps[i] * s_baseline_amps[i];
|
||||
}
|
||||
float score = (sum_base_sq > 0.0f) ? sqrtf(sum_diff_sq / sum_base_sq) : 0.0f;
|
||||
s_pr_scores[s_pr_score_idx % PRESENCE_WINDOW] = score;
|
||||
s_pr_score_idx++;
|
||||
}
|
||||
|
||||
/* Determine whether to send raw I/Q data this packet */
|
||||
@@ -754,6 +854,60 @@ static void adaptive_task(void *arg)
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
/* Calibration finalization (NVS I/O happens here, not in WiFi callback) */
|
||||
if (s_calibrating && s_calib_count >= (uint32_t)s_calib_target) {
|
||||
int nsub = s_calib_nsub;
|
||||
uint32_t count = s_calib_count;
|
||||
for (int i = 0; i < nsub; i++) {
|
||||
s_baseline_amps[i] = s_calib_accum[i] / (float)count;
|
||||
}
|
||||
s_baseline_nsub = nsub;
|
||||
config_save_blob("bl_amps", s_baseline_amps, nsub * sizeof(float));
|
||||
config_save_i8("bl_nsub", (int8_t)nsub);
|
||||
|
||||
char event[128];
|
||||
int len = snprintf(event, sizeof(event),
|
||||
"EVENT,%s,calibrate=done packets=%lu nsub=%d",
|
||||
s_hostname, (unsigned long)count, nsub);
|
||||
if (s_udp_socket >= 0) {
|
||||
sendto(s_udp_socket, event, len, 0,
|
||||
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
|
||||
}
|
||||
ESP_LOGI(TAG, "Calibration done: %lu packets, %d subcarriers",
|
||||
(unsigned long)count, nsub);
|
||||
s_calibrating = false;
|
||||
}
|
||||
|
||||
/* Presence event emission */
|
||||
if (s_presence_enabled && s_baseline_nsub > 0 && s_pr_score_idx >= PRESENCE_WINDOW) {
|
||||
float sum = 0.0f;
|
||||
for (int i = 0; i < PRESENCE_WINDOW; i++) {
|
||||
sum += s_pr_scores[i];
|
||||
}
|
||||
float mean_score = sum / PRESENCE_WINDOW;
|
||||
s_pr_last_score = mean_score;
|
||||
|
||||
bool detected = (mean_score > s_pr_threshold);
|
||||
if (detected != s_presence_detected) {
|
||||
int64_t now = esp_timer_get_time();
|
||||
if (now - s_last_presence_time > PRESENCE_HOLDOFF_US) {
|
||||
s_presence_detected = detected;
|
||||
s_last_presence_time = now;
|
||||
|
||||
char event[128];
|
||||
int len = snprintf(event, sizeof(event),
|
||||
"EVENT,%s,presence=%d score=%.4f",
|
||||
s_hostname, detected ? 1 : 0, mean_score);
|
||||
if (s_udp_socket >= 0) {
|
||||
sendto(s_udp_socket, event, len, 0,
|
||||
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
|
||||
}
|
||||
ESP_LOGI(TAG, "Presence: %s (score=%.4f threshold=%.4f)",
|
||||
detected ? "detected" : "cleared", mean_score, s_pr_threshold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!s_adaptive || s_energy_idx < WANDER_WINDOW) continue;
|
||||
|
||||
/* Compute mean */
|
||||
@@ -1272,7 +1426,8 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
|
||||
"OK STATUS uptime=%s uptime_s=%lld heap=%lu rssi=%d channel=%d tx_power=%d rate=%d csi_rate=%d"
|
||||
" hostname=%s version=%s adaptive=%s motion=%d ble=%s target=%s:%d"
|
||||
" temp=%.1f csi_count=%lu boots=%lu rssi_min=%d rssi_max=%d"
|
||||
" csi_mode=%s hybrid_n=%d auth=%s flood_thresh=%d/%d powersave=%s",
|
||||
" csi_mode=%s hybrid_n=%d auth=%s flood_thresh=%d/%d powersave=%s"
|
||||
" presence=%s pr_score=%.4f",
|
||||
uptime_str, (long long)up, (unsigned long)heap, rssi, channel, (int)s_tx_power_dbm,
|
||||
s_send_frequency, actual_rate,
|
||||
s_hostname, app_desc->version,
|
||||
@@ -1283,7 +1438,8 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
|
||||
csi_mode_str, s_hybrid_interval,
|
||||
s_auth_secret[0] ? "on" : "off",
|
||||
s_flood_thresh, s_flood_window_s,
|
||||
s_powersave ? "on" : "off");
|
||||
s_powersave ? "on" : "off",
|
||||
s_presence_enabled ? "on" : "off", s_pr_last_score);
|
||||
return strlen(reply);
|
||||
}
|
||||
|
||||
@@ -1656,6 +1812,106 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
|
||||
return strlen(reply);
|
||||
}
|
||||
|
||||
/* CALIBRATE [seconds | STATUS | CLEAR] */
|
||||
if (strcmp(cmd, "CALIBRATE") == 0 || strncmp(cmd, "CALIBRATE ", 10) == 0) {
|
||||
const char *arg = (cmd[9] == ' ') ? cmd + 10 : "";
|
||||
|
||||
if (strcmp(arg, "STATUS") == 0) {
|
||||
if (s_calibrating) {
|
||||
snprintf(reply, reply_size, "OK CALIBRATE in_progress packets=%lu target=%d",
|
||||
(unsigned long)s_calib_count, s_calib_target);
|
||||
} else if (s_baseline_nsub > 0) {
|
||||
snprintf(reply, reply_size, "OK CALIBRATE baseline valid nsub=%d", s_baseline_nsub);
|
||||
} else {
|
||||
snprintf(reply, reply_size, "OK CALIBRATE baseline none");
|
||||
}
|
||||
return strlen(reply);
|
||||
}
|
||||
|
||||
if (strcmp(arg, "CLEAR") == 0) {
|
||||
s_baseline_nsub = 0;
|
||||
s_presence_enabled = false;
|
||||
s_presence_detected = false;
|
||||
s_pr_last_score = 0.0f;
|
||||
config_erase_key("bl_amps");
|
||||
config_erase_key("bl_nsub");
|
||||
config_save_i8("presence", 0);
|
||||
snprintf(reply, reply_size, "OK CALIBRATE cleared (presence off)");
|
||||
return strlen(reply);
|
||||
}
|
||||
|
||||
if (s_calibrating) {
|
||||
snprintf(reply, reply_size, "ERR CALIBRATE already running");
|
||||
return strlen(reply);
|
||||
}
|
||||
if (s_csi_count < 200) {
|
||||
snprintf(reply, reply_size, "ERR CALIBRATE need 200+ CSI packets first (have %lu)",
|
||||
(unsigned long)s_csi_count);
|
||||
return strlen(reply);
|
||||
}
|
||||
|
||||
int seconds = 10;
|
||||
if (arg[0] != '\0') {
|
||||
seconds = atoi(arg);
|
||||
}
|
||||
if (seconds < 3 || seconds > 60) {
|
||||
snprintf(reply, reply_size, "ERR CALIBRATE range 3-60 seconds");
|
||||
return strlen(reply);
|
||||
}
|
||||
|
||||
/* Start calibration */
|
||||
s_calib_count = 0;
|
||||
s_calib_nsub = 0;
|
||||
s_calib_target = seconds * s_send_frequency;
|
||||
if (s_calib_target < 30) s_calib_target = 30;
|
||||
s_calibrating = true;
|
||||
snprintf(reply, reply_size, "OK CALIBRATE started %ds target=%d packets", seconds, s_calib_target);
|
||||
return strlen(reply);
|
||||
}
|
||||
|
||||
/* PRESENCE [ON|OFF|THRESHOLD <val>] */
|
||||
if (strcmp(cmd, "PRESENCE") == 0) {
|
||||
snprintf(reply, reply_size, "OK PRESENCE %s baseline=%s threshold=%.4f score=%.4f",
|
||||
s_presence_enabled ? "on" : "off",
|
||||
s_baseline_nsub > 0 ? "yes" : "no",
|
||||
s_pr_threshold, s_pr_last_score);
|
||||
return strlen(reply);
|
||||
}
|
||||
if (strncmp(cmd, "PRESENCE ", 9) == 0) {
|
||||
const char *arg = cmd + 9;
|
||||
if (strncmp(arg, "ON", 2) == 0) {
|
||||
if (s_baseline_nsub == 0) {
|
||||
snprintf(reply, reply_size, "ERR PRESENCE need baseline (run CALIBRATE first)");
|
||||
return strlen(reply);
|
||||
}
|
||||
s_presence_enabled = true;
|
||||
s_presence_detected = false;
|
||||
s_pr_last_score = 0.0f;
|
||||
s_pr_score_idx = 0;
|
||||
s_last_presence_time = 0;
|
||||
config_save_i8("presence", 1);
|
||||
snprintf(reply, reply_size, "OK PRESENCE on threshold=%.4f", s_pr_threshold);
|
||||
} else if (strncmp(arg, "OFF", 3) == 0) {
|
||||
s_presence_enabled = false;
|
||||
s_presence_detected = false;
|
||||
s_pr_last_score = 0.0f;
|
||||
config_save_i8("presence", 0);
|
||||
snprintf(reply, reply_size, "OK PRESENCE off");
|
||||
} else if (strncmp(arg, "THRESHOLD ", 10) == 0) {
|
||||
float val = strtof(arg + 10, NULL);
|
||||
if (val < 0.001f || val > 1.0f) {
|
||||
snprintf(reply, reply_size, "ERR PRESENCE THRESHOLD range 0.001-1.0");
|
||||
return strlen(reply);
|
||||
}
|
||||
s_pr_threshold = val;
|
||||
config_save_i32("pr_thresh", (int32_t)(val * 1000000.0f));
|
||||
snprintf(reply, reply_size, "OK PRESENCE THRESHOLD %.4f", val);
|
||||
} else {
|
||||
snprintf(reply, reply_size, "ERR PRESENCE ON|OFF|THRESHOLD <0.001-1.0>");
|
||||
}
|
||||
return strlen(reply);
|
||||
}
|
||||
|
||||
snprintf(reply, reply_size, "ERR UNKNOWN");
|
||||
return strlen(reply);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user