From 4d2c523e06e047b8baa7fa6b7b14ce71a6842507 Mon Sep 17 00:00:00 2001 From: soyunomas Date: Sun, 19 Jul 2026 03:36:07 +0100 Subject: [PATCH 1/9] Add configurable ESP32-C3 WiFi motion detector POC --- .github/workflows/wifi-motion-rssi-c3.yml | 36 +++ WiFi-Motion-RSSI-C3-Supermini/.gitignore | 9 + WiFi-Motion-RSSI-C3-Supermini/CMakeLists.txt | 3 + WiFi-Motion-RSSI-C3-Supermini/PLAN.md | 52 +++++ WiFi-Motion-RSSI-C3-Supermini/README.md | 66 ++++++ .../components/app_config/CMakeLists.txt | 5 + .../components/app_config/app_config.c | 147 ++++++++++++ .../app_config/include/app_config.h | 27 +++ .../components/motion_detector/CMakeLists.txt | 4 + .../motion_detector/include/motion_detector.h | 65 ++++++ .../motion_detector/motion_detector.c | 169 ++++++++++++++ .../main/CMakeLists.txt | 5 + .../main/Kconfig.projbuild | 69 ++++++ WiFi-Motion-RSSI-C3-Supermini/main/app_main.c | 219 ++++++++++++++++++ WiFi-Motion-RSSI-C3-Supermini/partitions.csv | 5 + .../sdkconfig.defaults | 9 + .../tests/host/CMakeLists.txt | 21 ++ .../tests/host/test_motion_detector.c | 178 ++++++++++++++ .../tools/bootstrap.sh | 14 ++ WiFi-Motion-RSSI-C3-Supermini/tools/build.sh | 6 + WiFi-Motion-RSSI-C3-Supermini/tools/flash.sh | 6 + .../tools/test-host.sh | 7 + 22 files changed, 1122 insertions(+) create mode 100644 .github/workflows/wifi-motion-rssi-c3.yml create mode 100644 WiFi-Motion-RSSI-C3-Supermini/.gitignore create mode 100644 WiFi-Motion-RSSI-C3-Supermini/CMakeLists.txt create mode 100644 WiFi-Motion-RSSI-C3-Supermini/PLAN.md create mode 100644 WiFi-Motion-RSSI-C3-Supermini/README.md create mode 100644 WiFi-Motion-RSSI-C3-Supermini/components/app_config/CMakeLists.txt create mode 100644 WiFi-Motion-RSSI-C3-Supermini/components/app_config/app_config.c create mode 100644 WiFi-Motion-RSSI-C3-Supermini/components/app_config/include/app_config.h create mode 100644 WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/CMakeLists.txt create mode 100644 WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/include/motion_detector.h create mode 100644 WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/motion_detector.c create mode 100644 WiFi-Motion-RSSI-C3-Supermini/main/CMakeLists.txt create mode 100644 WiFi-Motion-RSSI-C3-Supermini/main/Kconfig.projbuild create mode 100644 WiFi-Motion-RSSI-C3-Supermini/main/app_main.c create mode 100644 WiFi-Motion-RSSI-C3-Supermini/partitions.csv create mode 100644 WiFi-Motion-RSSI-C3-Supermini/sdkconfig.defaults create mode 100644 WiFi-Motion-RSSI-C3-Supermini/tests/host/CMakeLists.txt create mode 100644 WiFi-Motion-RSSI-C3-Supermini/tests/host/test_motion_detector.c create mode 100755 WiFi-Motion-RSSI-C3-Supermini/tools/bootstrap.sh create mode 100755 WiFi-Motion-RSSI-C3-Supermini/tools/build.sh create mode 100755 WiFi-Motion-RSSI-C3-Supermini/tools/flash.sh create mode 100755 WiFi-Motion-RSSI-C3-Supermini/tools/test-host.sh diff --git a/.github/workflows/wifi-motion-rssi-c3.yml b/.github/workflows/wifi-motion-rssi-c3.yml new file mode 100644 index 0000000..c812acc --- /dev/null +++ b/.github/workflows/wifi-motion-rssi-c3.yml @@ -0,0 +1,36 @@ +name: WiFi Motion RSSI C3 + +on: + push: + paths: + - "WiFi-Motion-RSSI-C3-Supermini/**" + - ".github/workflows/wifi-motion-rssi-c3.yml" + pull_request: + paths: + - "WiFi-Motion-RSSI-C3-Supermini/**" + - ".github/workflows/wifi-motion-rssi-c3.yml" + +jobs: + host-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure + run: cmake -S WiFi-Motion-RSSI-C3-Supermini/tests/host -B build-host + - name: Build + run: cmake --build build-host --parallel + - name: Test + run: ctest --test-dir build-host --output-on-failure + + firmware-build: + runs-on: ubuntu-latest + container: espressif/idf:v6.0.2 + steps: + - uses: actions/checkout@v4 + - name: Build ESP32-C3 firmware + shell: bash + working-directory: WiFi-Motion-RSSI-C3-Supermini + run: | + . "$IDF_PATH/export.sh" + idf.py set-target esp32c3 + idf.py -D SDKCONFIG_DEFAULTS=sdkconfig.defaults build diff --git a/WiFi-Motion-RSSI-C3-Supermini/.gitignore b/WiFi-Motion-RSSI-C3-Supermini/.gitignore new file mode 100644 index 0000000..511e941 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/.gitignore @@ -0,0 +1,9 @@ +/build/ +/build-host/ +/sdkconfig +/sdkconfig.old +/dependencies.lock +*.csv +*.log +.vscode/ +.idea/ diff --git a/WiFi-Motion-RSSI-C3-Supermini/CMakeLists.txt b/WiFi-Motion-RSSI-C3-Supermini/CMakeLists.txt new file mode 100644 index 0000000..6224465 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(wifi_motion_rssi_c3) diff --git a/WiFi-Motion-RSSI-C3-Supermini/PLAN.md b/WiFi-Motion-RSSI-C3-Supermini/PLAN.md new file mode 100644 index 0000000..a75a26a --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/PLAN.md @@ -0,0 +1,52 @@ +# Plan por fases + +## Fase 0 — Base reproducible (esta entrega) + +- ESP-IDF fijado a `v6.0.2`, objetivo `esp32c3`. +- Núcleo de detección desacoplado del Wi-Fi y de FreeRTOS. +- Pruebas host deterministas con GCC/CMake/CTest. +- Compilación de firmware en CI mediante la imagen oficial de Espressif. +- Configuración inicial con Kconfig y estructura versionada para persistencia NVS. + +Criterio de salida: pruebas host verdes y firmware compilando en CI. + +## Fase 1 — Captura RSSI y telemetría + +- Conexión STA 2.4 GHz. +- Muestreo periódico con ahorro de energía Wi-Fi desactivado. +- Salida CSV o JSON Lines. +- LED de estado opcional. +- Registro de baseline, score, umbral y transiciones. + +Criterio de salida: sesión de 15 minutos sin reinicios y sin pérdidas sostenidas de muestras. + +## Fase 2 — Configuración en tiempo de ejecución + +- Portal de configuración local reutilizando patrones de los Smartbutton. +- Credenciales Wi-Fi, intervalo, ventana, calibración, sensibilidad e histéresis en NVS. +- Importación/exportación JSON y restauración de fábrica. +- Validación de rangos y migración versionada de configuración. + +Criterio de salida: todos los parámetros modificables sin recompilar ni perder compatibilidad tras reinicio. + +## Fase 3 — Calibración experimental + +- Herramienta Python para capturar sesiones etiquetadas: vacío, movimiento y perturbaciones. +- Métricas: sensibilidad, falsos positivos por hora, latencia y tiempo de recuperación. +- Perfiles de habitación y selección automática de umbral. +- Dataset pequeño reproducible, sin datos personales. + +Criterio de salida: sensibilidad >= 90 % en el trayecto de prueba y <= 1 falso positivo/hora en reposo. + +## Fase 4 — Integraciones + +- MQTT y HTTP webhook. +- Home Assistant mediante MQTT Discovery. +- OTA con rollback y watchdog. +- Métricas de salud: uptime, reconexiones, heap y calidad de señal. + +## Fase 5 — CSI opcional + +- Evaluar CSI con uno o dos ESP32-C3. +- Mantener la misma interfaz del detector y sustituir únicamente la fuente de características. +- Comparativa RSSI frente a CSI con el mismo protocolo experimental. diff --git a/WiFi-Motion-RSSI-C3-Supermini/README.md b/WiFi-Motion-RSSI-C3-Supermini/README.md new file mode 100644 index 0000000..77bb753 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/README.md @@ -0,0 +1,66 @@ +# WiFi Motion RSSI — ESP32-C3 SuperMini + +Detector experimental de movimiento basado en las variaciones del RSSI del punto de acceso al que está conectado un ESP32-C3. No identifica personas ni detecta de forma fiable a alguien completamente inmóvil. + +## Estado + +Primera implementación funcional: + +- ESP-IDF `v6.0.2`, objetivo `esp32c3`. +- Muestreo de RSSI mediante `esp_wifi_sta_get_ap_info()`. +- Detector con calibración estadística, umbral adaptativo e histéresis. +- Salida CSV o JSON Lines por USB Serial/JTAG. +- Configuración con `idf.py menuconfig` y backend NVS versionado. +- Pruebas host y CI. + +Consulta [PLAN.md](PLAN.md) para las fases y criterios de aceptación. + +## Configuración rápida + +```bash +./tools/bootstrap.sh +source "$HOME/esp/esp-idf-v6.0.2/export.sh" +idf.py set-target esp32c3 +idf.py menuconfig +./tools/build.sh +PORT=/dev/ttyACM0 ./tools/flash.sh +``` + +En `menuconfig`, abre **WiFi motion detector** y configura al menos el SSID y la contraseña. + +## Parámetros + +| Parámetro | Predeterminado | Función | +|---|---:|---| +| Intervalo | 100 ms | Frecuencia de muestreo RSSI | +| Ventana | 20 muestras | Duración aproximada del score | +| Calibración | 120 scores | Baseline inicial en reposo | +| Multiplicador sigma | 6.0 | Sensibilidad adaptativa | +| Umbral mínimo | 0.30 dB/muestra | Evita umbrales degenerados | +| Disparo | 3 scores | Confirmación de movimiento | +| Liberación | 8 scores | Histéresis al volver a reposo | + +Durante la calibración, no debe moverse nadie entre el punto de acceso y el ESP32. + +## Formato CSV + +```text +t_ms,rssi_dbm,score,threshold,state,calibrated +12540,-61,0.4211,0.7832,idle,1 +12640,-54,1.2105,0.7832,motion,1 +``` + +## Pruebas host + +```bash +./tools/test-host.sh +``` + +Las pruebas validan configuración, calibración estable, activación por una secuencia perturbada, histéresis, umbral mínimo y una sesión sintética larga sin falsos positivos. + +## Limitaciones iniciales + +- El SSID y la contraseña se configuran por `menuconfig` en esta fase. El portal y la edición NVS llegan en fase 2. +- La detección depende de la geometría, multitrayecto, interferencias y tráfico del AP. +- No es un sistema de seguridad ni un detector certificado de ocupación. +- El GPIO 8 se usa como LED activo-bajo por defecto; se puede desactivar o cambiar en `menuconfig`. diff --git a/WiFi-Motion-RSSI-C3-Supermini/components/app_config/CMakeLists.txt b/WiFi-Motion-RSSI-C3-Supermini/components/app_config/CMakeLists.txt new file mode 100644 index 0000000..fa9798d --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/components/app_config/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "app_config.c" + INCLUDE_DIRS "include" + REQUIRES nvs_flash motion_detector +) diff --git a/WiFi-Motion-RSSI-C3-Supermini/components/app_config/app_config.c b/WiFi-Motion-RSSI-C3-Supermini/components/app_config/app_config.c new file mode 100644 index 0000000..c0395cc --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/components/app_config/app_config.c @@ -0,0 +1,147 @@ +#include "app_config.h" + +#include "nvs.h" +#include "nvs_flash.h" +#include "sdkconfig.h" + +#define CONFIG_NAMESPACE "motion" +#define CONFIG_BLOB_KEY "runtime_v1" +#define CONFIG_MAGIC 0x4D4F5431UL +#define CONFIG_VERSION 1U + +typedef struct { + uint32_t magic; + uint16_t version; + uint16_t size; + app_runtime_config_t config; +} stored_config_t; + +esp_err_t app_config_init(void) +{ + esp_err_t error = nvs_flash_init(); + if (error == ESP_ERR_NVS_NO_FREE_PAGES || + error == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + error = nvs_flash_init(); + } + return error; +} + +void app_config_defaults(app_runtime_config_t *config) +{ + if (config == NULL) { + return; + } + + *config = (app_runtime_config_t){ + .sample_interval_ms = CONFIG_MOTION_SAMPLE_INTERVAL_MS, + .detector = { + .window_size = CONFIG_MOTION_WINDOW_SIZE, + .calibration_samples = CONFIG_MOTION_CALIBRATION_SAMPLES, + .sigma_multiplier = + (float)CONFIG_MOTION_SIGMA_MULTIPLIER_X100 / 100.0f, + .minimum_threshold = + (float)CONFIG_MOTION_MIN_THRESHOLD_X100 / 100.0f, + .trigger_consecutive = CONFIG_MOTION_TRIGGER_CONSECUTIVE, + .clear_consecutive = CONFIG_MOTION_CLEAR_CONSECUTIVE, + }, + }; +} + +bool app_config_valid(const app_runtime_config_t *config) +{ + return config != NULL && + config->sample_interval_ms >= 50U && + config->sample_interval_ms <= 5000U && + motion_detector_config_valid(&config->detector); +} + +esp_err_t app_config_load(app_runtime_config_t *config) +{ + if (config == NULL) { + return ESP_ERR_INVALID_ARG; + } + + app_config_defaults(config); + + nvs_handle_t handle; + esp_err_t error = nvs_open(CONFIG_NAMESPACE, NVS_READONLY, &handle); + if (error == ESP_ERR_NVS_NOT_FOUND) { + return ESP_OK; + } + if (error != ESP_OK) { + return error; + } + + stored_config_t stored = {0}; + size_t size = sizeof(stored); + error = nvs_get_blob(handle, CONFIG_BLOB_KEY, &stored, &size); + nvs_close(handle); + + if (error == ESP_ERR_NVS_NOT_FOUND) { + return ESP_OK; + } + if (error != ESP_OK) { + return error; + } + + if (size != sizeof(stored) || + stored.magic != CONFIG_MAGIC || + stored.version != CONFIG_VERSION || + stored.size != sizeof(stored.config) || + !app_config_valid(&stored.config)) { + return ESP_ERR_INVALID_STATE; + } + + *config = stored.config; + return ESP_OK; +} + +esp_err_t app_config_save(const app_runtime_config_t *config) +{ + if (!app_config_valid(config)) { + return ESP_ERR_INVALID_ARG; + } + + const stored_config_t stored = { + .magic = CONFIG_MAGIC, + .version = CONFIG_VERSION, + .size = sizeof(*config), + .config = *config, + }; + + nvs_handle_t handle; + esp_err_t error = nvs_open(CONFIG_NAMESPACE, NVS_READWRITE, &handle); + if (error != ESP_OK) { + return error; + } + + error = nvs_set_blob(handle, CONFIG_BLOB_KEY, &stored, sizeof(stored)); + if (error == ESP_OK) { + error = nvs_commit(handle); + } + nvs_close(handle); + return error; +} + +esp_err_t app_config_erase(void) +{ + nvs_handle_t handle; + esp_err_t error = nvs_open(CONFIG_NAMESPACE, NVS_READWRITE, &handle); + if (error == ESP_ERR_NVS_NOT_FOUND) { + return ESP_OK; + } + if (error != ESP_OK) { + return error; + } + + error = nvs_erase_key(handle, CONFIG_BLOB_KEY); + if (error == ESP_ERR_NVS_NOT_FOUND) { + error = ESP_OK; + } + if (error == ESP_OK) { + error = nvs_commit(handle); + } + nvs_close(handle); + return error; +} diff --git a/WiFi-Motion-RSSI-C3-Supermini/components/app_config/include/app_config.h b/WiFi-Motion-RSSI-C3-Supermini/components/app_config/include/app_config.h new file mode 100644 index 0000000..de4e6b6 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/components/app_config/include/app_config.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "esp_err.h" +#include "motion_detector.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + uint32_t sample_interval_ms; + motion_detector_config_t detector; +} app_runtime_config_t; + +esp_err_t app_config_init(void); +void app_config_defaults(app_runtime_config_t *config); +esp_err_t app_config_load(app_runtime_config_t *config); +esp_err_t app_config_save(const app_runtime_config_t *config); +esp_err_t app_config_erase(void); +bool app_config_valid(const app_runtime_config_t *config); + +#ifdef __cplusplus +} +#endif diff --git a/WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/CMakeLists.txt b/WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/CMakeLists.txt new file mode 100644 index 0000000..b482d77 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + SRCS "motion_detector.c" + INCLUDE_DIRS "include" +) diff --git a/WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/include/motion_detector.h b/WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/include/motion_detector.h new file mode 100644 index 0000000..05fa283 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/include/motion_detector.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define MOTION_DETECTOR_MAX_WINDOW 128U + +typedef struct { + size_t window_size; + size_t calibration_samples; + float sigma_multiplier; + float minimum_threshold; + size_t trigger_consecutive; + size_t clear_consecutive; +} motion_detector_config_t; + +typedef enum { + MOTION_DETECTOR_CALIBRATING = 0, + MOTION_DETECTOR_IDLE, + MOTION_DETECTOR_ACTIVE, +} motion_detector_state_t; + +typedef struct { + motion_detector_state_t state; + bool score_ready; + bool calibrated; + bool state_changed; + float score; + float threshold; + float baseline_mean; + float baseline_stddev; +} motion_detector_result_t; + +typedef struct { + motion_detector_config_t config; + int8_t samples[MOTION_DETECTOR_MAX_WINDOW]; + size_t sample_count; + size_t write_index; + + size_t calibration_count; + double calibration_mean; + double calibration_m2; + float threshold; + + size_t above_count; + size_t below_count; + motion_detector_state_t state; +} motion_detector_t; + +bool motion_detector_config_valid(const motion_detector_config_t *config); +bool motion_detector_init(motion_detector_t *detector, + const motion_detector_config_t *config); +void motion_detector_reset(motion_detector_t *detector); +motion_detector_result_t motion_detector_push(motion_detector_t *detector, + int8_t rssi_dbm); +const char *motion_detector_state_name(motion_detector_state_t state); + +#ifdef __cplusplus +} +#endif diff --git a/WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/motion_detector.c b/WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/motion_detector.c new file mode 100644 index 0000000..ef02c8c --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/components/motion_detector/motion_detector.c @@ -0,0 +1,169 @@ +#include "motion_detector.h" + +#include +#include + +static float max_float(float a, float b) +{ + return a > b ? a : b; +} + +bool motion_detector_config_valid(const motion_detector_config_t *config) +{ + return config != NULL && + config->window_size >= 4U && + config->window_size <= MOTION_DETECTOR_MAX_WINDOW && + config->calibration_samples >= 2U && + isfinite(config->sigma_multiplier) && + config->sigma_multiplier > 0.0f && + isfinite(config->minimum_threshold) && + config->minimum_threshold > 0.0f && + config->trigger_consecutive >= 1U && + config->clear_consecutive >= 1U; +} + +bool motion_detector_init(motion_detector_t *detector, + const motion_detector_config_t *config) +{ + if (detector == NULL || !motion_detector_config_valid(config)) { + return false; + } + + memset(detector, 0, sizeof(*detector)); + detector->config = *config; + detector->state = MOTION_DETECTOR_CALIBRATING; + detector->threshold = config->minimum_threshold; + return true; +} + +void motion_detector_reset(motion_detector_t *detector) +{ + if (detector == NULL) { + return; + } + + const motion_detector_config_t config = detector->config; + (void)motion_detector_init(detector, &config); +} + +static float calculate_score(const motion_detector_t *detector) +{ + double total = 0.0; + const size_t window = detector->config.window_size; + + for (size_t i = 1U; i < window; ++i) { + const size_t previous_index = (detector->write_index + i - 1U) % window; + const size_t current_index = (detector->write_index + i) % window; + total += fabs((double)detector->samples[current_index] - + (double)detector->samples[previous_index]); + } + + return (float)(total / (double)(window - 1U)); +} + +static float calibration_stddev(const motion_detector_t *detector) +{ + if (detector->calibration_count < 2U) { + return 0.0f; + } + + return (float)sqrt(detector->calibration_m2 / + (double)(detector->calibration_count - 1U)); +} + +static motion_detector_result_t make_result(const motion_detector_t *detector, + bool score_ready, + bool changed, + float score) +{ + motion_detector_result_t result = { + .state = detector->state, + .score_ready = score_ready, + .calibrated = detector->state != MOTION_DETECTOR_CALIBRATING, + .state_changed = changed, + .score = score, + .threshold = detector->threshold, + .baseline_mean = (float)detector->calibration_mean, + .baseline_stddev = calibration_stddev(detector), + }; + return result; +} + +motion_detector_result_t motion_detector_push(motion_detector_t *detector, + int8_t rssi_dbm) +{ + if (detector == NULL) { + const motion_detector_result_t invalid = {0}; + return invalid; + } + + const size_t window = detector->config.window_size; + detector->samples[detector->write_index] = rssi_dbm; + detector->write_index = (detector->write_index + 1U) % window; + if (detector->sample_count < window) { + detector->sample_count++; + } + + if (detector->sample_count < window) { + return make_result(detector, false, false, 0.0f); + } + + const float score = calculate_score(detector); + bool changed = false; + + if (detector->state == MOTION_DETECTOR_CALIBRATING) { + detector->calibration_count++; + const double delta = (double)score - detector->calibration_mean; + detector->calibration_mean += delta / (double)detector->calibration_count; + const double delta2 = (double)score - detector->calibration_mean; + detector->calibration_m2 += delta * delta2; + + if (detector->calibration_count >= detector->config.calibration_samples) { + const float stddev = calibration_stddev(detector); + detector->threshold = max_float( + detector->config.minimum_threshold, + (float)detector->calibration_mean + + detector->config.sigma_multiplier * stddev); + detector->state = MOTION_DETECTOR_IDLE; + changed = true; + } + + return make_result(detector, true, changed, score); + } + + if (score > detector->threshold) { + detector->above_count++; + detector->below_count = 0U; + } else { + detector->below_count++; + detector->above_count = 0U; + } + + if (detector->state == MOTION_DETECTOR_IDLE && + detector->above_count >= detector->config.trigger_consecutive) { + detector->state = MOTION_DETECTOR_ACTIVE; + detector->above_count = 0U; + changed = true; + } else if (detector->state == MOTION_DETECTOR_ACTIVE && + detector->below_count >= detector->config.clear_consecutive) { + detector->state = MOTION_DETECTOR_IDLE; + detector->below_count = 0U; + changed = true; + } + + return make_result(detector, true, changed, score); +} + +const char *motion_detector_state_name(motion_detector_state_t state) +{ + switch (state) { + case MOTION_DETECTOR_CALIBRATING: + return "calibrating"; + case MOTION_DETECTOR_IDLE: + return "idle"; + case MOTION_DETECTOR_ACTIVE: + return "motion"; + default: + return "unknown"; + } +} diff --git a/WiFi-Motion-RSSI-C3-Supermini/main/CMakeLists.txt b/WiFi-Motion-RSSI-C3-Supermini/main/CMakeLists.txt new file mode 100644 index 0000000..a86bb6c --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "app_main.c" + INCLUDE_DIRS "." + REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_driver_gpio motion_detector app_config +) diff --git a/WiFi-Motion-RSSI-C3-Supermini/main/Kconfig.projbuild b/WiFi-Motion-RSSI-C3-Supermini/main/Kconfig.projbuild new file mode 100644 index 0000000..a2e4bc9 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/main/Kconfig.projbuild @@ -0,0 +1,69 @@ +menu "WiFi motion detector" + +config MOTION_WIFI_SSID + string "WiFi SSID" + default "" + help + Bootstrap SSID. Runtime provisioning is planned for phase 2. + +config MOTION_WIFI_PASSWORD + string "WiFi password" + default "" + +config MOTION_SAMPLE_INTERVAL_MS + int "RSSI sampling interval (ms)" + range 50 5000 + default 100 + +config MOTION_WINDOW_SIZE + int "Activity window (samples)" + range 4 128 + default 20 + +config MOTION_CALIBRATION_SAMPLES + int "Calibration scores" + range 10 10000 + default 120 + +config MOTION_SIGMA_MULTIPLIER_X100 + int "Threshold sigma multiplier x100" + range 100 1200 + default 600 + +config MOTION_MIN_THRESHOLD_X100 + int "Minimum activity threshold x100" + range 1 1000 + default 30 + +config MOTION_TRIGGER_CONSECUTIVE + int "Consecutive scores to trigger" + range 1 100 + default 3 + +config MOTION_CLEAR_CONSECUTIVE + int "Consecutive scores to clear" + range 1 100 + default 8 + +config MOTION_LED_GPIO + int "Status LED GPIO (-1 disables)" + range -1 21 + default 8 + +config MOTION_LED_ACTIVE_LOW + bool "Status LED is active-low" + default y + +choice MOTION_OUTPUT_FORMAT + prompt "Serial output format" + default MOTION_OUTPUT_CSV + +config MOTION_OUTPUT_CSV + bool "CSV" + +config MOTION_OUTPUT_JSON + bool "JSON lines" + +endchoice + +endmenu diff --git a/WiFi-Motion-RSSI-C3-Supermini/main/app_main.c b/WiFi-Motion-RSSI-C3-Supermini/main/app_main.c new file mode 100644 index 0000000..0b7a018 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/main/app_main.c @@ -0,0 +1,219 @@ +#include +#include +#include +#include + +#include "app_config.h" +#include "driver/gpio.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_timer.h" +#include "esp_wifi.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/task.h" +#include "motion_detector.h" +#include "sdkconfig.h" + +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAILED_BIT BIT1 +#define WIFI_MAXIMUM_RETRY 10 + +static const char *TAG = "wifi_motion"; +static EventGroupHandle_t wifi_event_group; +static int wifi_retry_count; + +static void set_motion_led(bool enabled) +{ +#if CONFIG_MOTION_LED_GPIO >= 0 + const int level = CONFIG_MOTION_LED_ACTIVE_LOW ? !enabled : enabled; + gpio_set_level((gpio_num_t)CONFIG_MOTION_LED_GPIO, level); +#else + (void)enabled; +#endif +} + +static void init_motion_led(void) +{ +#if CONFIG_MOTION_LED_GPIO >= 0 + const gpio_config_t config = { + .pin_bit_mask = 1ULL << CONFIG_MOTION_LED_GPIO, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + ESP_ERROR_CHECK(gpio_config(&config)); + set_motion_led(false); +#endif +} + +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 && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && + event_id == WIFI_EVENT_STA_DISCONNECTED) { + if (wifi_retry_count < WIFI_MAXIMUM_RETRY) { + wifi_retry_count++; + esp_wifi_connect(); + ESP_LOGW(TAG, "WiFi reconnect attempt %d/%d", + wifi_retry_count, WIFI_MAXIMUM_RETRY); + } else { + xEventGroupSetBits(wifi_event_group, WIFI_FAILED_BIT); + } + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + wifi_retry_count = 0; + xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT); + } +} + +static esp_err_t wifi_connect(void) +{ + if (strlen(CONFIG_MOTION_WIFI_SSID) == 0U) { + ESP_LOGE(TAG, "WiFi SSID is empty; run idf.py menuconfig"); + return ESP_ERR_INVALID_STATE; + } + + wifi_event_group = xEventGroupCreate(); + if (wifi_event_group == NULL) { + return ESP_ERR_NO_MEM; + } + + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_sta(); + + const wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&init_config)); + + esp_event_handler_instance_t wifi_handler; + esp_event_handler_instance_t ip_handler; + ESP_ERROR_CHECK(esp_event_handler_instance_register( + WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, + &wifi_handler)); + ESP_ERROR_CHECK(esp_event_handler_instance_register( + IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, + &ip_handler)); + + wifi_config_t wifi_config = {0}; + strlcpy((char *)wifi_config.sta.ssid, + CONFIG_MOTION_WIFI_SSID, + sizeof(wifi_config.sta.ssid)); + strlcpy((char *)wifi_config.sta.password, + CONFIG_MOTION_WIFI_PASSWORD, + sizeof(wifi_config.sta.password)); + wifi_config.sta.threshold.authmode = + strlen(CONFIG_MOTION_WIFI_PASSWORD) == 0U ? + WIFI_AUTH_OPEN : WIFI_AUTH_WPA2_PSK; + wifi_config.sta.pmf_cfg.capable = true; + wifi_config.sta.pmf_cfg.required = false; + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); + + const EventBits_t bits = xEventGroupWaitBits( + wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAILED_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY); + + if ((bits & WIFI_CONNECTED_BIT) != 0U) { + ESP_LOGI(TAG, "Connected to %s", CONFIG_MOTION_WIFI_SSID); + return ESP_OK; + } + + ESP_LOGE(TAG, "Unable to connect to %s", CONFIG_MOTION_WIFI_SSID); + return ESP_FAIL; +} + +static void emit_sample(int64_t timestamp_ms, + int rssi, + const motion_detector_result_t *result) +{ +#if CONFIG_MOTION_OUTPUT_JSON + printf("{\"t_ms\":%" PRId64 + ",\"rssi\":%d,\"score\":%.4f,\"threshold\":%.4f" + ",\"state\":\"%s\",\"calibrated\":%s}\n", + timestamp_ms, + rssi, + result->score, + result->threshold, + motion_detector_state_name(result->state), + result->calibrated ? "true" : "false"); +#else + printf("%" PRId64 ",%d,%.4f,%.4f,%s,%d\n", + timestamp_ms, + rssi, + result->score, + result->threshold, + motion_detector_state_name(result->state), + result->calibrated ? 1 : 0); +#endif + fflush(stdout); +} + +void app_main(void) +{ + ESP_ERROR_CHECK(app_config_init()); + init_motion_led(); + + app_runtime_config_t runtime_config; + esp_err_t config_error = app_config_load(&runtime_config); + if (config_error != ESP_OK) { + ESP_LOGW(TAG, "Invalid persisted config (%s); using defaults", + esp_err_to_name(config_error)); + app_config_defaults(&runtime_config); + } + + motion_detector_t detector; + if (!motion_detector_init(&detector, &runtime_config.detector)) { + ESP_LOGE(TAG, "Detector configuration is invalid"); + return; + } + + ESP_ERROR_CHECK(wifi_connect()); + +#if CONFIG_MOTION_OUTPUT_CSV + printf("t_ms,rssi_dbm,score,threshold,state,calibrated\n"); +#endif + + TickType_t next_wakeup = xTaskGetTickCount(); + for (;;) { + wifi_ap_record_t ap_info; + const esp_err_t error = esp_wifi_sta_get_ap_info(&ap_info); + if (error == ESP_OK) { + const motion_detector_result_t result = + motion_detector_push(&detector, ap_info.rssi); + set_motion_led(result.state == MOTION_DETECTOR_ACTIVE); + emit_sample(esp_timer_get_time() / 1000, + ap_info.rssi, + &result); + + if (result.state_changed) { + ESP_LOGI(TAG, + "State=%s score=%.3f threshold=%.3f baseline=%.3f±%.3f", + motion_detector_state_name(result.state), + result.score, + result.threshold, + result.baseline_mean, + result.baseline_stddev); + } + } else { + ESP_LOGW(TAG, "RSSI read failed: %s", esp_err_to_name(error)); + } + + vTaskDelayUntil(&next_wakeup, + pdMS_TO_TICKS(runtime_config.sample_interval_ms)); + } +} diff --git a/WiFi-Motion-RSSI-C3-Supermini/partitions.csv b/WiFi-Motion-RSSI-C3-Supermini/partitions.csv new file mode 100644 index 0000000..50f9c27 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 0x180000, +storage, data, spiffs, 0x190000,0x270000, diff --git a/WiFi-Motion-RSSI-C3-Supermini/sdkconfig.defaults b/WiFi-Motion-RSSI-C3-Supermini/sdkconfig.defaults new file mode 100644 index 0000000..3c99a69 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/sdkconfig.defaults @@ -0,0 +1,9 @@ +CONFIG_IDF_TARGET="esp32c3" +CONFIG_FREERTOS_HZ=1000 +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_ESPTOOLPY_FLASHSIZE="4MB" +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_CONSOLE_UART_DEFAULT=n +CONFIG_COMPILER_OPTIMIZATION_PERF=y +CONFIG_LOG_DEFAULT_LEVEL_INFO=y diff --git a/WiFi-Motion-RSSI-C3-Supermini/tests/host/CMakeLists.txt b/WiFi-Motion-RSSI-C3-Supermini/tests/host/CMakeLists.txt new file mode 100644 index 0000000..1d7d188 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/tests/host/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.16) +project(motion_detector_host_tests C) + +enable_testing() + +add_executable( + motion_detector_tests + test_motion_detector.c + ../../components/motion_detector/motion_detector.c +) + +target_include_directories( + motion_detector_tests + PRIVATE ../../components/motion_detector/include +) + +target_compile_features(motion_detector_tests PRIVATE c_std_11) +target_compile_options(motion_detector_tests PRIVATE -Wall -Wextra -Wpedantic -Werror) +target_link_libraries(motion_detector_tests PRIVATE m) + +add_test(NAME motion_detector_tests COMMAND motion_detector_tests) diff --git a/WiFi-Motion-RSSI-C3-Supermini/tests/host/test_motion_detector.c b/WiFi-Motion-RSSI-C3-Supermini/tests/host/test_motion_detector.c new file mode 100644 index 0000000..828490f --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/tests/host/test_motion_detector.c @@ -0,0 +1,178 @@ +#include +#include +#include +#include +#include + +#include "motion_detector.h" + +#define ASSERT_TRUE(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "Assertion failed at %s:%d: %s\n", \ + __FILE__, __LINE__, #condition); \ + exit(EXIT_FAILURE); \ + } \ + } while (0) + +#define ASSERT_NEAR(actual, expected, tolerance) \ + ASSERT_TRUE(fabs((double)(actual) - (double)(expected)) <= (tolerance)) + +static motion_detector_config_t test_config(void) +{ + return (motion_detector_config_t){ + .window_size = 6U, + .calibration_samples = 8U, + .sigma_multiplier = 4.0f, + .minimum_threshold = 0.30f, + .trigger_consecutive = 2U, + .clear_consecutive = 3U, + }; +} + +static motion_detector_result_t push_many(motion_detector_t *detector, + const int8_t *values, + size_t count) +{ + motion_detector_result_t result = {0}; + for (size_t i = 0; i < count; ++i) { + result = motion_detector_push(detector, values[i]); + } + return result; +} + +static void test_config_validation(void) +{ + motion_detector_config_t config = test_config(); + ASSERT_TRUE(motion_detector_config_valid(&config)); + config.window_size = 3U; + ASSERT_TRUE(!motion_detector_config_valid(&config)); + config = test_config(); + config.minimum_threshold = 0.0f; + ASSERT_TRUE(!motion_detector_config_valid(&config)); +} + +static void test_stable_signal_calibrates_without_triggering(void) +{ + motion_detector_t detector; + const motion_detector_config_t config = test_config(); + ASSERT_TRUE(motion_detector_init(&detector, &config)); + const int8_t stable[] = { + -60, -60, -59, -60, -60, -59, -60, -60, + -59, -60, -60, -59, -60, -60, -59, -60, + -60, -59, -60, -60, -59, -60, -60, -59, + }; + const motion_detector_result_t result = + push_many(&detector, stable, sizeof(stable) / sizeof(stable[0])); + ASSERT_TRUE(result.calibrated); + ASSERT_TRUE(result.state == MOTION_DETECTOR_IDLE); + ASSERT_TRUE(result.threshold >= config.minimum_threshold); +} + +static void calibrate(motion_detector_t *detector) +{ + const int8_t stable[] = { + -60, -60, -60, -60, -60, -60, -60, + -60, -60, -60, -60, -60, -60, -60, + }; + const motion_detector_result_t result = + push_many(detector, stable, sizeof(stable) / sizeof(stable[0])); + ASSERT_TRUE(result.calibrated); + ASSERT_TRUE(result.state == MOTION_DETECTOR_IDLE); +} + +static void test_motion_sequence_triggers(void) +{ + motion_detector_t detector; + const motion_detector_config_t config = test_config(); + ASSERT_TRUE(motion_detector_init(&detector, &config)); + calibrate(&detector); + const int8_t movement[] = {-52, -69, -50, -71, -49, -70, -51, -68}; + const motion_detector_result_t result = + push_many(&detector, movement, sizeof(movement) / sizeof(movement[0])); + ASSERT_TRUE(result.state == MOTION_DETECTOR_ACTIVE); + ASSERT_TRUE(result.score > result.threshold); +} + +static void test_hysteresis_clears_after_stability(void) +{ + motion_detector_t detector; + const motion_detector_config_t config = test_config(); + ASSERT_TRUE(motion_detector_init(&detector, &config)); + calibrate(&detector); + const int8_t movement[] = {-52, -69, -50, -71, -49, -70, -51, -68}; + motion_detector_result_t result = + push_many(&detector, movement, sizeof(movement) / sizeof(movement[0])); + ASSERT_TRUE(result.state == MOTION_DETECTOR_ACTIVE); + const int8_t stable[] = { + -60, -60, -60, -60, -60, -60, -60, -60, + -60, -60, -60, -60, -60, -60, -60, -60, + }; + result = push_many(&detector, stable, sizeof(stable) / sizeof(stable[0])); + ASSERT_TRUE(result.state == MOTION_DETECTOR_IDLE); +} + +static void test_minimum_threshold_is_respected(void) +{ + motion_detector_t detector; + motion_detector_config_t config = test_config(); + config.minimum_threshold = 1.25f; + ASSERT_TRUE(motion_detector_init(&detector, &config)); + calibrate(&detector); + ASSERT_NEAR(detector.threshold, 1.25f, 0.0001); +} + +static uint32_t pseudo_random_state = 0x12345678U; + +static int8_t stable_noise_sample(void) +{ + pseudo_random_state = pseudo_random_state * 1664525U + 1013904223U; + const int noise = (int)((pseudo_random_state >> 24U) % 3U) - 1; + return (int8_t)(-60 + noise); +} + +static void test_long_stable_run_and_detection_latency(void) +{ + motion_detector_t detector; + const motion_detector_config_t config = { + .window_size = 20U, + .calibration_samples = 120U, + .sigma_multiplier = 6.0f, + .minimum_threshold = 0.30f, + .trigger_consecutive = 3U, + .clear_consecutive = 8U, + }; + ASSERT_TRUE(motion_detector_init(&detector, &config)); + motion_detector_result_t result = {0}; + for (size_t i = 0; i < 250U; ++i) { + result = motion_detector_push(&detector, stable_noise_sample()); + } + ASSERT_TRUE(result.calibrated); + ASSERT_TRUE(result.state == MOTION_DETECTOR_IDLE); + for (size_t i = 0; i < 1000U; ++i) { + result = motion_detector_push(&detector, stable_noise_sample()); + ASSERT_TRUE(result.state != MOTION_DETECTOR_ACTIVE); + } + size_t samples_to_trigger = 0U; + for (; samples_to_trigger < 12U; ++samples_to_trigger) { + const int8_t rssi = (samples_to_trigger % 2U == 0U) ? -48 : -73; + result = motion_detector_push(&detector, rssi); + if (result.state == MOTION_DETECTOR_ACTIVE) { + break; + } + } + ASSERT_TRUE(result.state == MOTION_DETECTOR_ACTIVE); + ASSERT_TRUE(samples_to_trigger < 12U); +} + +int main(void) +{ + test_config_validation(); + test_stable_signal_calibrates_without_triggering(); + test_motion_sequence_triggers(); + test_hysteresis_clears_after_stability(); + test_minimum_threshold_is_respected(); + test_long_stable_run_and_detection_latency(); + puts("All motion detector tests passed."); + return EXIT_SUCCESS; +} diff --git a/WiFi-Motion-RSSI-C3-Supermini/tools/bootstrap.sh b/WiFi-Motion-RSSI-C3-Supermini/tools/bootstrap.sh new file mode 100755 index 0000000..5b735ec --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/tools/bootstrap.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +IDF_VERSION="${IDF_VERSION:-v6.0.2}" +IDF_ROOT="${IDF_ROOT:-$HOME/esp/esp-idf-${IDF_VERSION}}" + +if [[ ! -d "$IDF_ROOT/.git" ]]; then + mkdir -p "$(dirname "$IDF_ROOT")" + git clone --branch "$IDF_VERSION" --recursive \ + https://github.com/espressif/esp-idf.git "$IDF_ROOT" +fi + +"$IDF_ROOT/install.sh" esp32c3 +printf '\nRun:\n source %q\n' "$IDF_ROOT/export.sh" diff --git a/WiFi-Motion-RSSI-C3-Supermini/tools/build.sh b/WiFi-Motion-RSSI-C3-Supermini/tools/build.sh new file mode 100755 index 0000000..675a3a6 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/tools/build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +: "${IDF_PATH:?Source the ESP-IDF export.sh first}" +idf.py set-target esp32c3 +idf.py -D SDKCONFIG_DEFAULTS=sdkconfig.defaults build diff --git a/WiFi-Motion-RSSI-C3-Supermini/tools/flash.sh b/WiFi-Motion-RSSI-C3-Supermini/tools/flash.sh new file mode 100755 index 0000000..d7ce6aa --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/tools/flash.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +: "${IDF_PATH:?Source the ESP-IDF export.sh first}" +PORT="${PORT:-/dev/ttyACM0}" +idf.py -p "$PORT" flash monitor diff --git a/WiFi-Motion-RSSI-C3-Supermini/tools/test-host.sh b/WiFi-Motion-RSSI-C3-Supermini/tools/test-host.sh new file mode 100755 index 0000000..a8f671c --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/tools/test-host.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILD_DIR="${BUILD_DIR:-$ROOT/build-host}" +cmake -S "$ROOT/tests/host" -B "$BUILD_DIR" +cmake --build "$BUILD_DIR" --parallel +ctest --test-dir "$BUILD_DIR" --output-on-failure From cd134af85a2dbfa9b235f7fb775166def96b2780 Mon Sep 17 00:00:00 2001 From: soyunomas Date: Sun, 19 Jul 2026 03:36:54 +0100 Subject: [PATCH 2/9] Document WiFi motion detector project --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d2e570..def5525 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Nodos inteligentes configurables para control de escenas y automatización media | [**Smartbutton-C3-Plus-5**](./Smartbutton-esp32-C3-Supermini-Plus-5) | 5 Botones | LED RGB WS2812 | Paneles de control de múltiples escenas. | | [**Smartbutton-C3-SENSORS**](./Smartbutton-esp32-C3-Supermini-SENSORS) | 3 Entradas | LED Azul Onboard | Sensores de movimiento (**PIR, Radar**) y botones. | | [**Smartbutton-C3-Supermini**](./Smartbutton-esp32-C3-Supermini) | 2 Botones | LED Azul Onboard | Control simple en formato miniatura (~2€). | +| [**WiFi-Motion-RSSI-C3**](./WiFi-Motion-RSSI-C3-Supermini) | RSSI WiFi | LED opcional + CSV/JSON | Detección experimental de movimiento sin sensor externo. | ### Serie ESP32-C5 (WiFi 6 & Dual-Band) | Proyecto | Protocolos | Feedback Visual | Ventaja Clave | @@ -57,6 +58,7 @@ Nodos inteligentes configurables para control de escenas y automatización media ### 4. ¿Botón mecánico o Sensor de Presencia? * **Plus-5 / Supermini:** Diseñados para interacción humana directa. El modelo **Plus-5** destaca por su **LED RGB** que cambia de color según el estado (conectando, éxito, error). * **SENSORS:** Diseñado para automatización invisible. Incluye lógica de **estabilización** (evita falsos positivos al encender el sensor) y **cooldown** (tiempo de espera entre detecciones). +* **WiFi-Motion-RSSI-C3:** Adecuado para experimentación sin sensores adicionales; detecta perturbaciones de la señal, no presencia estática certificada. --- @@ -70,4 +72,5 @@ Todos los firmwares comparten una base técnica de nivel industrial: * **Seguridad:** Panel de administración protegido por credenciales configurables. --- -*Mantenido por [soyunomas]. Todos los proyectos bajo licencia MIT.* + +*Mantenido por [soyunomas]. Todos los proyectos bajo licencia MIT.* \ No newline at end of file From 60e13059a90201141723322ee4d178d8b82582b9 Mon Sep 17 00:00:00 2001 From: soyunomas Date: Sun, 19 Jul 2026 03:38:04 +0100 Subject: [PATCH 3/9] Declare esp_timer component dependency --- WiFi-Motion-RSSI-C3-Supermini/main/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WiFi-Motion-RSSI-C3-Supermini/main/CMakeLists.txt b/WiFi-Motion-RSSI-C3-Supermini/main/CMakeLists.txt index a86bb6c..25a7378 100644 --- a/WiFi-Motion-RSSI-C3-Supermini/main/CMakeLists.txt +++ b/WiFi-Motion-RSSI-C3-Supermini/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRCS "app_main.c" INCLUDE_DIRS "." - REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_driver_gpio motion_detector app_config + REQUIRES esp_wifi esp_event esp_netif esp_timer nvs_flash esp_driver_gpio motion_detector app_config ) From d1c80a551260dd0c0926a70b1d8c6ed4d98cab69 Mon Sep 17 00:00:00 2001 From: soyunomas Date: Sun, 19 Jul 2026 03:40:25 +0100 Subject: [PATCH 4/9] Handle active-high LED configuration safely --- WiFi-Motion-RSSI-C3-Supermini/main/app_main.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WiFi-Motion-RSSI-C3-Supermini/main/app_main.c b/WiFi-Motion-RSSI-C3-Supermini/main/app_main.c index 0b7a018..8f79450 100644 --- a/WiFi-Motion-RSSI-C3-Supermini/main/app_main.c +++ b/WiFi-Motion-RSSI-C3-Supermini/main/app_main.c @@ -27,7 +27,11 @@ static int wifi_retry_count; static void set_motion_led(bool enabled) { #if CONFIG_MOTION_LED_GPIO >= 0 - const int level = CONFIG_MOTION_LED_ACTIVE_LOW ? !enabled : enabled; +#if CONFIG_MOTION_LED_ACTIVE_LOW + const int level = !enabled; +#else + const int level = enabled; +#endif gpio_set_level((gpio_num_t)CONFIG_MOTION_LED_GPIO, level); #else (void)enabled; From 4d96f2b223e49cbe8e81744a683a7d942795f6b9 Mon Sep 17 00:00:00 2001 From: soyunomas Date: Sun, 19 Jul 2026 03:40:57 +0100 Subject: [PATCH 5/9] Run detector tests with sanitizers in CI --- .github/workflows/wifi-motion-rssi-c3.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/wifi-motion-rssi-c3.yml b/.github/workflows/wifi-motion-rssi-c3.yml index c812acc..c62106b 100644 --- a/.github/workflows/wifi-motion-rssi-c3.yml +++ b/.github/workflows/wifi-motion-rssi-c3.yml @@ -21,6 +21,19 @@ jobs: run: cmake --build build-host --parallel - name: Test run: ctest --test-dir build-host --output-on-failure + - name: Configure sanitizers + run: >- + cmake -S WiFi-Motion-RSSI-C3-Supermini/tests/host + -B build-sanitize + -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" + - name: Build sanitizers + run: cmake --build build-sanitize --parallel + - name: Test sanitizers + env: + ASAN_OPTIONS: detect_leaks=1 + UBSAN_OPTIONS: halt_on_error=1 + run: ctest --test-dir build-sanitize --output-on-failure firmware-build: runs-on: ubuntu-latest From 5e3184891fea22a4e24c6facb31311a57c4b52c9 Mon Sep 17 00:00:00 2001 From: soyunomas Date: Sun, 19 Jul 2026 03:58:55 +0100 Subject: [PATCH 6/9] docs: revise roadmap using WiFi sensing research --- WiFi-Motion-RSSI-C3-Supermini/PLAN.md | 141 ++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 22 deletions(-) diff --git a/WiFi-Motion-RSSI-C3-Supermini/PLAN.md b/WiFi-Motion-RSSI-C3-Supermini/PLAN.md index a75a26a..57fda8b 100644 --- a/WiFi-Motion-RSSI-C3-Supermini/PLAN.md +++ b/WiFi-Motion-RSSI-C3-Supermini/PLAN.md @@ -1,6 +1,23 @@ # Plan por fases -## Fase 0 — Base reproducible (esta entrega) +## Decisión técnica basada en la literatura + +El proyecto seguirá una estrategia de dos niveles: + +1. **RSSI como línea base de bajo coste.** Debe demostrar detección reproducible de movimiento evidente con un ESP32-C3 y un punto de acceso, sin prometer presencia estática, localización ni clasificación de actividades. +2. **CSI como siguiente hito experimental.** Una vez caracterizado RSSI, se mantendrá la misma interfaz del detector y se sustituirá la fuente de características por CSI, primero con un ESP32-C3 y un router y después, si es necesario, con dos ESP32-C3. + +Esta decisión se apoya en los siguientes resultados: + +- **RASID** demuestra detección pasiva robusta con RSSI, calibración de reposo, dispersión en ventanas y adaptación estadística. Su arquitectura suele beneficiarse de varios enlaces Wi-Fi. +- **WiRSSI** confirma que RSSI contiene información útil para sensing, aunque sus resultados de seguimiento emplean varias antenas y muestreo mucho más rápido que el disponible mediante un RSSI agregado convencional. +- **FIMD** y **PhaseMode** muestran que CSI proporciona mayor resolución y robustez que RSSI para detección fina. +- Los experimentos publicados con **ESP32 y CSI** demuestran presencia, reconocimiento de actividad y localización aproximada, pero dependen fuertemente de geometría, número de receptores, antenas y procesamiento. +- Espressif mantiene `esp-csi` y `esp-radar`, incluyendo una ruta compatible con ESP32-C3. + +Por tanto, RSSI será un baseline medido y documentado, no el objetivo final de prestaciones. + +## Fase 0 — Base reproducible (completada) - ESP-IDF fijado a `v6.0.2`, objetivo `esp32c3`. - Núcleo de detección desacoplado del Wi-Fi y de FreeRTOS. @@ -14,39 +31,119 @@ Criterio de salida: pruebas host verdes y firmware compilando en CI. - Conexión STA 2.4 GHz. - Muestreo periódico con ahorro de energía Wi-Fi desactivado. +- Medición de la cadencia real de actualización del RSSI, diferenciándola de la frecuencia con la que se consulta la API. - Salida CSV o JSON Lines. - LED de estado opcional. -- Registro de baseline, score, umbral y transiciones. +- Registro de RSSI, baseline, score, umbral, estado y transiciones. +- Registro de pérdidas de muestra, reconexiones, canal, BSSID y uptime. + +Criterio de salida: sesión de 15 minutos sin reinicios, sin pérdidas sostenidas y con cadencia real caracterizada. -Criterio de salida: sesión de 15 minutos sin reinicios y sin pérdidas sostenidas de muestras. +## Fase 2 — Detector RSSI configurable y comparativo -## Fase 2 — Configuración en tiempo de ejecución +Se implementarán varios extractores de características sobre la misma ventana de muestras: + +- Media de diferencias absolutas, ya disponible como baseline inicial. +- Desviación estándar. +- Varianza muestral. +- Rango máximo-mínimo. +- Desviación absoluta mediana. + +Además: + +- Umbral adaptativo por media y desviación estándar. +- Alternativa robusta por mediana, MAD o percentiles. +- Actualización lenta del baseline únicamente durante períodos clasificados como reposo. +- Histéresis y confirmación temporal independientes para activación y liberación. +- Perfiles `low`, `balanced` y `high`. +- Posibilidad de fijar el algoritmo y todos sus parámetros desde configuración. + +Criterio de salida: todos los algoritmos pasan el mismo conjunto de pruebas sintéticas y producen telemetría comparable. + +## Fase 3 — Configuración en tiempo de ejecución - Portal de configuración local reutilizando patrones de los Smartbutton. -- Credenciales Wi-Fi, intervalo, ventana, calibración, sensibilidad e histéresis en NVS. -- Importación/exportación JSON y restauración de fábrica. +- Credenciales Wi-Fi, intervalo, ventana, calibración, algoritmo, sensibilidad e histéresis en NVS. +- Importación y exportación JSON. +- Restauración de fábrica. - Validación de rangos y migración versionada de configuración. +- Endpoint de diagnóstico con configuración efectiva y estado del detector. -Criterio de salida: todos los parámetros modificables sin recompilar ni perder compatibilidad tras reinicio. +Criterio de salida: todos los parámetros modificables sin recompilar y conservados correctamente tras reinicio y actualización. -## Fase 3 — Calibración experimental +## Fase 4 — Protocolo experimental RSSI -- Herramienta Python para capturar sesiones etiquetadas: vacío, movimiento y perturbaciones. -- Métricas: sensibilidad, falsos positivos por hora, latencia y tiempo de recuperación. -- Perfiles de habitación y selección automática de umbral. -- Dataset pequeño reproducible, sin datos personales. +- Herramienta Python para capturar sesiones etiquetadas: habitación vacía, cruce del enlace, movimiento lateral, movimiento fuera del trayecto y perturbaciones no humanas. +- Pruebas con varias posiciones, alturas, orientaciones y distancias entre AP y ESP32. +- Comparación de algoritmos y perfiles sobre las mismas capturas. +- Separación estricta entre datos de ajuste y datos de evaluación. +- Dataset pequeño reproducible y sin datos personales. -Criterio de salida: sensibilidad >= 90 % en el trayecto de prueba y <= 1 falso positivo/hora en reposo. +Métricas obligatorias: -## Fase 4 — Integraciones +- Sensibilidad o recall por evento. +- Precisión y F1. +- Falsos positivos por hora. +- Latencia de activación. +- Tiempo de liberación. +- Porcentaje de muestras repetidas o sin actualización real. +- Estabilidad del baseline. -- MQTT y HTTP webhook. -- Home Assistant mediante MQTT Discovery. -- OTA con rollback y watchdog. -- Métricas de salud: uptime, reconexiones, heap y calidad de señal. +Objetivo inicial, limitado al trayecto de prueba: + +- Sensibilidad >= 90 % para cruces definidos. +- <= 1 falso positivo por hora en reposo. +- Latencia mediana <= 1 segundo. + +Si RSSI no alcanza esos objetivos después de ajustar geometría y detector, se documentará el resultado negativo y se avanzará a CSI sin sobreadaptar el algoritmo. + +## Fase 5 — CSI con ESP32-C3 + +### Fase 5A — Un ESP32-C3 y un router + +- Integrar `esp-csi` o `esp-radar` como fuente alternativa. +- Generar tráfico controlado para obtener muestras CSI de forma estable. +- Mantener la interfaz común de salida: timestamp, score, threshold, state y calidad de muestra. +- Extraer características simples antes de introducir machine learning: amplitud, varianza por subportadora, energía temporal y componentes principales. +- Repetir exactamente el protocolo experimental de la fase RSSI. -## Fase 5 — CSI opcional +### Fase 5B — Dos ESP32-C3 -- Evaluar CSI con uno o dos ESP32-C3. -- Mantener la misma interfaz del detector y sustituir únicamente la fuente de características. -- Comparativa RSSI frente a CSI con el mismo protocolo experimental. +- Un nodo transmisor y otro receptor. +- Control explícito de canal, tasa de paquetes, potencia y geometría. +- Sincronización y etiquetado de sesiones. +- Comparación directa con un único receptor y con RSSI. + +Criterio de salida: informe reproducible RSSI frente a CSI con igual ubicación, eventos, métricas y criterios de evaluación. + +## Fase 6 — Integraciones y operación + +Solo se abordará después de disponer de un detector validado: + +- MQTT y HTTP webhook. +- Home Assistant mediante MQTT Discovery. +- OTA con rollback. +- Watchdog. +- Métricas de salud: uptime, reconexiones, heap, canal, BSSID y calidad de señal. +- Eventos con cooldown y deduplicación. + +## Alcance explícitamente excluido del POC RSSI + +La primera versión RSSI no se presentará como solución fiable para: + +- Detectar una persona completamente inmóvil. +- Cubrir uniformemente una habitación completa. +- Contar, identificar o localizar personas. +- Clasificar actividades. +- Uso como alarma de seguridad certificada. + +## Referencias técnicas + +- Kosba et al., **RASID: A Robust WLAN Device-Free Passive Motion Detection System**: https://arxiv.org/abs/1105.6084 +- **Rethinking RSSI for WiFi Sensing / WiRSSI**: https://www.nature.com/articles/s44459-026-00053-y +- Xiao et al., **FIMD: Fine-Grained Device-Free Motion Detection**: https://doi.org/10.1109/ICPADS.2012.49 +- **PhaseMode: A Robust Passive Intrusion Detection System**: https://doi.org/10.1155/2018/8243905 +- Strohmayer and Kampel, **WiFi CSI-Based Long-Range Through-Wall Human Activity Recognition with the ESP32**: https://repositum.tuwien.at/handle/20.500.12708/190653 +- **From CSI to Coordinates: Device-Free Indoor Localization with ESP32 Nodes**: https://www.mdpi.com/1999-5903/17/9/395 +- Espressif, **ESP-CSI**: https://github.com/espressif/esp-csi +- Espressif, **ESP-CSI Solution / ESP-Radar**: https://docs.espressif.com/projects/esp-techpedia/en/latest/esp-friends/solution-introduction/esp-csi/esp-csi-solution.html From 518fc1c42abc572d92e71e4ecbd5bc9a6da5662a Mon Sep 17 00:00:00 2001 From: soyunomas Date: Sun, 19 Jul 2026 04:18:09 +0100 Subject: [PATCH 7/9] docs: add alternative signal-processing research plan --- .../PLAN_ALTERNATIVO_PROCESADO_SENAL.md | 467 ++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 WiFi-Motion-RSSI-C3-Supermini/PLAN_ALTERNATIVO_PROCESADO_SENAL.md diff --git a/WiFi-Motion-RSSI-C3-Supermini/PLAN_ALTERNATIVO_PROCESADO_SENAL.md b/WiFi-Motion-RSSI-C3-Supermini/PLAN_ALTERNATIVO_PROCESADO_SENAL.md new file mode 100644 index 0000000..8f4dcb3 --- /dev/null +++ b/WiFi-Motion-RSSI-C3-Supermini/PLAN_ALTERNATIVO_PROCESADO_SENAL.md @@ -0,0 +1,467 @@ +# Plan alternativo — Procesamiento matemático de señal RSSI + +## Propósito + +Este plan investiga si una transformación matemática más rica del RSSI escalar puede superar al detector base de media de diferencias absolutas, calibración estadística e histéresis. + +No sustituye al plan principal. Se ejecutará como una comparación A/B sobre las mismas capturas, con idénticas particiones de entrenamiento y evaluación. + +## Hipótesis + +El detector inicial resume cada ventana con una única medida de variación. Puede perder información contenida en: + +- cambios de curvatura y pendiente; +- perturbaciones coherentes en varias escalas temporales; +- pequeños cambios persistentes que no superan un umbral instantáneo; +- variaciones de energía local; +- cambios en la organización temporal con media y varianza similares; +- desviaciones respecto al subespacio dinámico de la habitación vacía. + +La hipótesis principal es que una combinación de curvatura multiescala, energía no lineal y acumulación secuencial detectará transitorios más débiles y reducirá falsas alarmas frente al detector inicial. + +La presencia estática solo se considerará demostrada si las características de una persona quieta se separan de las capturas vacías en sesiones y días distintos. Recordar una entrada no cuenta como evidencia física de permanencia. + +## Resultado esperado frente al plan inicial + +Se espera una mejora probable en: + +- detección de entradas, salidas y cruces débiles; +- latencia de detección; +- robustez frente a impulsos aislados; +- adaptación a diferentes escalas temporales; +- explicación del motivo de cada alarma; +- capacidad para decidir objetivamente si el RSSI escalar contiene información suficiente. + +No se presupone una mejora en presencia completamente inmóvil. Esa capacidad queda sujeta a evidencia experimental. + +## Detector de referencia + +El detector principal se congela como baseline: + +- ventana de RSSI; +- media de diferencias absolutas consecutivas; +- calibración en reposo; +- umbral adaptativo; +- confirmación e histéresis. + +Ningún algoritmo alternativo se considerará mejor si no supera este baseline en sesiones de evaluación no utilizadas para ajustar parámetros. + +--- + +## Fase A0 — Contrato experimental + +### Objetivos + +- Definir escenarios, etiquetas, métricas y particiones antes de modificar algoritmos. +- Evitar ajustar y evaluar con las mismas capturas. +- Establecer límites de recursos del ESP32-C3. + +### Escenarios mínimos + +1. Habitación vacía en varias horas y días. +2. Entrada y salida cruzando el enlace. +3. Movimiento lateral, cerca y lejos del enlace. +4. Persona quieta durante al menos diez minutos. +5. Puertas, ventilador, tráfico Wi-Fi y objetos en movimiento. +6. Cambios normales del punto de acceso y reconexiones. + +### Métricas + +- precisión; +- recall; +- F1; +- falsos positivos por hora; +- latencia p50, p95 y máxima; +- tiempo de liberación; +- estabilidad entre sesiones; +- CPU, RAM y tiempo por muestra. + +### Criterio de salida + +Protocolo versionado y un baseline reproducible con parámetros congelados. + +--- + +## Fase A1 — Auditoría de la señal disponible + +### Objetivos + +Determinar qué información entrega realmente `esp_wifi_sta_get_ap_info()` y a qué frecuencia efectiva cambia. + +### Medidas + +- intervalo real entre lecturas; +- intervalo entre cambios de valor RSSI; +- porcentaje de muestras repetidas; +- número de niveles RSSI distintos por minuto; +- autocorrelación; +- distribución de primeras y segundas diferencias; +- deriva térmica y temporal; +- Allan deviation por escala; +- respuesta ante cambios de tráfico generado. + +### Decisiones + +- Seleccionar frecuencia de consulta sin contar lecturas repetidas como observaciones independientes. +- Identificar las escalas temporales donde domina ruido, deriva o perturbación ambiental. +- Definir un preprocesamiento de valores repetidos y muestras perdidas. + +### Criterio de salida + +Informe que determine la frecuencia informativa efectiva y las escalas candidatas. + +### Criterio de abandono + +Si el RSSI permanece prácticamente constante durante perturbaciones etiquetadas, no se invertirá esfuerzo en operadores más complejos y se adelantará CSI. + +--- + +## Fase A2 — Detector de curvatura multiescala + +### Objetivo + +Trasladar al dominio temporal la idea de detección de bordes mediante derivadas suavizadas. + +### Operadores + +Para escalas `s` configurables: + +```text +D1_s[n] = (x[n+s] - x[n-s]) / (2s) +D2_s[n] = (x[n+s] - 2x[n] + x[n-s]) / s² +``` + +Se compararán: + +- diferencias directas; +- derivadas Savitzky-Golay; +- primera derivada de gaussiana; +- segunda derivada de gaussiana o wavelet Mexican Hat; +- máximos persistentes entre escalas. + +Cada salida se normalizará mediante mediana y desviación absoluta mediana del reposo: + +```text +z = (feature - median) / (1.4826 * MAD + epsilon) +``` + +### Pruebas + +- escalas de 100 ms a varios segundos; +- ruido sintético y cuantización entera; +- impulsos aislados; +- rampas, escalones y cruces reales; +- persistencia de máximos entre escalas. + +### Criterio de salida + +Mejorar el recall o la latencia del baseline sin aumentar más de un 10 % los falsos positivos por hora. + +--- + +## Fase A3 — Energía local y detección secuencial + +### Objetivo + +Detectar oscilaciones débiles y acumular cambios pequeños persistentes. + +### Características + +- operador de energía Teager-Kaiser sobre señal centrada; +- razón de varianza corta/larga; +- energía de primera y segunda derivada; +- rango y MAD robusta por ventana. + +Operador Teager-Kaiser: + +```text +Psi[n] = z[n]² - z[n-1] * z[n+1] +``` + +El baseline `z[n]` solo se actualizará durante estados considerados de reposo. + +### Detectores secuenciales + +- CUSUM positivo y negativo; +- CUSUM de cambio de varianza; +- EWMA como referencia simple; +- detector GLR offline para comparar límites. + +### Criterio de salida + +Detectar cambios pequeños acumulados que el baseline no detecte, manteniendo `<= 1` falso positivo por hora en el perfil equilibrado. + +--- + +## Fase A4 — Estructura temporal no lineal + +### Objetivo + +Comprobar si existen diferencias de dinámica que no aparezcan en media, dispersión o derivadas. + +### Métodos offline + +- entropía de permutación con tratamiento explícito de empates; +- probabilidades de patrones ordinales de longitud 3 y 4; +- tasa de cambios de dirección; +- irreversibilidad temporal; +- curtosis y curtosis espectral; +- sample entropy solo si la longitud y cuantización lo permiten. + +### Restricción + +Estas características no se llevarán al firmware hasta demostrar ganancia estable entre sesiones. + +### Criterio de salida + +Seleccionar únicamente características cuya separación se mantenga en días y posiciones diferentes. + +--- + +## Fase A5 — Modelo de habitación vacía mediante subespacios + +### Objetivo + +Determinar si una ventana con perturbación o persona quieta deja de pertenecer a la dinámica habitual del entorno vacío. + +### Método principal + +- construir matrices de Hankel con ventanas RSSI; +- aplicar Singular Spectrum Analysis o SVD; +- estimar un subespacio de baja dimensión con capturas vacías; +- medir la energía residual de cada nueva ventana: + +```text +R(y) = ||y - U_r * U_r^T * y||² +``` + +### Métodos de contraste + +- PCA sobre vectores de características; +- Matrix Profile para descubrir subsecuencias discordantes; +- distancia Mahalanobis robusta; +- Isolation Forest únicamente como referencia offline. + +### Prueba crítica de presencia estática + +Comparar exclusivamente: + +- habitación vacía en sesiones no vistas; +- persona quieta después de los primeros 30 segundos; +- persona quieta después de cinco minutos. + +Los transitorios de entrada y salida se excluirán de esta evaluación. + +### Criterio de éxito + +Separación repetible entre vacío y quieto con F1 `>= 0.80` en días no vistos. + +### Criterio de abandono + +Si no supera F1 `0.65` entre sesiones, se concluirá que el RSSI escalar no ofrece evidencia estable de presencia quieta para esa geometría. + +--- + +## Fase A6 — Selección y fusión de características + +### Objetivo + +Construir el detector alternativo mínimo que aporte una mejora real. + +### Proceso + +1. Normalizar todas las características con estadísticas robustas. +2. Medir información mutua y redundancia. +3. Ejecutar ablaciones: retirar una característica cada vez. +4. Comparar suma ponderada, regresión logística y modelo de dos estados. +5. Rechazar cualquier característica cuya mejora no se reproduzca. + +### Vector candidato + +```text +[curvatura_multiescala, + energía_Teager, + razón_varianza, + CUSUM, + residuo_subespacio] +``` + +### Reglas + +- No usar una red neuronal en esta fase. +- No optimizar pesos con el conjunto de evaluación. +- Mantener una versión explicable con umbrales visibles. + +### Criterio de salida + +Detector alternativo con como máximo cinco características y mejora estadísticamente consistente frente al baseline. + +--- + +## Fase A7 — Implementación ESP32-C3 + +### Arquitectura + +Crear un frontend de señal intercambiable: + +```text +RSSI source + -> preprocessing + -> feature bank + -> sequential detector + -> occupancy state + -> telemetry +``` + +### Configuración + +- selección de detector `baseline` o `advanced`; +- escalas temporales; +- longitud Savitzky-Golay; +- activación de Teager-Kaiser; +- parámetros CUSUM; +- política de actualización del baseline; +- pesos de fusión; +- perfiles `low`, `balanced` y `high`. + +### Límites iniciales + +- memoria estática; +- sin asignaciones dinámicas en el bucle de muestreo; +- tiempo de procesamiento menor al 20 % del intervalo de muestra; +- telemetría de tiempos y desbordamientos; +- posibilidad de compilar cada característica de forma opcional. + +### Pruebas + +- vectores dorados generados en Python; +- igualdad numérica dentro de tolerancia entre Python y C; +- fuzzing de configuración; +- ASan/UBSan en host; +- sesiones largas sobre hardware. + +### Criterio de salida + +Paridad con el prototipo Python y cero pérdidas sostenidas de muestras. + +--- + +## Fase A8 — Inferencia de estado y presencia + +### Objetivo + +Separar claramente la detección física de eventos de la memoria lógica de ocupación. + +### Estados + +```text +VACIO -> ENTRADA_PROBABLE -> OCUPADO -> SALIDA_PROBABLE -> VACIO +``` + +### Evidencias + +- transitorios multiescala; +- evidencia secuencial acumulada; +- residuo respecto al subespacio vacío; +- tiempo desde el último evento; +- confianza independiente para movimiento y ocupación. + +### Salidas + +```text +motion_probability +occupancy_probability +physical_evidence_age_ms +last_transition +``` + +### Regla de honestidad + +Cuando solo exista memoria de una entrada, el estado debe exponerse como `ocupación inferida`, no como `presencia físicamente observada`. + +### Criterio de salida + +Transiciones consistentes y ausencia de confusión entre evento detectado y presencia medida. + +--- + +## Fase A9 — Comparación A/B final + +### Detectores + +- `B0`: detector inicial; +- `B1`: mejor estadística simple por ventana; +- `A1`: curvatura multiescala; +- `A2`: curvatura + Teager + CUSUM; +- `A3`: fusión final; +- `A4`: fusión con residuo SSA, si es viable. + +### Evaluación + +- validación por sesión y por día; +- bootstrap de intervalos de confianza; +- curvas precision-recall; +- comparación a igual tasa de falsos positivos; +- análisis de errores por escenario; +- costes de CPU, RAM y energía. + +### Condición para adoptar el plan alternativo + +El detector avanzado debe cumplir simultáneamente: + +- mejora absoluta de F1 de al menos `0.05` frente al baseline; +- o reducción de al menos `30 %` de falsos positivos a igual recall; +- latencia p95 no superior al baseline en más de `250 ms`; +- funcionamiento estable en sesiones no vistas; +- coste compatible con ESP32-C3. + +### Condición para conservar el detector inicial + +Si la mejora es menor o depende de una sola sesión, se mantendrá el baseline por simplicidad. + +--- + +## Fase A10 — Decisión RSSI o CSI + +### Continuar con RSSI cuando + +- el detector avanzado cumpla los criterios A/B; +- la geometría objetivo sea fija; +- la presencia estática no sea un requisito duro; +- la tasa de falsas alarmas sea aceptable. + +### Migrar a CSI cuando + +- el RSSI no cambie con perturbaciones relevantes; +- vacío y persona quieta no sean separables entre sesiones; +- el resultado dependa excesivamente del día o del tráfico; +- se necesite cobertura uniforme, localización o actividad fina. + +El trabajo matemático no se pierde al migrar: derivadas multiescala, CUSUM, entropías y análisis de subespacios pueden reutilizarse sobre amplitudes y fases CSI, donde existe una observación de mayor dimensión. + +--- + +## Orden recomendado de implementación + +1. Auditoría de señal y dataset. +2. Segunda derivada multiescala. +3. Normalización robusta. +4. CUSUM de curvatura y varianza. +5. Teager-Kaiser. +6. Evaluación A/B inicial. +7. SSA y Matrix Profile offline. +8. Fusión mínima. +9. Portado a ESP32-C3. +10. Decisión RSSI/CSI. + +## Riesgos + +- El RSSI puede actualizarse a una frecuencia menor que la frecuencia de consulta. +- La cuantización puede dominar derivadas y patrones ordinales. +- Un algoritmo complejo puede ajustarse a deriva o tráfico y no a la presencia física. +- La persona quieta puede no dejar una firma observable en RSSI escalar. +- Una mejora offline puede no compensar el coste embebido. + +## Conclusión + +Este plan tiene una probabilidad razonable de superar al detector inicial para movimiento y cambios débiles. Su mayor valor adicional es convertir la pregunta sobre presencia estática en una prueba falsable: o encontramos una firma reproducible entre sesiones, o demostramos que el RSSI escalar es insuficiente y migramos a CSI sin seguir ajustando umbrales indefinidamente. From 5c6c5a84a246a0d887c658d8a2c196b1e1fc816e Mon Sep 17 00:00:00 2001 From: soyunomas Date: Sun, 19 Jul 2026 04:19:05 +0100 Subject: [PATCH 8/9] docs: link alternative signal-processing plan --- WiFi-Motion-RSSI-C3-Supermini/PLAN.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WiFi-Motion-RSSI-C3-Supermini/PLAN.md b/WiFi-Motion-RSSI-C3-Supermini/PLAN.md index 57fda8b..07f2a3a 100644 --- a/WiFi-Motion-RSSI-C3-Supermini/PLAN.md +++ b/WiFi-Motion-RSSI-C3-Supermini/PLAN.md @@ -17,6 +17,10 @@ Esta decisión se apoya en los siguientes resultados: Por tanto, RSSI será un baseline medido y documentado, no el objetivo final de prestaciones. +## Línea alternativa de investigación matemática + +El archivo [PLAN_ALTERNATIVO_PROCESADO_SENAL.md](PLAN_ALTERNATIVO_PROCESADO_SENAL.md) define una comparación A/B frente al detector inicial mediante derivadas multiescala, Teager-Kaiser, CUSUM, entropía ordinal, análisis de subespacios y Matrix Profile. Esta línea solo se adoptará si demuestra una mejora reproducible en sesiones no vistas y dentro de los límites del ESP32-C3. + ## Fase 0 — Base reproducible (completada) - ESP-IDF fijado a `v6.0.2`, objetivo `esp32c3`. From ce2f6010f1ad3af4a7290937189c5dae909b63a7 Mon Sep 17 00:00:00 2001 From: soyunomas Date: Tue, 21 Jul 2026 00:03:01 +0100 Subject: [PATCH 9/9] Add files via upload --- WiFi-Motion-RSSI-C3-Supermini/README.md | 66 ------------------------- 1 file changed, 66 deletions(-) diff --git a/WiFi-Motion-RSSI-C3-Supermini/README.md b/WiFi-Motion-RSSI-C3-Supermini/README.md index 77bb753..e69de29 100644 --- a/WiFi-Motion-RSSI-C3-Supermini/README.md +++ b/WiFi-Motion-RSSI-C3-Supermini/README.md @@ -1,66 +0,0 @@ -# WiFi Motion RSSI — ESP32-C3 SuperMini - -Detector experimental de movimiento basado en las variaciones del RSSI del punto de acceso al que está conectado un ESP32-C3. No identifica personas ni detecta de forma fiable a alguien completamente inmóvil. - -## Estado - -Primera implementación funcional: - -- ESP-IDF `v6.0.2`, objetivo `esp32c3`. -- Muestreo de RSSI mediante `esp_wifi_sta_get_ap_info()`. -- Detector con calibración estadística, umbral adaptativo e histéresis. -- Salida CSV o JSON Lines por USB Serial/JTAG. -- Configuración con `idf.py menuconfig` y backend NVS versionado. -- Pruebas host y CI. - -Consulta [PLAN.md](PLAN.md) para las fases y criterios de aceptación. - -## Configuración rápida - -```bash -./tools/bootstrap.sh -source "$HOME/esp/esp-idf-v6.0.2/export.sh" -idf.py set-target esp32c3 -idf.py menuconfig -./tools/build.sh -PORT=/dev/ttyACM0 ./tools/flash.sh -``` - -En `menuconfig`, abre **WiFi motion detector** y configura al menos el SSID y la contraseña. - -## Parámetros - -| Parámetro | Predeterminado | Función | -|---|---:|---| -| Intervalo | 100 ms | Frecuencia de muestreo RSSI | -| Ventana | 20 muestras | Duración aproximada del score | -| Calibración | 120 scores | Baseline inicial en reposo | -| Multiplicador sigma | 6.0 | Sensibilidad adaptativa | -| Umbral mínimo | 0.30 dB/muestra | Evita umbrales degenerados | -| Disparo | 3 scores | Confirmación de movimiento | -| Liberación | 8 scores | Histéresis al volver a reposo | - -Durante la calibración, no debe moverse nadie entre el punto de acceso y el ESP32. - -## Formato CSV - -```text -t_ms,rssi_dbm,score,threshold,state,calibrated -12540,-61,0.4211,0.7832,idle,1 -12640,-54,1.2105,0.7832,motion,1 -``` - -## Pruebas host - -```bash -./tools/test-host.sh -``` - -Las pruebas validan configuración, calibración estable, activación por una secuencia perturbada, histéresis, umbral mínimo y una sesión sintética larga sin falsos positivos. - -## Limitaciones iniciales - -- El SSID y la contraseña se configuran por `menuconfig` en esta fase. El portal y la edición NVS llegan en fase 2. -- La detección depende de la geometría, multitrayecto, interferencias y tráfico del AP. -- No es un sistema de seguridad ni un detector certificado de ocupación. -- El GPIO 8 se usa como LED activo-bajo por defecto; se puede desactivar o cambiar en `menuconfig`.