diff --git a/get-started/csi_recv_router/main/app_main.c b/get-started/csi_recv_router/main/app_main.c index bb25661..7186996 100644 --- a/get-started/csi_recv_router/main/app_main.c +++ b/get-started/csi_recv_router/main/app_main.c @@ -193,6 +193,14 @@ static int s_calib_nsub = 0; static float s_pr_scores[PRESENCE_WINDOW]; static uint32_t s_pr_score_idx = 0; +/* Multi-channel scanning */ +#define CHANSCAN_CHANNELS 13 +#define CHANSCAN_DWELL_MS 100 +static bool s_chanscan_enabled = false; +static int s_chanscan_interval_s = 300; /* 5 min default */ +static volatile bool s_chanscan_active = false; +static int64_t s_chanscan_last = 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; @@ -283,6 +291,14 @@ static void config_load_nvs(void) s_baseline_nsub = (int)bl_nsub; } } + int8_t chanscan; + if (nvs_get_i8(h, "chanscan", &chanscan) == ESP_OK) { + s_chanscan_enabled = (chanscan != 0); + } + int32_t chanscan_int; + if (nvs_get_i32(h, "chanscan_int", &chanscan_int) == ESP_OK && chanscan_int >= 60 && chanscan_int <= 3600) { + s_chanscan_interval_s = (int)chanscan_int; + } 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", s_hostname, s_send_frequency, s_tx_power_dbm, s_adaptive, s_motion_threshold, s_ble_enabled, @@ -769,27 +785,50 @@ static int ble_gap_event_cb(struct ble_gap_event *event, void *arg) disc->rssi, disc->addr.val[5], disc->addr.val[4], disc->addr.val[3], disc->addr.val[2], disc->addr.val[1], disc->addr.val[0]); - /* Parse advertisement for device name */ + /* Parse advertisement for device name, manufacturer data, TX power, flags */ struct ble_hs_adv_fields fields; int rc = ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data); char name[32] = ""; - if (rc == 0 && fields.name != NULL && fields.name_len > 0) { - int nlen = fields.name_len < (int)sizeof(name) - 1 ? fields.name_len : (int)sizeof(name) - 1; - memcpy(name, fields.name, nlen); - name[nlen] = '\0'; + uint16_t company_id = 0; + int8_t tx_power = 127; /* 127 = not present */ + uint8_t adv_flags = 0; + + if (rc == 0) { + /* Device name */ + if (fields.name != NULL && fields.name_len > 0) { + int nlen = fields.name_len < (int)sizeof(name) - 1 ? fields.name_len : (int)sizeof(name) - 1; + memcpy(name, fields.name, nlen); + name[nlen] = '\0'; + } + + /* Manufacturer-specific data: first 2 bytes = company ID (little-endian) */ + if (fields.mfg_data != NULL && fields.mfg_data_len >= 2) { + company_id = fields.mfg_data[0] | (fields.mfg_data[1] << 8); + } + + /* TX power level */ + if (fields.tx_pwr_lvl_is_present) { + tx_power = fields.tx_pwr_lvl; + } + + /* Advertisement flags (always present in struct, 0 if not in advert) */ + adv_flags = fields.flags; } - /* Send BLE_DATA via UDP */ - char buf[160]; + /* Send BLE_DATA via UDP with extended format */ + char buf[192]; int len = snprintf(buf, sizeof(buf), - "BLE_DATA,%s,%02x:%02x:%02x:%02x:%02x:%02x,%d,%s,%s\n", + "BLE_DATA,%s,%02x:%02x:%02x:%02x:%02x:%02x,%d,%s,%s,0x%04X,%d,%u\n", s_hostname, disc->addr.val[5], disc->addr.val[4], disc->addr.val[3], disc->addr.val[2], disc->addr.val[1], disc->addr.val[0], disc->rssi, disc->addr.type == BLE_ADDR_PUBLIC ? "pub" : "rnd", - name); + name, + company_id, + (int)tx_power, + (unsigned)adv_flags); if (s_udp_socket >= 0) { sendto(s_udp_socket, buf, len, 0, @@ -847,6 +886,9 @@ static void ble_host_task(void *param) nimble_port_freertos_deinit(); } +/* --- Forward declarations --- */ +static void channel_scan_run(void); + /* --- Adaptive sampling --- */ static void adaptive_task(void *arg) @@ -908,6 +950,15 @@ static void adaptive_task(void *arg) } } + /* Periodic channel scanning */ + if (s_chanscan_enabled && !s_chanscan_active) { + int64_t now = esp_timer_get_time(); + int64_t interval_us = (int64_t)s_chanscan_interval_s * 1000000LL; + if (s_chanscan_last == 0 || (now - s_chanscan_last) >= interval_us) { + channel_scan_run(); + } + } + if (!s_adaptive || s_energy_idx < WANDER_WINDOW) continue; /* Compute mean */ @@ -966,6 +1017,56 @@ static void adaptive_task(void *arg) } } +/* --- Channel scanning --- */ + +static void channel_scan_run(void) +{ + if (s_chanscan_active) return; + s_chanscan_active = true; + + /* Get current AP info for return */ + wifi_ap_record_t ap; + uint8_t home_channel = 1; + if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) { + home_channel = ap.primary; + } + + ESP_LOGI(TAG, "CHANSCAN: starting scan, home channel=%d", home_channel); + + /* Stop ping to pause CSI during scan */ + if (s_ping_handle) { + esp_ping_stop(s_ping_handle); + esp_ping_delete_session(s_ping_handle); + s_ping_handle = NULL; + } + + /* Hop through channels 1-13 */ + for (int ch = 1; ch <= CHANSCAN_CHANNELS; ch++) { + esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE); + vTaskDelay(pdMS_TO_TICKS(CHANSCAN_DWELL_MS)); + } + + /* Return to AP channel */ + esp_wifi_set_channel(home_channel, WIFI_SECOND_CHAN_NONE); + + /* Restart ping */ + wifi_ping_router_start(); + + s_chanscan_last = esp_timer_get_time(); + s_chanscan_active = false; + + /* Emit completion event */ + char event[128]; + int len = snprintf(event, sizeof(event), + "EVENT,%s,chanscan=done channels=%d", + s_hostname, CHANSCAN_CHANNELS); + if (s_udp_socket >= 0) { + sendto(s_udp_socket, event, len, 0, + (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr)); + } + ESP_LOGI(TAG, "CHANSCAN: complete, returned to channel %d", home_channel); +} + /* --- OTA --- */ static void ota_task(void *arg) @@ -1155,12 +1256,13 @@ static void wifi_promiscuous_cb(void *buf, wifi_promiscuous_pkt_type_t type) char probe[192]; int len = snprintf(probe, sizeof(probe), - "PROBE_DATA,%s,%02x:%02x:%02x:%02x:%02x:%02x,%d,%s\n", + "PROBE_DATA,%s,%02x:%02x:%02x:%02x:%02x:%02x,%d,%s,%d\n", s_hostname, hdr->addr2[0], hdr->addr2[1], hdr->addr2[2], hdr->addr2[3], hdr->addr2[4], hdr->addr2[5], pkt->rx_ctrl.rssi, - ssid); + ssid, + pkt->rx_ctrl.channel); if (s_udp_socket >= 0) { sendto(s_udp_socket, probe, len, 0, @@ -1427,7 +1529,7 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) " 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" - " presence=%s pr_score=%.4f", + " presence=%s pr_score=%.4f chanscan=%s", 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, @@ -1439,7 +1541,8 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) s_auth_secret[0] ? "on" : "off", s_flood_thresh, s_flood_window_s, 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"); return strlen(reply); } @@ -1912,6 +2015,46 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) return strlen(reply); } + /* CHANSCAN [ON|OFF|NOW|INTERVAL <60-3600>] */ + if (strcmp(cmd, "CHANSCAN") == 0) { + snprintf(reply, reply_size, "OK CHANSCAN %s interval=%ds active=%s", + s_chanscan_enabled ? "on" : "off", + s_chanscan_interval_s, + s_chanscan_active ? "yes" : "no"); + return strlen(reply); + } + if (strncmp(cmd, "CHANSCAN ", 9) == 0) { + const char *arg = cmd + 9; + if (strncmp(arg, "ON", 2) == 0) { + s_chanscan_enabled = true; + config_save_i8("chanscan", 1); + snprintf(reply, reply_size, "OK CHANSCAN on interval=%ds", s_chanscan_interval_s); + } else if (strncmp(arg, "OFF", 3) == 0) { + s_chanscan_enabled = false; + config_save_i8("chanscan", 0); + snprintf(reply, reply_size, "OK CHANSCAN off"); + } else if (strncmp(arg, "NOW", 3) == 0) { + if (s_chanscan_active) { + snprintf(reply, reply_size, "ERR CHANSCAN already in progress"); + } else { + channel_scan_run(); + snprintf(reply, reply_size, "OK CHANSCAN triggered"); + } + } else if (strncmp(arg, "INTERVAL ", 9) == 0) { + int val = atoi(arg + 9); + if (val < 60 || val > 3600) { + snprintf(reply, reply_size, "ERR CHANSCAN INTERVAL range 60-3600 seconds"); + return strlen(reply); + } + s_chanscan_interval_s = val; + config_save_i32("chanscan_int", (int32_t)val); + snprintf(reply, reply_size, "OK CHANSCAN INTERVAL %ds (saved)", val); + } else { + snprintf(reply, reply_size, "ERR CHANSCAN ON|OFF|NOW|INTERVAL <60-3600>"); + } + return strlen(reply); + } + snprintf(reply, reply_size, "ERR UNKNOWN"); return strlen(reply); }