Merge branch 'update_csi_viewer_html' into 'master'

feat: update_csi_viewer_html & add esp wifi sensing example

See merge request esp-components/esp-csi-ext!75
This commit is contained in:
Ning Yu Xiang 2026-04-22 15:33:54 +08:00
commit 8633d67152
21 changed files with 5908 additions and 407 deletions

View file

@ -73,6 +73,7 @@ before_script:
get-started/csi_recv_router
esp-radar/console_test
esp-radar/connect_rainmaker
esp-radar/wifi_sensing_demo
esp-crab/master_recv
esp-crab/slave_recv
esp-crab/slave_send
@ -82,6 +83,7 @@ before_script:
get-started/csi_recv_router: esp32 esp32s3 esp32s2 esp32c3 esp32c6 esp32c5 esp32c61
esp-radar/console_test: esp32 esp32s3 esp32s2 esp32c3 esp32c6 esp32c5 esp32c61
esp-radar/connect_rainmaker: esp32s3 esp32s2 esp32c3 esp32c6 esp32c5 esp32c61
esp-radar/wifi_sensing_demo: esp32 esp32s3 esp32s2 esp32c3 esp32c6 esp32c5 esp32c61
esp-crab/master_recv: esp32c5
esp-crab/slave_recv: esp32c5
esp-crab/slave_send: esp32c5

View file

@ -41,6 +41,7 @@ Provides some applications using CSI data, including RainMaker cloud reporting a
- [connect_rainmaker](./examples/esp-radar/connect_rainmaker) demonstrates capturing CSI data and uploading it to Espressif's RainMaker cloud platform.
- [console_test](./examples/esp-radar/console_test) demonstrates an interactive console that allows dynamic configuration and capture of CSI data, with applications for human activity detection algorithms.
- [wifi_sensing_demo](./examples/esp-radar/wifi_sensing_demo) demonstrates motion and human presence detection on top of the `esp_wifi_sensing` component, with on-site training, LED feedback, and a browser-based Web Serial monitor for live diagnostics and tuning.
## How to get CSI

View file

@ -43,6 +43,7 @@
- [connect_rainmaker](./examples/esp-radar/connect_rainmaker) 演示了将 CSI 数据捕获并上传到 Espressif 的 RainMaker 云平台
- [console_test](./examples/esp-radar/console_test) 演示一个交互式控制台,允许动态配置和捕获 CSI 数据,并提供了人体活动检测的算法应用
- [wifi_sensing_demo](./examples/esp-radar/wifi_sensing_demo) 基于 `esp_wifi_sensing` 组件的 Wi-Fi 感知演示包含运动和人体存在检测、现场训练标定、LED 状态反馈,以及配套的浏览器端 Web Serial 监控页面,用于实时诊断与调参
## 如何获取CSI

View file

@ -7,6 +7,7 @@ This directory contains multiple example projects for esp-csi. These examples ar
- `get-started/csi_recv_router`: Receives CSI in router communication mode by pinging a router and parsing the CSI from its reply packets.
- `esp-radar/connect_rainmaker`: Connects CSI data to Espressif's Rainmaker cloud platform for remote visualization or control.
- `esp-radar/console_test`: A console-based testing example for debugging and evaluating CSI data and algorithm performance.
- `esp-radar/wifi_sensing_demo`: A Wi-Fi sensing demo built on the `esp_wifi_sensing` component, featuring motion and human presence detection, on-site training, LED feedback, and a browser-based Web Serial monitor for live diagnostics and tuning.
- `esp-crab/master_recv`: The master receiver for the esp-crab hardware platform; responsible for acquiring and parsing Wi-Fi CIR/CSI data.
- `esp-crab/slave_recv`: The slave receiver on the esp-crab platform, assisting the master receiver with multi-channel data collection.
- `esp-crab/slave_send`: The transmitter on the esp-crab platform, responsible for sending packets periodically for CSI extraction by other nodes.

View file

@ -7,6 +7,7 @@
- `get-started/csi_recv_router`:在路由器通信模式下接收 CSI通过 ping 路由器并解析其应答包中的 CSI 信息。
- `esp-radar/connect_rainmaker`:将 CSI 数据与 Espressif 的云平台 Rainmaker 连接,用于远程展示或控制。
- `esp-radar/console_test`:用于控制台调试测试 CSI 数据和算法效果的示例。
- `esp-radar/wifi_sensing_demo`:基于 `esp_wifi_sensing` 组件的 Wi-Fi 感知演示示例包含运动和人体存在检测、现场训练标定、LED 状态反馈,以及配套的浏览器端 Web Serial 监控页面,便于调试和调参。
- `esp-crab/master_recv`esp-crab 硬件平台上的主接收端,支持获取并解析 Wi-Fi CIR/CSI 数据。
- `esp-crab/slave_recv`esp-crab 平台的从接收端,辅助主接收端进行多通道数据收集。
- `esp-crab/slave_send`esp-crab 平台的发送端,负责定时发送数据包供其他节点进行接收和 CSI 提取。

View file

@ -0,0 +1,17 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
add_compile_options(-fdiagnostics-color=always)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
message("EXTRA_COMPONENT_DIRS: " ${EXTRA_COMPONENT_DIRS})
string(REGEX REPLACE ".*/\(.*\)" "\\1" CURDIR ${CMAKE_CURRENT_SOURCE_DIR})
project(${CURDIR})
git_describe(PROJECT_VERSION ${COMPONENT_DIR})
message("Project commit: " ${PROJECT_VERSION})

View file

@ -0,0 +1,96 @@
# ESP Wi-Fi Sensing Demo
This directory contains a self-contained ESP-IDF demo used to exercise the
`esp_wifi_sensing` component in a realistic bring-up flow.
The example combines:
- Wi-Fi STA connection and sensing FSM startup
- Three monitored channels: the connected AP plus two fixed peer MAC addresses
- A status LED policy for quick visual feedback
- A lightweight serial protocol that can be consumed by the browser-based Web
Serial monitor in `tools/web_serial_monitor.html`
The example follows the same public workflow recommended by the component:
1. Create the FSM with `esp_wifi_sensing_fsm_create()`.
2. Add the AP channel and two extra peer channels.
3. Register ACTIVE / INACTIVE callbacks.
4. Start the FSM with `esp_wifi_sensing_fsm_control(..., ESP_WIFI_SENSING_FSM_CTRL_START, NULL)`.
5. Optionally keep CSI traffic flowing with `esp_wifi_sensing_fsm_ping_router_start()`.
## Directory Layout
- `main/app_main.c`: example entry point and sensing FSM wiring
- `main/led_control.*`: LED backend and state mapping
- `main/web_serial_monitor.*`: line-oriented serial protocol for diagnostics and runtime tuning
- `tools/web_serial_monitor.html`: browser UI for charts, live diagnostics, and configuration
## Build and Run
Open the `wifi_sensing_demo` directory as an ESP-IDF project, then build and
flash it in the usual way:
```bash
idf.py set-target <chip>
idf.py flash monitor
```
The example uses `protocol_examples_common` for Wi-Fi provisioning, so configure
your Wi-Fi credentials before flashing if your local workflow requires it.
## Web Serial Monitor
When `CONFIG_ESP_WIFI_SENSING_WEB_SERIAL_ENABLE=y`, the firmware emits
line-delimited JSON messages prefixed with `HMS:` and accepts commands prefixed
with `HMSCMD `. This allows ordinary ESP logs and structured diagnostics to
share the same serial port.
The browser UI currently focuses on the minimum public diagnostics exported by
the component: `jitter_value`, `smooth_scaled`, `enter_level_scaled`,
`exit_level_scaled`, `state`, and `init_stage`.
The serial monitor also exposes the main runtime controls used by the demo:
- `HMSCMD HELLO`
- `HMSCMD START_STREAM`
- `HMSCMD STOP_STREAM`
- `HMSCMD FSM_START`
- `HMSCMD FSM_STOP`
- `HMSCMD RESET_BASELINE`
- `HMSCMD GET_CFG ALL`
- `HMSCMD GET_CFG <peer>`
- `HMSCMD GET_RUNTIME`
- `HMSCMD SET_AMPLITUDE_LOG on`
- `HMSCMD SET_AMPLITUDE_LOG off`
- `HMSCMD SET_CFG <peer> motion_sensitivity=<float> active_jitter_min=<float> active_filter_ms=<ms>`
Here `<peer>` can be either the demo name (`AP`, `MAC_1`, `MAC_2`) or the peer
MAC string.
To use the browser monitor:
1. Build and run the firmware.
2. Open `tools/web_serial_monitor.html` in a Chromium-based browser.
3. Connect to the board through Web Serial.
4. Inspect live channel diagnostics or update channel parameters from the page.
If the browser blocks `file://` access for Web Serial, serve the directory
locally:
```bash
cd tools
python3 -m http.server
```
Then open `http://127.0.0.1:8000/web_serial_monitor.html`.
## Demo Behavior
- The AP BSSID channel is treated as the primary channel for LED feedback.
- The LED blinks while Wi-Fi is disconnected.
- When the AP channel becomes ACTIVE, the LED turns fully on.
- When the AP channel returns to INACTIVE, the LED fades out.
- The two fixed peer channels remain visible in logs and the browser monitor as
extra sensing references.

View file

@ -0,0 +1,12 @@
set(reqs esp_wifi_sensing
protocol_examples_common
driver
esp_driver_ledc
led_strip)
idf_component_register(SRCS "app_main.c"
"led_control.c"
"web_serial_monitor.c"
INCLUDE_DIRS "."
REQUIRES ${reqs}
PRIV_REQUIRES esp_driver_usb_serial_jtag)

View file

@ -0,0 +1,55 @@
menu "Wi-Fi sensing demo"
choice ESP_WIFI_SENSING_DEMO_LED_TYPE
prompt "LED type"
default ESP_WIFI_SENSING_DEMO_LED_TYPE_GPIO
help
Choose between a single GPIO LED and a single WS2812 LED.
config ESP_WIFI_SENSING_DEMO_LED_TYPE_GPIO
bool "GPIO LED"
config ESP_WIFI_SENSING_DEMO_LED_TYPE_WS2812
bool "WS2812 LED"
endchoice
config ESP_WIFI_SENSING_DEMO_LED_GPIO
int "LED GPIO"
default 2
help
GPIO used for the LED output. For WS2812 this is the data pin.
if ESP_WIFI_SENSING_DEMO_LED_TYPE_WS2812
choice ESP_WIFI_SENSING_DEMO_WS2812_BACKEND
prompt "WS2812 backend"
default ESP_WIFI_SENSING_DEMO_WS2812_BACKEND_RMT
help
Select the peripheral used to drive the WS2812 LED.
config ESP_WIFI_SENSING_DEMO_WS2812_BACKEND_RMT
bool "RMT backend"
config ESP_WIFI_SENSING_DEMO_WS2812_BACKEND_SPI
bool "SPI backend"
endchoice
endif
config ESP_WIFI_SENSING_WEB_SERIAL_ENABLE
bool "Enable Web Serial monitor output"
default y
help
Stream sensing diagnostics to the host over the standard serial console.
The browser Web Serial page can connect to whichever serial port is
exposed by the current console configuration.
config ESP_WIFI_SENSING_WEB_SERIAL_STREAM_PERIOD_MS
int "Web Serial stream period (ms)"
default 50
range 20 1000
depends on ESP_WIFI_SENSING_WEB_SERIAL_ENABLE
help
Period used to push one diagnostic snapshot per channel to the Web Serial UI.
endmenu

View file

@ -0,0 +1,240 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <inttypes.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "nvs_flash.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_wifi.h"
#include "esp_now.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "esp_wifi_sensing.h"
#include "protocol_examples_common.h"
#include "led_control.h"
#include "web_serial_monitor.h"
static const char *TAG = "wifi_sensing_demo";
static esp_wifi_sensing_fsm_handle_t s_hms = NULL;
static uint8_t s_mac_ap[6] = {0};
/*
* The demo monitors three channels:
* 1. The connected AP BSSID.
* 2. A fixed CSI sender peer.
* 3. A second fixed peer used as an extra reference channel.
*/
static const uint8_t CONFIG_CSI_SEND_MAC[] = {0x1a, 0x00, 0x00, 0x00, 0x00, 0x00};
static const uint8_t CONFIG_CSI_SEND_MAC_2[] = {0x1a, 0x00, 0x00, 0x00, 0x00, 0x01};
static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
(void)arg;
(void)event_data;
if (event_base == WIFI_EVENT) {
if (event_id == WIFI_EVENT_STA_CONNECTED) {
led_set_wifi_connected(true);
} else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
led_set_wifi_connected(false);
}
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
led_set_wifi_connected(true);
}
}
static void espnow_recv_cb(const esp_now_recv_info_t *recv_info, const uint8_t *data, int data_len)
{
if (!recv_info || !data || data_len <= 0) {
return;
}
if (memcmp(recv_info->src_addr, CONFIG_CSI_SEND_MAC_2, 6) != 0) {
/* Keep the console focused on the dedicated CSI sender. */
return;
}
if (data_len >= (int)sizeof(uint32_t)) {
uint32_t cnt = 0;
memcpy(&cnt, data, sizeof(cnt));
ESP_LOGD(TAG, "ESPNOW RX <- " MACSTR " len=%d cnt=%" PRIu32, MAC2STR(recv_info->src_addr), data_len, cnt);
} else {
ESP_LOGD(TAG, "ESPNOW RX <- " MACSTR " len=%d", MAC2STR(recv_info->src_addr), data_len);
}
}
static void espnow_rx_init(void)
{
esp_err_t err = esp_now_init();
if (err == ESP_ERR_ESPNOW_EXIST) {
err = ESP_OK;
}
ESP_ERROR_CHECK(err);
/*
* The current sender keeps ESPNOW encryption disabled.
* Setting a PMK here is optional and harmless for the receiver path.
*/
ESP_ERROR_CHECK(esp_now_set_pmk((uint8_t *)"pmk1234567890123"));
ESP_ERROR_CHECK(esp_now_register_recv_cb(espnow_recv_cb));
/*
* Adding a broadcast peer is not strictly required for RX, but it makes the
* setup more tolerant across targets and console workflows.
*/
if (!esp_now_is_peer_exist((const uint8_t[6]) {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff
})) {
esp_now_peer_info_t peer = {0};
memcpy(peer.peer_addr, (const uint8_t[6]) {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff
}, 6);
peer.channel = 0; /* Follow the current STA channel from the connected AP. */
peer.encrypt = false;
peer.ifidx = WIFI_IF_STA;
ESP_ERROR_CHECK(esp_now_add_peer(&peer));
}
}
static const char *channel_name(const uint8_t mac[6])
{
if (memcmp(mac, s_mac_ap, 6) == 0) {
return "AP";
}
if (memcmp(mac, CONFIG_CSI_SEND_MAC, 6) == 0) {
return "MAC_1";
}
if (memcmp(mac, CONFIG_CSI_SEND_MAC_2, 6) == 0) {
return "MAC_2";
}
return "?";
}
static void on_motion_event(esp_wifi_sensing_fsm_handle_t handle,
const uint8_t peer_mac[6],
esp_wifi_sensing_fsm_event_t event,
uint32_t data,
void *usr_data)
{
(void)handle;
(void)usr_data;
const char *name = channel_name(peer_mac);
bool is_ap_channel = (memcmp(peer_mac, s_mac_ap, 6) == 0);
if (event == ESP_WIFI_SENSING_FSM_EVENT_ACTIVE) {
if (is_ap_channel) {
led_notify_ap_active();
}
ESP_LOGI(TAG, "[%s] ACTIVE peer=" MACSTR " data=%" PRIu32,
name, MAC2STR(peer_mac), data);
} else if (event == ESP_WIFI_SENSING_FSM_EVENT_INACTIVE) {
if (is_ap_channel) {
led_notify_ap_inactive();
}
ESP_LOGI(TAG, "[%s] INACTIVE peer=" MACSTR,
name, MAC2STR(peer_mac));
}
}
static void demo_init(void)
{
/*
* Follow the public component workflow:
* 1. Resolve the peers used by this demo.
* 2. Create one FSM handle.
* 3. Add one channel per peer.
* 4. Register ACTIVE / INACTIVE callbacks.
* 5. Start the FSM and optionally enable router-ping-assisted sampling.
*/
wifi_ap_record_t ap_info = {0};
ESP_ERROR_CHECK(esp_wifi_sta_get_ap_info(&ap_info));
memcpy(s_mac_ap, ap_info.bssid, 6);
ESP_LOGI(TAG, "peer mac (AP BSSID): " MACSTR, MAC2STR(ap_info.bssid));
ESP_LOGI(TAG, "peer mac 1 (CONFIG_CSI_SEND_MAC): " MACSTR, MAC2STR(CONFIG_CSI_SEND_MAC));
ESP_LOGI(TAG, "peer mac 2 (CONFIG_CSI_SEND_MAC_2): " MACSTR, MAC2STR(CONFIG_CSI_SEND_MAC_2));
esp_wifi_sensing_fsm_config_t cfg = DEFAULT_ESP_WIFI_SENSING_FSM_CONFIG();
cfg.max_channel_num = 3;
ESP_ERROR_CHECK(esp_wifi_sensing_fsm_create(&cfg, &s_hms));
/* The demo monitors the connected AP plus two fixed reference peers. */
ESP_ERROR_CHECK(esp_wifi_sensing_fsm_add_channel(s_hms, ap_info.bssid));
ESP_ERROR_CHECK(esp_wifi_sensing_fsm_add_channel(s_hms, CONFIG_CSI_SEND_MAC));
ESP_ERROR_CHECK(esp_wifi_sensing_fsm_add_channel(s_hms, CONFIG_CSI_SEND_MAC_2));
/* Register the same callback for both state transitions to keep logging symmetric. */
ESP_ERROR_CHECK(esp_wifi_sensing_fsm_register_event_cb(s_hms, ESP_WIFI_SENSING_FSM_EVENT_ACTIVE, on_motion_event, NULL));
ESP_ERROR_CHECK(esp_wifi_sensing_fsm_register_event_cb(s_hms, ESP_WIFI_SENSING_FSM_EVENT_INACTIVE, on_motion_event, NULL));
ESP_ERROR_CHECK(esp_wifi_sensing_fsm_control(s_hms, ESP_WIFI_SENSING_FSM_CTRL_START, NULL));
ESP_LOGI(TAG,
"default config: motion_detection_sensitivity=%.3f active_jitter_min=%.3f hold=%" PRIu32 "ms confirm=%d ping=%" PRIu32 "Hz",
cfg.default_channel_config.sensitivity,
cfg.default_channel_config.active_jitter_min,
cfg.default_channel_config.active_filter_ms,
CONFIG_ESP_WIFI_SENSING_CONFIRM_COUNT,
cfg.ping_frequency_hz);
esp_err_t ping_err = esp_wifi_sensing_fsm_ping_router_start(s_hms);
if (ping_err == ESP_OK) {
ESP_LOGI(TAG, "router ping started");
} else if (ping_err == ESP_ERR_INVALID_STATE) {
ESP_LOGW(TAG, "router ping start failed: Wi-Fi STA not connected or no gateway");
} else {
ESP_LOGE(TAG, "router ping start failed: %s", esp_err_to_name(ping_err));
}
}
void app_main(void)
{
/* Standard ESP-IDF bring-up followed by Wi-Fi connection and sensing start. */
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(led_init());
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL));
led_set_wifi_connected(false);
ESP_ERROR_CHECK(example_connect());
/* Disable Wi-Fi power save to keep CSI sampling cadence stable. */
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
espnow_rx_init();
demo_init();
#if CONFIG_ESP_WIFI_SENSING_WEB_SERIAL_ENABLE
/* Web UI also maps esp-radar train (TRAIN_START/STOP/REMOVE) and streams wander / train thresholds. */
/* Export the same three demo channels to the browser monitor UI. */
web_serial_monitor_peer_t serial_peers[] = {
{ .name = "AP" },
{ .name = "MAC_1" },
{ .name = "MAC_2" },
};
memcpy(serial_peers[0].mac, s_mac_ap, sizeof(s_mac_ap));
memcpy(serial_peers[1].mac, CONFIG_CSI_SEND_MAC, sizeof(serial_peers[1].mac));
memcpy(serial_peers[2].mac, CONFIG_CSI_SEND_MAC_2, sizeof(serial_peers[2].mac));
web_serial_monitor_config_t serial_cfg = {
.fsm = s_hms,
.peers = serial_peers,
.peer_num = sizeof(serial_peers) / sizeof(serial_peers[0]),
.stream_period_ms = CONFIG_ESP_WIFI_SENSING_WEB_SERIAL_STREAM_PERIOD_MS,
};
ESP_ERROR_CHECK(web_serial_monitor_init(&serial_cfg));
#endif
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

View file

@ -0,0 +1,7 @@
## IDF Component Manager Manifest File
dependencies:
idf: '>=5.4'
espressif/led_strip: "^3.0.0"
esp_wifi_sensing:
version: '>=0.1.1'

View file

@ -0,0 +1,285 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "led_control.h"
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"
#include "esp_check.h"
#include "esp_log.h"
#include "sdkconfig.h"
#if CONFIG_ESP_WIFI_SENSING_DEMO_LED_TYPE_WS2812
#include "led_strip.h"
#endif
static const char *TAG = "demo_led";
#define LED_UPDATE_PERIOD_MS 20
#define LED_BLINK_PERIOD_MS 500
#define LED_BLINK_DUTY_PERCENT 50
#define LED_FADE_OUT_TIME_MS 1000
typedef enum {
LED_MOTION_IDLE = 0,
LED_MOTION_ACTIVE,
LED_MOTION_FADING,
} led_motion_state_t;
typedef struct {
bool initialized;
bool wifi_connected;
led_motion_state_t motion_state;
TickType_t fade_start_tick;
TaskHandle_t task_handle;
#if CONFIG_ESP_WIFI_SENSING_DEMO_LED_TYPE_WS2812
led_strip_handle_t strip_handle;
uint8_t last_r;
uint8_t last_g;
uint8_t last_b;
#else
uint32_t last_duty;
#endif
} led_ctx_t;
static led_ctx_t s_led_ctx = {0};
static portMUX_TYPE s_led_lock = portMUX_INITIALIZER_UNLOCKED;
#if CONFIG_ESP_WIFI_SENSING_DEMO_LED_TYPE_GPIO
static const ledc_mode_t s_ledc_mode = LEDC_LOW_SPEED_MODE;
static const ledc_timer_t s_ledc_timer = LEDC_TIMER_0;
static const ledc_channel_t s_ledc_channel = LEDC_CHANNEL_0;
static const uint32_t s_ledc_max_duty = (1U << LEDC_TIMER_8_BIT) - 1U;
#endif
static void led_task(void *arg);
static void led_apply_state(TickType_t now);
static void led_trigger_update(void)
{
if (s_led_ctx.task_handle != NULL) {
xTaskNotifyGive(s_led_ctx.task_handle);
}
}
#if CONFIG_ESP_WIFI_SENSING_DEMO_LED_TYPE_GPIO
static void led_apply_gpio_brightness(float brightness)
{
if (brightness < 0.0f) {
brightness = 0.0f;
} else if (brightness > 1.0f) {
brightness = 1.0f;
}
uint32_t duty = (uint32_t)(brightness * s_ledc_max_duty + 0.5f);
if (duty == s_led_ctx.last_duty) {
return;
}
ESP_ERROR_CHECK(ledc_set_duty(s_ledc_mode, s_ledc_channel, duty));
ESP_ERROR_CHECK(ledc_update_duty(s_ledc_mode, s_ledc_channel));
s_led_ctx.last_duty = duty;
}
#else
static void led_apply_ws2812_color(uint8_t red, uint8_t green, uint8_t blue)
{
if (red == s_led_ctx.last_r && green == s_led_ctx.last_g && blue == s_led_ctx.last_b) {
return;
}
ESP_ERROR_CHECK(led_strip_set_pixel(s_led_ctx.strip_handle, 0, red, green, blue));
ESP_ERROR_CHECK(led_strip_refresh(s_led_ctx.strip_handle));
s_led_ctx.last_r = red;
s_led_ctx.last_g = green;
s_led_ctx.last_b = blue;
}
#endif
static void led_apply_state(TickType_t now)
{
bool wifi_connected;
led_motion_state_t motion_state;
TickType_t fade_start_tick;
portENTER_CRITICAL(&s_led_lock);
wifi_connected = s_led_ctx.wifi_connected;
motion_state = s_led_ctx.motion_state;
fade_start_tick = s_led_ctx.fade_start_tick;
portEXIT_CRITICAL(&s_led_lock);
if (!wifi_connected) {
/* Wi-Fi not ready yet: blink to signal the demo is still connecting. */
uint32_t phase_ms = (uint32_t)(now * portTICK_PERIOD_MS) % LED_BLINK_PERIOD_MS;
bool on_phase = phase_ms < (LED_BLINK_PERIOD_MS / 2);
#if CONFIG_ESP_WIFI_SENSING_DEMO_LED_TYPE_GPIO
led_apply_gpio_brightness(on_phase ? (LED_BLINK_DUTY_PERCENT / 100.0f) : 0.0f);
#else
uint8_t red = (uint8_t)((255U * LED_BLINK_DUTY_PERCENT) / 100U);
led_apply_ws2812_color(on_phase ? red : 0, 0, 0);
#endif
return;
}
if (motion_state == LED_MOTION_ACTIVE) {
/* Motion on the AP channel is mapped to a fully lit "active" state. */
#if CONFIG_ESP_WIFI_SENSING_DEMO_LED_TYPE_GPIO
led_apply_gpio_brightness(1.0f);
#else
led_apply_ws2812_color(0, 255, 0);
#endif
return;
}
if (motion_state == LED_MOTION_FADING) {
/* Fade out after motion stops so short inactive gaps remain readable. */
uint32_t elapsed_ms = (uint32_t)((now - fade_start_tick) * portTICK_PERIOD_MS);
if (elapsed_ms >= LED_FADE_OUT_TIME_MS) {
portENTER_CRITICAL(&s_led_lock);
if (s_led_ctx.motion_state == LED_MOTION_FADING) {
s_led_ctx.motion_state = LED_MOTION_IDLE;
}
portEXIT_CRITICAL(&s_led_lock);
#if CONFIG_ESP_WIFI_SENSING_DEMO_LED_TYPE_GPIO
led_apply_gpio_brightness(0.0f);
#else
led_apply_ws2812_color(0, 0, 0);
#endif
return;
}
float brightness = 1.0f - ((float)elapsed_ms / (float)LED_FADE_OUT_TIME_MS);
#if CONFIG_ESP_WIFI_SENSING_DEMO_LED_TYPE_GPIO
led_apply_gpio_brightness(brightness);
#else
uint8_t green = (uint8_t)(255.0f * brightness + 0.5f);
led_apply_ws2812_color(0, green, 0);
#endif
return;
}
#if CONFIG_ESP_WIFI_SENSING_DEMO_LED_TYPE_GPIO
led_apply_gpio_brightness(0.0f);
#else
led_apply_ws2812_color(0, 0, 0);
#endif
}
static void led_task(void *arg)
{
(void)arg;
while (true) {
led_apply_state(xTaskGetTickCount());
(void)ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(LED_UPDATE_PERIOD_MS));
}
}
esp_err_t led_init(void)
{
if (s_led_ctx.initialized) {
return ESP_OK;
}
#if CONFIG_ESP_WIFI_SENSING_DEMO_LED_TYPE_GPIO
ledc_timer_config_t timer_config = {
.speed_mode = s_ledc_mode,
.duty_resolution = LEDC_TIMER_8_BIT,
.timer_num = s_ledc_timer,
.freq_hz = 5000,
.clk_cfg = LEDC_AUTO_CLK,
};
ESP_RETURN_ON_ERROR(ledc_timer_config(&timer_config), TAG, "configure LEDC timer failed");
ledc_channel_config_t channel_config = {
.gpio_num = CONFIG_ESP_WIFI_SENSING_DEMO_LED_GPIO,
.speed_mode = s_ledc_mode,
.channel = s_ledc_channel,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = s_ledc_timer,
.duty = 0,
.hpoint = 0,
.flags.output_invert = 0,
};
ESP_RETURN_ON_ERROR(ledc_channel_config(&channel_config), TAG, "configure LEDC channel failed");
#else
led_strip_config_t strip_config = {
.strip_gpio_num = CONFIG_ESP_WIFI_SENSING_DEMO_LED_GPIO,
.max_leds = 1,
};
#if CONFIG_ESP_WIFI_SENSING_DEMO_WS2812_BACKEND_RMT
led_strip_rmt_config_t rmt_config = {
.resolution_hz = 10 * 1000 * 1000,
.flags.with_dma = false,
};
ESP_RETURN_ON_ERROR(led_strip_new_rmt_device(&strip_config, &rmt_config, &s_led_ctx.strip_handle),
TAG, "create RMT strip failed");
#else
led_strip_spi_config_t spi_config = {
.spi_bus = SPI2_HOST,
.flags.with_dma = true,
};
ESP_RETURN_ON_ERROR(led_strip_new_spi_device(&strip_config, &spi_config, &s_led_ctx.strip_handle),
TAG, "create SPI strip failed");
#endif
ESP_RETURN_ON_ERROR(led_strip_clear(s_led_ctx.strip_handle), TAG, "clear strip failed");
#endif
BaseType_t task_created = xTaskCreate(led_task, "demo_led", 3072, NULL, 4, &s_led_ctx.task_handle);
ESP_RETURN_ON_FALSE(task_created == pdPASS, ESP_ERR_NO_MEM, TAG, "create LED task failed");
s_led_ctx.initialized = true;
s_led_ctx.wifi_connected = false;
s_led_ctx.motion_state = LED_MOTION_IDLE;
ESP_LOGI(TAG, "status LED initialized on GPIO %d", CONFIG_ESP_WIFI_SENSING_DEMO_LED_GPIO);
led_trigger_update();
return ESP_OK;
}
void led_set_wifi_connected(bool connected)
{
if (!s_led_ctx.initialized) {
return;
}
portENTER_CRITICAL(&s_led_lock);
s_led_ctx.wifi_connected = connected;
if (!connected) {
s_led_ctx.motion_state = LED_MOTION_IDLE;
}
portEXIT_CRITICAL(&s_led_lock);
led_trigger_update();
}
void led_notify_ap_active(void)
{
if (!s_led_ctx.initialized) {
return;
}
portENTER_CRITICAL(&s_led_lock);
if (s_led_ctx.wifi_connected) {
s_led_ctx.motion_state = LED_MOTION_ACTIVE;
}
portEXIT_CRITICAL(&s_led_lock);
led_trigger_update();
}
void led_notify_ap_inactive(void)
{
if (!s_led_ctx.initialized) {
return;
}
portENTER_CRITICAL(&s_led_lock);
if (s_led_ctx.wifi_connected) {
s_led_ctx.motion_state = LED_MOTION_FADING;
s_led_ctx.fade_start_tick = xTaskGetTickCount();
}
portEXIT_CRITICAL(&s_led_lock);
led_trigger_update();
}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize the status LED backend used by the demo.
*
* The LED policy is intentionally simple for bring-up and public examples:
* blinking while Wi-Fi is disconnected, solid on motion, then fade out after
* the AP channel becomes inactive again.
*/
esp_err_t led_init(void);
/**
* @brief Update the Wi-Fi connectivity state shown on the LED.
*/
void led_set_wifi_connected(bool connected);
/**
* @brief Notify the LED state machine that AP-channel motion became active.
*/
void led_notify_ap_active(void);
/**
* @brief Notify the LED state machine that AP-channel motion became inactive.
*/
void led_notify_ap_inactive(void);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,729 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#if CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED
#include "driver/usb_serial_jtag.h"
#endif
#include "esp_check.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_timer.h"
#include "sdkconfig.h"
#include "web_serial_monitor.h"
#define WEB_SERIAL_PREFIX "HMS:"
#define WEB_SERIAL_COMMAND_PREFIX "HMSCMD "
#define WEB_SERIAL_TASK_STACK_SIZE 4096
#define WEB_SERIAL_TASK_PRIORITY 4
#define WEB_SERIAL_LINE_BUF_SIZE 512
#define WEB_SERIAL_TX_BUF_SIZE 1024
#define WEB_SERIAL_RX_POLL_BUF_SIZE 64
#define WEB_SERIAL_DEFAULT_PERIOD_MS 50
static const char *TAG = "web_serial_monitor";
/*
* This module exposes a tiny line-oriented JSON protocol over the serial
* console so the browser UI can observe and tune the sensing FSM without any
* additional transport layer.
*/
typedef struct {
bool initialized;
bool stream_enabled;
esp_wifi_sensing_fsm_handle_t fsm;
web_serial_monitor_peer_t peers[WEB_SERIAL_MONITOR_MAX_PEERS];
size_t peer_num;
uint32_t stream_period_ms;
TaskHandle_t task;
#if CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED
bool usb_driver_owned;
#endif
} web_serial_monitor_ctx_t;
static web_serial_monitor_ctx_t s_ctx = {0};
static void handle_command(char *line);
static esp_err_t configure_stdin_nonblocking(void)
{
int fd = fileno(stdin);
if (fd < 0) {
ESP_LOGE(TAG, "failed to get stdin fd");
return ESP_FAIL;
}
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) {
ESP_LOGE(TAG, "failed to get stdin flags: errno=%d", errno);
return ESP_FAIL;
}
if ((flags & O_NONBLOCK) != 0) {
return ESP_OK;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
ESP_LOGE(TAG, "failed to set stdin nonblocking: errno=%d", errno);
return ESP_FAIL;
}
return ESP_OK;
}
#if CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED
static esp_err_t configure_usb_serial_jtag_input(void)
{
if (usb_serial_jtag_is_driver_installed()) {
return ESP_OK;
}
usb_serial_jtag_driver_config_t usb_cfg = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT();
usb_cfg.rx_buffer_size = 512;
usb_cfg.tx_buffer_size = 256;
esp_err_t err = usb_serial_jtag_driver_install(&usb_cfg);
if (err == ESP_OK) {
s_ctx.usb_driver_owned = true;
}
return err;
}
#endif
static void process_rx_bytes(char *line_buf, size_t *line_len, const uint8_t *rx_buf, size_t read_len)
{
if (!line_buf || !line_len || !rx_buf || read_len == 0) {
return;
}
/* Commands are newline-delimited so both terminals and browsers can share the same protocol. */
for (size_t i = 0; i < read_len; i++) {
char ch = (char)rx_buf[i];
if (ch == '\r') {
continue;
}
if (ch == '\n') {
line_buf[*line_len] = '\0';
handle_command(line_buf);
*line_len = 0;
continue;
}
if (*line_len < (WEB_SERIAL_LINE_BUF_SIZE - 1)) {
line_buf[(*line_len)++] = ch;
} else {
*line_len = 0;
}
}
}
static const char *state_name(esp_wifi_sensing_fsm_process_state_t state)
{
switch (state) {
case ESP_WIFI_SENSING_FSM_PROCESS_IDLE:
return "IDLE";
case ESP_WIFI_SENSING_FSM_PROCESS_DEBOUNCE_ACTIVE:
return "DEBOUNCE_ACTIVE";
case ESP_WIFI_SENSING_FSM_PROCESS_ACTIVE:
return "ACTIVE";
case ESP_WIFI_SENSING_FSM_PROCESS_DEBOUNCE_INACTIVE:
return "DEBOUNCE_INACTIVE";
default:
return "UNKNOWN";
}
}
static const char *init_stage_name(esp_wifi_sensing_fsm_init_stage_t stage)
{
switch (stage) {
case ESP_WIFI_SENSING_FSM_INIT_STAGE_START:
return "START";
case ESP_WIFI_SENSING_FSM_INIT_STAGE_GOLD_FOUND:
return "GOLD_FOUND";
case ESP_WIFI_SENSING_FSM_INIT_STAGE_STABLE:
return "STABLE";
default:
return "UNKNOWN";
}
}
static const char *train_status_name(esp_wifi_sensing_fsm_train_status_t status)
{
switch (status) {
case ESP_WIFI_SENSING_FSM_TRAIN_STATUS_NONE:
return "NONE";
case ESP_WIFI_SENSING_FSM_TRAIN_STATUS_PROGRESS:
return "PROGRESS";
case ESP_WIFI_SENSING_FSM_TRAIN_STATUS_COMPLETE:
return "COMPLETE";
default:
return "UNKNOWN";
}
}
static const char *train_action_name(esp_wifi_sensing_fsm_train_action_t action)
{
switch (action) {
case ESP_WIFI_SENSING_FSM_TRAIN_ACTION_IDLE:
return "IDLE";
case ESP_WIFI_SENSING_FSM_TRAIN_ACTION_WAIT_BUFFER:
return "WAIT_BUFFER";
case ESP_WIFI_SENSING_FSM_TRAIN_ACTION_DISCARD_OUTLIER:
return "DISCARD_OUTLIER";
case ESP_WIFI_SENSING_FSM_TRAIN_ACTION_COLLECT_SAMPLE:
return "COLLECT_SAMPLE";
case ESP_WIFI_SENSING_FSM_TRAIN_ACTION_ACCUMULATE_BACKGROUND:
return "ACCUMULATE_BACKGROUND";
default:
return "UNKNOWN";
}
}
static const web_serial_monitor_peer_t *find_peer_by_name_or_mac(const char *token)
{
if (!token) {
return NULL;
}
for (size_t i = 0; i < s_ctx.peer_num; i++) {
const web_serial_monitor_peer_t *peer = &s_ctx.peers[i];
if (strcmp(token, peer->name) == 0) {
return peer;
}
char mac_str[18] = {0};
snprintf(mac_str, sizeof(mac_str), MACSTR, MAC2STR(peer->mac));
if (strcasecmp(token, mac_str) == 0) {
return peer;
}
}
return NULL;
}
static int write_line(const char *fmt, ...)
{
char line[WEB_SERIAL_TX_BUF_SIZE] = {0};
/* Prefix structured messages so ordinary ESP logs can coexist on the same port. */
int prefix_len = snprintf(line, sizeof(line), "%s", WEB_SERIAL_PREFIX);
if (prefix_len <= 0 || prefix_len >= (int)sizeof(line)) {
return 0;
}
va_list args;
va_start(args, fmt);
int body_len = vsnprintf(line + prefix_len, sizeof(line) - prefix_len, fmt, args);
va_end(args);
if (body_len < 0) {
return 0;
}
int total_len = prefix_len + body_len;
if (total_len >= (int)sizeof(line)) {
total_len = sizeof(line) - 1;
line[total_len - 1] = '\n';
line[total_len] = '\0';
}
int written = (int)fwrite(line, 1, total_len, stdout);
fflush(stdout);
return written;
}
static void send_error(const char *message)
{
if (!message) {
return;
}
(void)write_line("{\"type\":\"error\",\"message\":\"%s\"}\n", message);
}
static void send_ack(const char *cmd, bool ok, const char *detail)
{
(void)write_line("{\"type\":\"ack\",\"cmd\":\"%s\",\"ok\":%s,\"detail\":\"%s\"}\n",
cmd ? cmd : "",
ok ? "true" : "false",
detail ? detail : "");
}
static void send_runtime_config(void)
{
bool enabled = false;
if (esp_wifi_sensing_fsm_get_amplitude_log_enabled(s_ctx.fsm, &enabled) != ESP_OK) {
return;
}
(void)write_line("{\"type\":\"runtime\",\"amplitude_log_enabled\":%s}\n", enabled ? "true" : "false");
}
static void send_hello(void)
{
char peers_json[256] = {0};
size_t used = 0;
for (size_t i = 0; i < s_ctx.peer_num; i++) {
char item[48] = {0};
snprintf(item, sizeof(item), "%s\"%s\"", (i == 0) ? "" : ",", s_ctx.peers[i].name);
size_t item_len = strlen(item);
if ((used + item_len + 1) >= sizeof(peers_json)) {
break;
}
memcpy(peers_json + used, item, item_len);
used += item_len;
peers_json[used] = '\0';
}
bool enabled = false;
(void)esp_wifi_sensing_fsm_get_amplitude_log_enabled(s_ctx.fsm, &enabled);
/* Send enough metadata for the web page to populate its initial UI state. */
(void)write_line(
"{\"type\":\"hello\",\"transport\":\"stdio\",\"target\":\"%s\",\"stream_period_ms\":%" PRIu32 ",\"amplitude_log_enabled\":%s,\"peers\":[%s]}\n",
CONFIG_IDF_TARGET,
s_ctx.stream_period_ms,
enabled ? "true" : "false",
peers_json);
}
static void send_channel_config(const web_serial_monitor_peer_t *peer)
{
if (!peer) {
return;
}
esp_wifi_sensing_fsm_channel_config_t cfg = {0};
if (esp_wifi_sensing_fsm_get_channel_config(s_ctx.fsm, peer->mac, &cfg) != ESP_OK) {
return;
}
(void)write_line(
"{\"type\":\"config\",\"peer\":\"%s\",\"mac\":\"" MACSTR "\",\"sensitivity\":%.6f,"
"\"presence_sensitivity\":%.6f,"
"\"active_jitter_min\":%.6f,"
"\"active_filter_ms\":%" PRIu32 "}\n",
peer->name,
MAC2STR(peer->mac),
cfg.sensitivity,
cfg.presence_sensitivity,
cfg.active_jitter_min,
cfg.active_filter_ms);
}
static void send_all_configs(void)
{
for (size_t i = 0; i < s_ctx.peer_num; i++) {
send_channel_config(&s_ctx.peers[i]);
}
}
static void send_channel_sample(const web_serial_monitor_peer_t *peer)
{
if (!peer) {
return;
}
esp_wifi_sensing_fsm_channel_diag_t diag = {0};
if (esp_wifi_sensing_fsm_get_channel_diag(s_ctx.fsm, peer->mac, &diag) != ESP_OK) {
return;
}
esp_wifi_sensing_fsm_state_t motion_state = ESP_WIFI_SENSING_FSM_STATE_INACTIVE;
if (esp_wifi_sensing_fsm_get_state(s_ctx.fsm, peer->mac, &motion_state) != ESP_OK) {
return;
}
/* Ship only the minimum public diagnostics required by the browser UI. */
(void)write_line(
"{\"type\":\"sample\",\"ts_ms\":%" PRIu64 ",\"peer\":\"%s\",\"mac\":\"" MACSTR "\","
"\"motion_status\":%s,\"motion_state\":%d,"
"\"jitter_value\":%.6f,"
"\"wander_value\":%.6f,"
"\"presence_ready\":%s,\"presence_wander_average\":%.6f,\"presence_someone_threshold\":%.6f,\"presence_someone_status\":%s,"
"\"train_wander_threshold\":%.6f,\"train_jitter_threshold\":%.6f,\"train_thresholds_valid\":%s,"
"\"train_status\":%d,\"train_status_name\":\"%s\",\"train_last_action\":%d,\"train_last_action_name\":\"%s\","
"\"train_sample_count\":%" PRIu32 ",\"train_background_count\":%" PRIu32 ","
"\"train_background_avg\":%.6f,\"train_last_basis_wander\":%.6f,"
"\"smooth_scaled\":%" PRIu32 ",\"enter_level_scaled\":%" PRIu32 ",\"exit_level_scaled\":%" PRIu32 ","
"\"state\":%d,\"state_name\":\"%s\",\"init_stage\":%d,\"init_stage_name\":\"%s\"}\n",
(uint64_t)(esp_timer_get_time() / 1000ULL),
peer->name,
MAC2STR(peer->mac),
(motion_state == ESP_WIFI_SENSING_FSM_STATE_ACTIVE) ? "true" : "false",
(int)motion_state,
diag.jitter_value,
diag.wander_value,
diag.presence_ready ? "true" : "false",
diag.presence_wander_average,
diag.presence_someone_threshold,
diag.presence_someone_status ? "true" : "false",
diag.train_wander_threshold,
diag.train_jitter_threshold,
diag.train_thresholds_valid ? "true" : "false",
(int)diag.train_status,
train_status_name(diag.train_status),
(int)diag.train_last_action,
train_action_name(diag.train_last_action),
diag.train_sample_count,
diag.train_background_count,
diag.train_background_avg,
diag.train_last_basis_wander,
diag.smooth_scaled,
diag.enter_level_scaled,
diag.exit_level_scaled,
(int)diag.state,
state_name(diag.state),
(int)diag.init_stage,
init_stage_name(diag.init_stage));
}
static void apply_cfg_value(esp_wifi_sensing_fsm_channel_config_t *cfg, const char *key, const char *value)
{
if (!cfg || !key || !value) {
return;
}
if (strcmp(key, "motion_sensitivity") == 0) {
cfg->sensitivity = strtof(value, NULL);
} else if (strcmp(key, "presence_sensitivity") == 0) {
cfg->presence_sensitivity = strtof(value, NULL);
} else if (strcmp(key, "active_jitter_min") == 0) {
cfg->active_jitter_min = strtof(value, NULL);
} else if (strcmp(key, "active_filter_ms") == 0) {
cfg->active_filter_ms = (uint32_t)strtoul(value, NULL, 10);
}
}
static void handle_set_cfg(char *args)
{
char *saveptr = NULL;
char *peer_token = strtok_r(args, " ", &saveptr);
if (!peer_token) {
send_ack("SET_CFG", false, "missing peer");
return;
}
const web_serial_monitor_peer_t *peer = find_peer_by_name_or_mac(peer_token);
if (!peer) {
send_ack("SET_CFG", false, "peer not found");
return;
}
esp_wifi_sensing_fsm_channel_config_t cfg = {0};
if (esp_wifi_sensing_fsm_get_channel_config(s_ctx.fsm, peer->mac, &cfg) != ESP_OK) {
send_ack("SET_CFG", false, "get cfg failed");
return;
}
char *token = NULL;
while ((token = strtok_r(NULL, " ", &saveptr)) != NULL) {
char *sep = strchr(token, '=');
if (!sep) {
continue;
}
*sep = '\0';
apply_cfg_value(&cfg, token, sep + 1);
}
if (esp_wifi_sensing_fsm_set_channel_config(s_ctx.fsm, peer->mac, &cfg) != ESP_OK) {
send_ack("SET_CFG", false, "set cfg failed");
return;
}
send_ack("SET_CFG", true, peer->name);
send_channel_config(peer);
}
static void handle_command(char *line)
{
if (!line || strncmp(line, WEB_SERIAL_COMMAND_PREFIX, strlen(WEB_SERIAL_COMMAND_PREFIX)) != 0) {
return;
}
/* The command grammar stays intentionally small to keep the demo debuggable from a plain terminal. */
char *cmd = line + strlen(WEB_SERIAL_COMMAND_PREFIX);
if (strcmp(cmd, "HELLO") == 0) {
send_hello();
send_runtime_config();
send_all_configs();
return;
}
if (strcmp(cmd, "START_STREAM") == 0) {
s_ctx.stream_enabled = true;
send_ack("START_STREAM", true, "stream enabled");
return;
}
if (strcmp(cmd, "STOP_STREAM") == 0) {
s_ctx.stream_enabled = false;
send_ack("STOP_STREAM", true, "stream disabled");
return;
}
if (strcmp(cmd, "FSM_START") == 0) {
esp_err_t err = esp_wifi_sensing_fsm_control(s_ctx.fsm, ESP_WIFI_SENSING_FSM_CTRL_START, NULL);
send_ack("FSM_START", err == ESP_OK, esp_err_to_name(err));
return;
}
if (strcmp(cmd, "FSM_STOP") == 0) {
esp_err_t err = esp_wifi_sensing_fsm_control(s_ctx.fsm, ESP_WIFI_SENSING_FSM_CTRL_STOP, NULL);
send_ack("FSM_STOP", err == ESP_OK, esp_err_to_name(err));
return;
}
if (strcmp(cmd, "RESET_BASELINE") == 0) {
esp_err_t err = esp_wifi_sensing_fsm_control(s_ctx.fsm, ESP_WIFI_SENSING_FSM_CTRL_RESET_BASELINE, NULL);
send_ack("RESET_BASELINE", err == ESP_OK, esp_err_to_name(err));
return;
}
if (strncmp(cmd, "GET_CFG", 7) == 0) {
char *arg = cmd + 7;
while (*arg == ' ') {
arg++;
}
if (*arg == '\0' || strcmp(arg, "ALL") == 0) {
send_all_configs();
send_ack("GET_CFG", true, "all");
return;
}
const web_serial_monitor_peer_t *peer = find_peer_by_name_or_mac(arg);
if (!peer) {
send_ack("GET_CFG", false, "peer not found");
return;
}
send_channel_config(peer);
send_ack("GET_CFG", true, peer->name);
return;
}
if (strcmp(cmd, "GET_RUNTIME") == 0) {
send_runtime_config();
send_ack("GET_RUNTIME", true, "runtime");
return;
}
if (strncmp(cmd, "SET_AMPLITUDE_LOG ", 18) == 0) {
const char *value = cmd + 18;
bool enabled = false;
if ((strcmp(value, "1") == 0) || (strcasecmp(value, "true") == 0) || (strcasecmp(value, "on") == 0)) {
enabled = true;
} else if ((strcmp(value, "0") == 0) || (strcasecmp(value, "false") == 0) || (strcasecmp(value, "off") == 0)) {
enabled = false;
} else {
send_ack("SET_AMPLITUDE_LOG", false, "invalid value");
return;
}
esp_err_t err = esp_wifi_sensing_fsm_set_amplitude_log_enabled(s_ctx.fsm, enabled);
send_ack("SET_AMPLITUDE_LOG", err == ESP_OK, esp_err_to_name(err));
if (err == ESP_OK) {
send_runtime_config();
send_hello();
}
return;
}
if (strncmp(cmd, "SET_CFG ", 8) == 0) {
handle_set_cfg(cmd + 8);
return;
}
if (strncmp(cmd, "TRAIN_START ", 12) == 0) {
const char *arg = cmd + 12;
while (*arg == ' ') {
arg++;
}
const web_serial_monitor_peer_t *peer = find_peer_by_name_or_mac(arg);
if (!peer) {
send_ack("TRAIN_START", false, "peer not found");
return;
}
esp_err_t err = esp_wifi_sensing_fsm_train_start(s_ctx.fsm, peer->mac);
send_ack("TRAIN_START", err == ESP_OK, esp_err_to_name(err));
return;
}
if (strncmp(cmd, "TRAIN_STOP ", 11) == 0) {
const char *arg = cmd + 11;
while (*arg == ' ') {
arg++;
}
const web_serial_monitor_peer_t *peer = find_peer_by_name_or_mac(arg);
if (!peer) {
send_ack("TRAIN_STOP", false, "peer not found");
return;
}
float wander_th = 0.0f;
float jitter_th = 0.0f;
esp_err_t err = esp_wifi_sensing_fsm_train_stop(s_ctx.fsm, peer->mac, &wander_th, &jitter_th);
if (err == ESP_OK) {
char detail[96] = {0};
snprintf(detail, sizeof(detail), "%s wander=%.4f jitter=%.4f", peer->name, wander_th, jitter_th);
send_ack("TRAIN_STOP", true, detail);
} else {
send_ack("TRAIN_STOP", false, esp_err_to_name(err));
}
return;
}
if (strncmp(cmd, "TRAIN_REMOVE ", 13) == 0) {
const char *arg = cmd + 13;
while (*arg == ' ') {
arg++;
}
const web_serial_monitor_peer_t *peer = find_peer_by_name_or_mac(arg);
if (!peer) {
send_ack("TRAIN_REMOVE", false, "peer not found");
return;
}
esp_err_t err = esp_wifi_sensing_fsm_train_remove(s_ctx.fsm, peer->mac);
send_ack("TRAIN_REMOVE", err == ESP_OK, esp_err_to_name(err));
return;
}
send_error("unknown command");
}
static void poll_rx_and_handle_commands(void)
{
static char stdin_line_buf[WEB_SERIAL_LINE_BUF_SIZE] = {0};
static size_t stdin_line_len = 0;
#if CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED
static char usb_line_buf[WEB_SERIAL_LINE_BUF_SIZE] = {0};
static size_t usb_line_len = 0;
#endif
uint8_t rx_buf[WEB_SERIAL_RX_POLL_BUF_SIZE] = {0};
/* Accept commands from both the standard console and USB Serial/JTAG when available. */
ssize_t read_len = read(fileno(stdin), rx_buf, sizeof(rx_buf));
if (read_len < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
ESP_LOGD(TAG, "stdin read failed: errno=%d", errno);
}
} else if (read_len > 0) {
process_rx_bytes(stdin_line_buf, &stdin_line_len, rx_buf, (size_t)read_len);
}
#if CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED
int usb_read_len = usb_serial_jtag_read_bytes(rx_buf, sizeof(rx_buf), 0);
if (usb_read_len > 0) {
process_rx_bytes(usb_line_buf, &usb_line_len, rx_buf, (size_t)usb_read_len);
}
#endif
}
static esp_err_t configure_command_inputs(void)
{
esp_err_t err = configure_stdin_nonblocking();
if (err != ESP_OK) {
return err;
}
#if CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED
err = configure_usb_serial_jtag_input();
if (err != ESP_OK) {
return err;
}
#endif
return ESP_OK;
}
static void cleanup_command_inputs(void)
{
#if CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED
if (s_ctx.usb_driver_owned) {
(void)usb_serial_jtag_driver_uninstall();
s_ctx.usb_driver_owned = false;
}
#endif
}
static const char *input_mode_name(void)
{
#if CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED
return "stdio + usb_serial_jtag";
#else
return "stdio";
#endif
}
static void web_serial_monitor_task(void *arg)
{
(void)arg;
int64_t last_stream_us = 0;
/* Emit an initial snapshot so the browser can render immediately after connecting. */
send_hello();
send_runtime_config();
send_all_configs();
while (true) {
poll_rx_and_handle_commands();
int64_t now_us = esp_timer_get_time();
if (s_ctx.stream_enabled &&
(last_stream_us == 0 || (now_us - last_stream_us) >= ((int64_t)s_ctx.stream_period_ms * 1000LL))) {
last_stream_us = now_us;
for (size_t i = 0; i < s_ctx.peer_num; i++) {
send_channel_sample(&s_ctx.peers[i]);
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
esp_err_t web_serial_monitor_init(const web_serial_monitor_config_t *config)
{
if (!config || !config->fsm || !config->peers || config->peer_num == 0 ||
config->peer_num > WEB_SERIAL_MONITOR_MAX_PEERS) {
return ESP_ERR_INVALID_ARG;
}
if (s_ctx.initialized) {
return ESP_ERR_INVALID_STATE;
}
memset(&s_ctx, 0, sizeof(s_ctx));
s_ctx.initialized = true;
s_ctx.stream_enabled = true;
s_ctx.fsm = config->fsm;
s_ctx.peer_num = config->peer_num;
s_ctx.stream_period_ms = (config->stream_period_ms > 0) ? config->stream_period_ms : WEB_SERIAL_DEFAULT_PERIOD_MS;
memcpy(s_ctx.peers, config->peers, config->peer_num * sizeof(web_serial_monitor_peer_t));
esp_err_t err = configure_command_inputs();
if (err != ESP_OK) {
memset(&s_ctx, 0, sizeof(s_ctx));
return err;
}
BaseType_t ok = xTaskCreate(web_serial_monitor_task,
"web_serial_monitor",
WEB_SERIAL_TASK_STACK_SIZE,
NULL,
WEB_SERIAL_TASK_PRIORITY,
&s_ctx.task);
if (ok != pdPASS) {
cleanup_command_inputs();
memset(&s_ctx, 0, sizeof(s_ctx));
return ESP_FAIL;
}
ESP_LOGI(TAG, "web serial monitor started, input=%s, output=stdio, period=%" PRIu32 "ms",
input_mode_name(), s_ctx.stream_period_ms);
return ESP_OK;
}

View file

@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#include "esp_wifi_sensing.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Maximum number of peers supported by the demo's browser monitor.
*/
#define WEB_SERIAL_MONITOR_MAX_PEERS 8
/**
* @brief Peer entry exported to the browser monitor.
*/
typedef struct {
const char *name; /**< Human-readable peer name shown by the UI. */
uint8_t mac[6]; /**< Peer MAC address bound to the sensing channel. */
} web_serial_monitor_peer_t;
/**
* @brief Configuration passed to `web_serial_monitor_init()`.
*/
typedef struct {
esp_wifi_sensing_fsm_handle_t fsm; /**< Sensing FSM instance to inspect and control. */
const web_serial_monitor_peer_t *peers; /**< Peer table visible to the UI. */
size_t peer_num; /**< Number of valid entries in `peers`. */
uint32_t stream_period_ms; /**< Streaming period in milliseconds. Set `0` to use the default. */
} web_serial_monitor_config_t;
/**
* @brief Start the lightweight serial protocol used by the browser monitor.
*
* The module emits line-delimited JSON messages with the `HMS:` prefix and
* accepts line-delimited commands prefixed with `HMSCMD `. This keeps the demo
* easy to inspect from a terminal while still being simple for a Web Serial UI
* to parse.
*
* @param config Browser-monitor configuration.
* @return
* - ESP_OK: Success
* - ESP_ERR_INVALID_ARG: `config` is invalid
* - ESP_ERR_INVALID_STATE: The monitor is already initialized
* - ESP_FAIL: Failed to create the background task or configure inputs
*/
esp_err_t web_serial_monitor_init(const web_serial_monitor_config_t *config);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,12 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 5.5.0 Project Minimal Configuration
#
CONFIG_PARTITION_TABLE_OFFSET=0xa000
CONFIG_ESP_CONSOLE_UART_CUSTOM=y
CONFIG_ESP_CONSOLE_UART_BAUDRATE=921600
CONFIG_ESP_TASK_WDT_TIMEOUT_S=30
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=128
CONFIG_ESP_WIFI_CSI_ENABLED=y
CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n
CONFIG_ESP32_WIFI_CSI_ENABLED=y
CONFIG_ESP_WIFI_SENSING_DEFAULT_PRESENCE_SENSITIVITY=250

File diff suppressed because it is too large Load diff

View file

@ -171,17 +171,17 @@ static void wifi_csi_rx_cb(void *ctx, wifi_csi_info_t *info)
#if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32C61
if (!s_count) {
ESP_LOGI(TAG, "================ CSI RECV ================");
ets_printf("type,seq,mac,rssi,rate,noise_floor,fft_gain,agc_gain,channel,local_timestamp,sig_len,rx_state,len,first_word,data\n");
ets_printf("type,seq,mac,rssi,rate,noise_floor,fft_gain,agc_gain,channel,local_timestamp,sig_len,rx_format,len,first_word,data\n");
}
ets_printf("CSI_DATA,%d," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d",
rx_id, MAC2STR(info->mac), rx_ctrl->rssi, rx_ctrl->rate,
rx_ctrl->noise_floor, fft_gain, agc_gain, rx_ctrl->channel,
rx_ctrl->timestamp, rx_ctrl->sig_len, rx_ctrl->rx_state);
rx_ctrl->timestamp, rx_ctrl->sig_len, rx_ctrl->cur_bb_format);
#else
if (!s_count) {
ESP_LOGI(TAG, "================ CSI RECV ================");
ets_printf("type,id,mac,rssi,rate,sig_mode,mcs,bandwidth,smoothing,not_sounding,aggregation,stbc,fec_coding,sgi,noise_floor,ampdu_cnt,channel,secondary_channel,local_timestamp,ant,sig_len,rx_state,len,first_word,data\n");
ets_printf("type,id,mac,rssi,rate,sig_mode,mcs,bandwidth,smoothing,not_sounding,aggregation,stbc,fec_coding,sgi,noise_floor,ampdu_cnt,channel,secondary_channel,local_timestamp,ant,sig_len,rx_format,len,first_word,data\n");
}
ets_printf("CSI_DATA,%d," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
@ -189,7 +189,7 @@ static void wifi_csi_rx_cb(void *ctx, wifi_csi_info_t *info)
rx_ctrl->mcs, rx_ctrl->cwb, rx_ctrl->smoothing, rx_ctrl->not_sounding,
rx_ctrl->aggregation, rx_ctrl->stbc, rx_ctrl->fec_coding, rx_ctrl->sgi,
rx_ctrl->noise_floor, rx_ctrl->ampdu_cnt, rx_ctrl->channel, rx_ctrl->secondary_channel,
rx_ctrl->timestamp, rx_ctrl->ant, rx_ctrl->sig_len, rx_ctrl->rx_state);
rx_ctrl->timestamp, rx_ctrl->ant, rx_ctrl->sig_len, rx_ctrl->sig_mode);
#endif
#if (CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61) && CSI_FORCE_LLTF

View file

@ -87,23 +87,23 @@ static void wifi_csi_rx_cb(void *ctx, wifi_csi_info_t *info)
#if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32C61
if (!s_count) {
ESP_LOGI(TAG, "================ CSI RECV ================");
ets_printf("type,seq,mac,rssi,rate,noise_floor,fft_gain,agc_gain,channel,local_timestamp,sig_len,rx_state,len,first_word,data\n");
ets_printf("type,seq,mac,rssi,rate,noise_floor,fft_gain,agc_gain,channel,local_timestamp,sig_len,rx_format,len,first_word,data\n");
}
ets_printf("CSI_DATA,%d," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d",
s_count, MAC2STR(info->mac), rx_ctrl->rssi, rx_ctrl->rate,
rx_ctrl->noise_floor, fft_gain, agc_gain, rx_ctrl->channel,
rx_ctrl->timestamp, rx_ctrl->sig_len, rx_ctrl->rx_state);
rx_ctrl->timestamp, rx_ctrl->sig_len, rx_ctrl->cur_bb_format);
#else
if (!s_count) {
ESP_LOGI(TAG, "================ CSI RECV ================");
ets_printf("type,id,mac,rssi,rate,sig_mode,mcs,bandwidth,smoothing,not_sounding,aggregation,stbc,fec_coding,sgi,noise_floor,ampdu_cnt,channel,secondary_channel,local_timestamp,ant,sig_len,rx_state,len,first_word,data\n");
ets_printf("type,id,mac,rssi,rate,sig_mode,mcs,bandwidth,smoothing,not_sounding,aggregation,stbc,fec_coding,sgi,noise_floor,ampdu_cnt,channel,secondary_channel,local_timestamp,ant,sig_len,rx_format,len,first_word,data\n");
}
ets_printf("CSI_DATA,%d," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
s_count, MAC2STR(info->mac), rx_ctrl->rssi, rx_ctrl->rate, rx_ctrl->sig_mode,
rx_ctrl->mcs, rx_ctrl->cwb, rx_ctrl->smoothing, rx_ctrl->not_sounding,
rx_ctrl->aggregation, rx_ctrl->stbc, rx_ctrl->fec_coding, rx_ctrl->sgi,
rx_ctrl->noise_floor, rx_ctrl->ampdu_cnt, rx_ctrl->channel, rx_ctrl->secondary_channel,
rx_ctrl->timestamp, rx_ctrl->ant, rx_ctrl->sig_len, rx_ctrl->rx_state);
rx_ctrl->timestamp, rx_ctrl->ant, rx_ctrl->sig_len, rx_ctrl->sig_mode);
#endif
#if (CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61) && CSI_FORCE_LLTF

View file

@ -1,399 +0,0 @@
#!/usr/bin/env python3
# -*-coding:utf-8-*-
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
# WARNING: we don't check for Python build-time dependencies until
# check_environment() function below. If possible, avoid importing
# any external libraries here - put in external script, or import in
# their specific function instead.
import sys
import csv
import json
import argparse
import pandas as pd
import numpy as np
import serial
from os import path
from io import StringIO
from PyQt5.Qt import *
from pyqtgraph import PlotWidget
from PyQt5 import QtCore
import pyqtgraph as pg
from pyqtgraph import ScatterPlotItem
from PyQt5.QtCore import pyqtSignal, QThread
import threading
import time
from scipy.optimize import minimize
import matplotlib.pyplot as plt
from scipy.stats import linregress
import statsmodels.api as sm
# Reduce displayed waveforms to avoid display freezes
CSI_VAID_SUBCARRIER_INTERVAL = 1
csi_vaid_subcarrier_len =0
CSI_DATA_INDEX = 200 # buffer size
CSI_DATA_COLUMNS = 490
DATA_COLUMNS_NAMES_C5C6 = ['type', 'id', 'mac', 'rssi', 'rate','noise_floor','fft_gain','agc_gain', 'channel', 'local_timestamp', 'sig_len', 'rx_state', 'len', 'first_word', 'data']
DATA_COLUMNS_NAMES_NEW = ['type', 'mac','len', 'first_word', 'data']
DATA_COLUMNS_NAMES = ['type', 'id', 'mac', 'rssi', 'rate', 'sig_mode', 'mcs', 'bandwidth', 'smoothing', 'not_sounding', 'aggregation', 'stbc', 'fec_coding',
'sgi', 'noise_floor', 'ampdu_cnt', 'channel', 'secondary_channel', 'local_timestamp', 'ant', 'sig_len', 'rx_state', 'len', 'first_word', 'data']
csi_data_array = np.zeros(
[CSI_DATA_INDEX, CSI_DATA_COLUMNS], dtype=np.float64)
csi_data_phase = np.zeros([CSI_DATA_INDEX, CSI_DATA_COLUMNS], dtype=np.float64)
csi_data_complex = np.zeros([CSI_DATA_INDEX, CSI_DATA_COLUMNS], dtype=np.complex64)
agc_gain_data = np.zeros([CSI_DATA_INDEX], dtype=np.float64)
fft_gain_data = np.zeros([CSI_DATA_INDEX], dtype=np.float64)
fft_gains = []
agc_gains = []
# RAW_DATA 独立数据数组
RAW_DATA_COLUMNS = 612 # RAW_DATA 最大长度
raw_data_complex = np.zeros([CSI_DATA_INDEX, RAW_DATA_COLUMNS], dtype=np.complex64)
class csi_data_graphical_window(QWidget):
def __init__(self):
super().__init__()
self.resize(1280, 900)
self.plotWidget_ted = PlotWidget(self)
self.plotWidget_ted.setGeometry(QtCore.QRect(0, 0, 640, 300))
self.plotWidget_ted.setYRange(-2*np.pi, 2*np.pi)
self.plotWidget_ted.addLegend()
self.plotWidget_ted.setTitle('Phase Data - Last Frame') # 添加标题
self.plotWidget_ted.setLabel('left', 'Phase (rad)') # Y轴标签
self.plotWidget_ted.setLabel('bottom', 'Subcarrier Index') # X轴标签
self.csi_amplitude_array = np.abs(csi_data_complex)
self.csi_phase_array = np.angle(csi_data_complex)
self.curve = self.plotWidget_ted.plot([], name='CSI Phase', pen='r')
# RAW_DATA 相位曲线(蓝色)
self.raw_amplitude_array = np.abs(raw_data_complex)
self.raw_phase_array = np.angle(raw_data_complex)
self.curve_raw = self.plotWidget_ted.plot([], name='RAW Phase', pen='b')
self.plotWidget_multi_data = PlotWidget(self)
self.plotWidget_multi_data.setGeometry(QtCore.QRect(0, 300, 1280, 300))
self.plotWidget_multi_data.getViewBox().enableAutoRange(axis=pg.ViewBox.YAxis)
self.plotWidget_multi_data.addLegend()
self.plotWidget_multi_data.setTitle('Subcarrier Amplitude Data') # 添加标题
self.plotWidget_multi_data.setLabel('left', 'Amplitude') # Y轴标签
self.plotWidget_multi_data.setLabel('bottom', 'Time (Cumulative Packet Count)') # X轴标签
self.curve_list = []
agc_curve = self.plotWidget_multi_data.plot(
agc_gain_data, name='AGC Gain', pen=[255,255,0])
fft_curve = self.plotWidget_multi_data.plot(
fft_gain_data, name='FFT Gain', pen=[255,255,0])
self.curve_list.append(agc_curve)
self.curve_list.append(fft_curve)
for i in range(CSI_DATA_COLUMNS):
curve = self.plotWidget_multi_data.plot(
self.csi_amplitude_array[:, i], name=str(i), pen=(255, 255, 255))
self.curve_list.append(curve)
self.plotWidget_phase_data = PlotWidget(self)
self.plotWidget_phase_data.setGeometry(QtCore.QRect(0, 600, 1280, 300))
self.plotWidget_phase_data.getViewBox().enableAutoRange(axis=pg.ViewBox.YAxis)
self.plotWidget_phase_data.addLegend()
self.plotWidget_multi_data.setTitle('Subcarrier Phase Data') # 添加标题
self.plotWidget_multi_data.setLabel('left', 'Phase (rad)') # Y轴标签
self.plotWidget_multi_data.setLabel('bottom', 'Time (Cumulative Packet Count)') # X轴标签
self.curve_phase_list = []
for i in range(CSI_DATA_COLUMNS):
phase_curve = self.plotWidget_phase_data.plot(
np.angle(self.csi_amplitude_array[:, i]), name=str(i), pen=(255, 255, 255))
self.curve_phase_list.append(phase_curve)
# IQ 图窗口
self.plotWidget_iq = PlotWidget(self)
self.plotWidget_iq.setGeometry(QtCore.QRect(640, 0, 640, 300))
self.plotWidget_iq.setLabel('left', 'Q (Imag)')
self.plotWidget_iq.setLabel('bottom', 'I (Real)')
self.plotWidget_iq.setTitle('IQ Plot - Last Frame')
view_box = self.plotWidget_iq.getViewBox()
view_box.setRange(QtCore.QRectF(-30, -30, 60, 60)) # 可以调整范围的大小,保证原点在中间
self.plotWidget_iq.getViewBox().setAspectLocked(True)
self.iq_scatter = ScatterPlotItem(size=6)
self.plotWidget_iq.addItem(self.iq_scatter)
self.iq_colors = []
self.timer = pg.QtCore.QTimer()
self.timer.timeout.connect(self.update_data)
self.timer.start(100)
self.deta_len = 0
def update_curve_colors(self, color_list):
self.deta_len = len(color_list)
self.iq_colors = color_list
self.plotWidget_ted.setXRange(0, self.deta_len//2)
for i in range(self.deta_len):
self.curve_list[i].setPen(color_list[i])
self.curve_phase_list[i].setPen(color_list[i])
def update_data(self):
i = np.real(csi_data_complex[-1, :])
q = np.imag(csi_data_complex[-1, :])
# points = []
# for idx in range(self.deta_len):
# if idx < len(self.iq_colors):
# color = self.iq_colors[idx]
# else:
# color = (200, 200, 200)
# points.append({'pos': (i[idx], q[idx]), 'brush': pg.mkBrush(color)})
# self.iq_scatter.setData(points)
# CSI_DATA 相位更新
self.csi_amplitude_array = np.abs(csi_data_complex)
self.csi_phase_array = np.angle(csi_data_complex)
self.csi_row_data = self.csi_phase_array[-1, :]
self.csi_row_data = np.unwrap(self.csi_row_data)
self.curve.setData(self.csi_row_data)
# RAW_DATA 相位更新
self.raw_amplitude_array = np.abs(raw_data_complex)
self.raw_phase_array = np.angle(raw_data_complex)
self.raw_row_data = self.raw_phase_array[-1, :]
self.raw_row_data = np.unwrap(self.raw_row_data)
self.curve_raw.setData(self.raw_row_data)
# self.curve_list[CSI_DATA_COLUMNS].setData(agc_gain_data)
# self.curve_list[CSI_DATA_COLUMNS+1].setData(fft_gain_data)
for i in range(CSI_DATA_COLUMNS):
self.curve_list[i].setData(self.csi_amplitude_array[:, i])
# self.curve_phase_list[i].setData(self.csi_phase_array[:, i])
def generate_subcarrier_colors(red_range, green_range, yellow_range, total_num,interval=1):
colors = []
for i in range(total_num):
if red_range and red_range[0] <= i <= red_range[1]:
intensity = int(255 * (i - red_range[0]) / (red_range[1] - red_range[0]))
colors.append((intensity, 0, 0))
elif green_range and green_range[0] <= i <= green_range[1]:
intensity = int(255 * (i - green_range[0]) / (green_range[1] - green_range[0]))
colors.append((0, intensity, 0))
elif yellow_range and yellow_range[0] <= i <= yellow_range[1]:
intensity = int(255 * (i - yellow_range[0]) / (yellow_range[1] - yellow_range[0]))
colors.append((0, intensity, intensity))
else:
colors.append((200, 200, 200))
return colors
def csi_data_read_parse(port: str, csv_writer, log_file_fd,callback=None):
global fft_gains, agc_gains
set = serial.Serial(port=port, baudrate=921600,bytesize=8, parity='N', stopbits=1)
csi_count = 0 # CSI_DATA 计数器
raw_count = 0 # RAW_DATA 计数器
if set.isOpen():
print('open success')
else:
print('open failed')
return
while True:
strings = str(set.readline())
if not strings:
break
strings = strings.lstrip('b\'').rstrip('\\r\\n\'')
index = strings.find('CSI_DATA')
raw_index = strings.find('RAW_DATA')
if index == -1 and raw_index == -1:
log_file_fd.write(strings + '\n')
log_file_fd.flush()
continue
# RAW_DATA 单独处理
if raw_index != -1:
csv_reader = csv.reader(StringIO(strings))
csi_data = next(csv_reader)
csi_data_len = int(csi_data[-3])
try:
csi_raw_data = json.loads(csi_data[-1])
except json.JSONDecodeError:
print('RAW_DATA is incomplete')
log_file_fd.write('RAW_DATA is incomplete\n')
log_file_fd.write(strings + '\n')
log_file_fd.flush()
continue
if csi_data_len != len(csi_raw_data):
print('RAW_DATA csi_data_len is not equal',csi_data_len,len(csi_raw_data))
log_file_fd.write('RAW_DATA csi_data_len is not equal\n')
log_file_fd.write(strings + '\n')
log_file_fd.flush()
continue
# RAW_DATA 使用独立的数据数组
raw_data_complex[:-1] = raw_data_complex[1:]
# 清空当前行
raw_data_complex[-1, :] = 0
if raw_count == 0:
raw_count = 1
print('RAW_DATA detected, length:', csi_data_len)
# RAW_DATA 不需要初始化颜色,只是接收数据显示相位
# RAW_DATA 转换为复数存入独立数组
for i in range(csi_data_len // 2):
raw_data_complex[-1][i] = complex(csi_raw_data[i * 2 + 1],
csi_raw_data[i * 2])
continue
# CSI_DATA 处理
csv_reader = csv.reader(StringIO(strings))
csi_data = next(csv_reader)
csi_data_len = int (csi_data[-3])
if len(csi_data) != len(DATA_COLUMNS_NAMES) and len(csi_data) != len(DATA_COLUMNS_NAMES_C5C6 ) and len(csi_data) != len(DATA_COLUMNS_NAMES_NEW):
print('element number is not equal',len(csi_data),len(DATA_COLUMNS_NAMES) )
print(strings)
log_file_fd.write('element number is not equal\n')
log_file_fd.write(strings + '\n')
log_file_fd.flush()
continue
try:
csi_raw_data = json.loads(csi_data[-1])
except json.JSONDecodeError:
print('data is incomplete')
log_file_fd.write('data is incomplete\n')
log_file_fd.write(strings + '\n')
log_file_fd.flush()
continue
if csi_data_len != len(csi_raw_data):
print('csi_data_len is not equal',csi_data_len,len(csi_raw_data))
log_file_fd.write('csi_data_len is not equal\n')
log_file_fd.write(strings + '\n')
log_file_fd.flush()
continue
fft_gain = 0 # int(csi_data[6])
agc_gain = 0 # int(csi_data[7])
fft_gains.append(fft_gain)
agc_gains.append(agc_gain)
csv_writer.writerow(csi_data)
# Rotate data to the left
# csi_data_array[:-1] = csi_data_array[1:]
# csi_data_phase[:-1] = csi_data_phase[1:]
csi_data_complex[:-1] = csi_data_complex[1:]
agc_gain_data[:-1] = agc_gain_data[1:]
fft_gain_data[:-1] = fft_gain_data[1:]
agc_gain_data[-1] = agc_gain
fft_gain_data[-1] = fft_gain
if csi_count == 0:
csi_count = 1
print('CSI_DATA detected, length:',csi_data_len)
if csi_data_len == 106:
colors = generate_subcarrier_colors((0,25), (27,53), None, len(csi_raw_data))
elif csi_data_len == 114:
colors = generate_subcarrier_colors((0,27), (29,56), None, len(csi_raw_data))
elif csi_data_len == 52:
colors = generate_subcarrier_colors((0,12), (13,26), None, len(csi_raw_data))
elif csi_data_len == 234 :
colors = generate_subcarrier_colors((0,28), (29,56), (60,116), len(csi_raw_data))
elif csi_data_len == 228 :
colors = generate_subcarrier_colors((0,28), (29,56), (57,114), len(csi_raw_data))
elif csi_data_len == 328 :
colors = generate_subcarrier_colors((0,164), None, None, len(csi_raw_data))
elif csi_data_len == 490 :
colors = generate_subcarrier_colors((0,61), (62,122), (123,245), len(csi_raw_data))
elif csi_data_len == 128 :
colors = generate_subcarrier_colors((0,31), (32,63), None, len(csi_raw_data))
elif csi_data_len == 256 :
colors = generate_subcarrier_colors((0,32), (32,63), (64,128), len(csi_raw_data))
elif csi_data_len == 512 :
colors = generate_subcarrier_colors((0,63), (64,127), (128,256), len(csi_raw_data))
elif csi_data_len == 384 :
colors = generate_subcarrier_colors((0,63), (64,127), (128,192), len(csi_raw_data))
else:
print('Please add more color schemes.')
csi_count = 0
continue
callback(colors)
for i in range(csi_data_len // 2):
csi_data_complex[-1][i] = complex(csi_raw_data[i * 2 + 1],
csi_raw_data[i * 2])
set.close()
return
class SubThread (QThread):
data_ready = pyqtSignal(object)
def __init__(self, serial_port, save_file_name, log_file_name):
super().__init__()
self.serial_port = serial_port
save_file_fd = open(save_file_name, 'w')
self.log_file_fd = open(log_file_name, 'w')
self.csv_writer = csv.writer(save_file_fd)
self.csv_writer.writerow(DATA_COLUMNS_NAMES)
def run(self):
csi_data_read_parse(self.serial_port, self.csv_writer, self.log_file_fd,callback=self.data_ready.emit)
def __del__(self):
self.wait()
self.log_file_fd.close()
if __name__ == '__main__':
if sys.version_info < (3, 6):
print(' Python version should >= 3.6')
exit()
parser = argparse.ArgumentParser(
description='Read CSI data from serial port and display it graphically')
parser.add_argument('-p', '--port', dest='port', action='store', required=True,
help='Serial port number of csv_recv device')
parser.add_argument('-s', '--store', dest='store_file', action='store', default='./csi_data.csv',
help='Save the data printed by the serial port to a file')
parser.add_argument('-l', '--log', dest='log_file', action='store', default='./csi_data_log.txt',
help='Save other serial data the bad CSI data to a log file')
args = parser.parse_args()
serial_port = args.port
file_name = args.store_file
log_file_name = args.log_file
app = QApplication(sys.argv)
subthread = SubThread(serial_port, file_name, log_file_name)
window = csi_data_graphical_window()
subthread.data_ready.connect(window.update_curve_colors)
subthread.start()
window.show()
sys.exit(app.exec())

File diff suppressed because it is too large Load diff