mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-28 14:09:33 +00:00
* docs: ADR-063 mmWave sensor fusion with WiFi CSI 60 GHz mmWave radar (Seeed MR60BHA2, HLK-LD2410/LD2450) fusion with WiFi CSI for dual-confirm fall detection, clinical-grade vitals, and self-calibrating CSI pipeline. Covers auto-detection, 6 supported sensors, Kalman fusion, extended 48-byte vitals packet, RuVector/RuvSense integration points, and 6-phase implementation plan. Based on live hardware capture from ESP32-C6 + MR60BHA2 on COM4. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(firmware): ADR-063 mmWave sensor fusion — full implementation Phase 1-2 of ADR-063: mmwave_sensor.c/h: - MR60BHA2 UART parser (60 GHz: HR, BR, presence, distance) - LD2410 UART parser (24 GHz: presence, distance) - Auto-detection: probes UART for known frame headers at boot - Mock generator for QEMU testing (synthetic HR 72±2, BR 16±1) - Capability flag registration per sensor type edge_processing.c/h: - 48-byte fused vitals packet (magic 0xC5110004) - Kalman-style fusion: mmWave 80% + CSI 20% when both available - Automatic fallback to CSI-only 32-byte packet when no mmWave - Dual presence flag (Bit3 = mmwave_present) main.c: - mmwave_sensor_init() called at boot with auto-detect - Status logged in startup banner Fuzz stubs updated for mmwave_sensor API. Build verified: QEMU mock build passes. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(firmware): correct MR60BHA2 + LD2410 UART protocols (ADR-063) MR60BHA2: SOF=0x01 (not 0x5359), XOR+NOT checksums on header and data, frame types 0x0A14 (BR), 0x0A15 (HR), 0x0A16 (distance), 0x0F09 (presence). Based on Seeed Arduino library research. LD2410: 256000 baud (not 115200), 0xAA report head marker, target state byte at offset 2 (after data_type + head_marker). Auto-detect: probes MR60 at 115200 first, then LD2410 at 256000. Sets final baud rate after detection. Co-Authored-By: claude-flow <ruv@ruv.net> * feat: ADR-063 Phase 6 server-side mmWave + CSI fusion bridge Python script reads both serial ports simultaneously: - COM4 (ESP32-C6 + MR60BHA2): parses ESPHome debug output for HR, BR, presence, distance - COM7 (ESP32-S3): reads CSI edge processing frames Kalman-style fusion: mmWave 80% + CSI 20% for vitals, OR gate for presence. Verified on real hardware: mmWave HR=75bpm, BR=25/min at 52cm range, CSI frames flowing concurrently. Both sensors live for 30 seconds. Co-Authored-By: claude-flow <ruv@ruv.net> * docs: ADR-064 multimodal ambient intelligence roadmap 25+ applications across 4 tiers from practical to exotic: - Tier 1 (build now): zero-FP fall detection, sleep monitoring, occupancy HVAC, baby breathing, bathroom safety - Tier 2 (research): gait analysis, stress detection, gesture control, respiratory screening, multi-room activity - Tier 3 (frontier): cardiac arrhythmia, RF tomography, sign language, cognitive load, swarm sensing - Tier 4 (exotic): emotion contagion, lucid dreaming, plant monitoring, pet behavior Priority matrix with effort estimates. All P0-P1 items work with existing hardware (ESP32-S3 + MR60BHA2 + BH1750). Co-Authored-By: claude-flow <ruv@ruv.net> * fix(ci): add ESP_ERR_NOT_FOUND to fuzz stubs mmwave_sensor stub returns ESP_ERR_NOT_FOUND which wasn't defined in the minimal esp_stubs.h for host-based fuzz testing. Co-Authored-By: claude-flow <ruv@ruv.net>
207 lines
8.3 KiB
C
207 lines
8.3 KiB
C
/**
|
|
* @file edge_processing.h
|
|
* @brief ADR-039 Edge Intelligence — dual-core CSI processing pipeline.
|
|
*
|
|
* Core 0 (WiFi): Produces CSI frames into a lock-free SPSC ring buffer.
|
|
* Core 1 (DSP): Consumes frames, runs signal processing, extracts vitals.
|
|
*
|
|
* Features:
|
|
* - Biquad IIR bandpass filters for breathing (0.1-0.5 Hz) and heart rate (0.8-2.0 Hz)
|
|
* - Phase unwrapping and Welford running statistics
|
|
* - Top-K subcarrier selection by variance
|
|
* - Presence detection with adaptive threshold calibration
|
|
* - Vital signs: breathing rate, heart rate (zero-crossing BPM)
|
|
* - Fall detection (phase acceleration exceeds threshold)
|
|
* - Delta compression (XOR + RLE) for bandwidth reduction
|
|
* - Multi-person vitals via subcarrier group clustering
|
|
* - 32-byte vitals packet (magic 0xC5110002) for server-side parsing
|
|
*/
|
|
|
|
#ifndef EDGE_PROCESSING_H
|
|
#define EDGE_PROCESSING_H
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include "esp_err.h"
|
|
|
|
/* ---- Magic numbers ---- */
|
|
#define EDGE_VITALS_MAGIC 0xC5110002 /**< Vitals packet magic. */
|
|
#define EDGE_COMPRESSED_MAGIC 0xC5110003 /**< Compressed frame magic. */
|
|
|
|
/* ---- Buffer sizes ---- */
|
|
#define EDGE_RING_SLOTS 16 /**< SPSC ring buffer slots (power of 2). */
|
|
#define EDGE_MAX_IQ_BYTES 1024 /**< Max I/Q payload per slot. */
|
|
#define EDGE_PHASE_HISTORY_LEN 256 /**< Phase history buffer depth. */
|
|
#define EDGE_TOP_K 8 /**< Top-K subcarriers to track. */
|
|
#define EDGE_MAX_SUBCARRIERS 128 /**< Max subcarriers per frame. */
|
|
|
|
/* ---- Multi-person ---- */
|
|
#define EDGE_MAX_PERSONS 4 /**< Max simultaneous persons. */
|
|
|
|
/* ---- Calibration ---- */
|
|
#define EDGE_CALIB_FRAMES 1200 /**< Frames for adaptive calibration (~60s at 20 Hz). */
|
|
#define EDGE_CALIB_SIGMA_MULT 3.0f /**< Threshold = mean + 3*sigma of ambient. */
|
|
|
|
/* ---- Fall detection ---- */
|
|
#define EDGE_FALL_COOLDOWN_MS 5000 /**< Minimum ms between fall alerts (debounce). */
|
|
#define EDGE_FALL_CONSEC_MIN 3 /**< Consecutive frames above threshold to trigger. */
|
|
|
|
/* ---- SPSC ring buffer slot ---- */
|
|
typedef struct {
|
|
uint8_t iq_data[EDGE_MAX_IQ_BYTES]; /**< Raw I/Q bytes from CSI callback. */
|
|
uint16_t iq_len; /**< Actual I/Q data length. */
|
|
int8_t rssi; /**< RSSI from rx_ctrl. */
|
|
uint8_t channel; /**< WiFi channel. */
|
|
uint32_t timestamp_us; /**< Microsecond timestamp. */
|
|
} edge_ring_slot_t;
|
|
|
|
/* ---- SPSC ring buffer ---- */
|
|
typedef struct {
|
|
edge_ring_slot_t slots[EDGE_RING_SLOTS];
|
|
volatile uint32_t head; /**< Written by producer (Core 0). */
|
|
volatile uint32_t tail; /**< Written by consumer (Core 1). */
|
|
} edge_ring_buf_t;
|
|
|
|
/* ---- Biquad IIR filter state ---- */
|
|
typedef struct {
|
|
float b0, b1, b2; /**< Numerator coefficients. */
|
|
float a1, a2; /**< Denominator coefficients (a0 = 1). */
|
|
float x1, x2; /**< Input delay line. */
|
|
float y1, y2; /**< Output delay line. */
|
|
} edge_biquad_t;
|
|
|
|
/* ---- Welford running statistics ---- */
|
|
typedef struct {
|
|
double mean;
|
|
double m2;
|
|
uint32_t count;
|
|
} edge_welford_t;
|
|
|
|
/* ---- Per-person vitals state (multi-person mode) ---- */
|
|
typedef struct {
|
|
float phase_history[EDGE_PHASE_HISTORY_LEN];
|
|
uint16_t history_len;
|
|
uint16_t history_idx;
|
|
float breathing_bpm;
|
|
float heartrate_bpm;
|
|
uint8_t subcarrier_idx; /**< Which subcarrier group this person tracks. */
|
|
bool active;
|
|
} edge_person_vitals_t;
|
|
|
|
/* ---- Vitals packet (32 bytes, wire format) ---- */
|
|
typedef struct __attribute__((packed)) {
|
|
uint32_t magic; /**< EDGE_VITALS_MAGIC = 0xC5110002. */
|
|
uint8_t node_id; /**< ESP32 node identifier. */
|
|
uint8_t flags; /**< Bit0=presence, Bit1=fall, Bit2=motion. */
|
|
uint16_t breathing_rate; /**< BPM * 100 (fixed-point). */
|
|
uint32_t heartrate; /**< BPM * 10000 (fixed-point). */
|
|
int8_t rssi; /**< Latest RSSI. */
|
|
uint8_t n_persons; /**< Number of detected persons (multi-person). */
|
|
uint8_t reserved[2];
|
|
float motion_energy; /**< Phase variance / motion metric. */
|
|
float presence_score; /**< Presence detection score. */
|
|
uint32_t timestamp_ms; /**< Milliseconds since boot. */
|
|
uint32_t reserved2; /**< Reserved for future use. */
|
|
} edge_vitals_pkt_t;
|
|
|
|
_Static_assert(sizeof(edge_vitals_pkt_t) == 32, "vitals packet must be 32 bytes");
|
|
|
|
/* ---- ADR-063: Fused vitals packet (48 bytes, wire format) ---- */
|
|
#define EDGE_FUSED_MAGIC 0xC5110004 /**< Fused vitals packet magic. */
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
/* First 32 bytes match edge_vitals_pkt_t layout */
|
|
uint32_t magic; /**< EDGE_FUSED_MAGIC = 0xC5110004. */
|
|
uint8_t node_id;
|
|
uint8_t flags; /**< Bit0=presence, Bit1=fall, Bit2=motion, Bit3=mmwave_present. */
|
|
uint16_t breathing_rate; /**< Fused BPM * 100 (CSI + mmWave Kalman). */
|
|
uint32_t heartrate; /**< Fused BPM * 10000. */
|
|
int8_t rssi;
|
|
uint8_t n_persons;
|
|
uint8_t mmwave_type; /**< mmwave_type_t enum. */
|
|
uint8_t fusion_confidence; /**< 0-100 fusion quality score. */
|
|
float motion_energy;
|
|
float presence_score;
|
|
uint32_t timestamp_ms;
|
|
/* mmWave extension (16 bytes) */
|
|
float mmwave_hr_bpm; /**< Raw mmWave heart rate. */
|
|
float mmwave_br_bpm; /**< Raw mmWave breathing rate. */
|
|
float mmwave_distance;/**< Distance to nearest target (cm). */
|
|
uint8_t mmwave_targets; /**< Target count from mmWave. */
|
|
uint8_t mmwave_confidence; /**< mmWave signal quality 0-100. */
|
|
uint16_t reserved3;
|
|
uint32_t reserved4; /**< Pad to 48 bytes for alignment. */
|
|
} edge_fused_vitals_pkt_t;
|
|
|
|
_Static_assert(sizeof(edge_fused_vitals_pkt_t) == 48, "fused vitals must be 48 bytes");
|
|
|
|
/* ---- Edge configuration (from NVS) ---- */
|
|
typedef struct {
|
|
uint8_t tier; /**< Processing tier: 0=raw, 1=basic, 2=full. */
|
|
float presence_thresh;/**< Presence detection threshold (0 = auto-calibrate). */
|
|
float fall_thresh; /**< Fall detection threshold (phase accel, rad/s^2). */
|
|
uint16_t vital_window; /**< Phase history window for BPM estimation. */
|
|
uint16_t vital_interval_ms; /**< Vitals packet send interval in ms. */
|
|
uint8_t top_k_count; /**< Number of top subcarriers to track. */
|
|
uint8_t power_duty; /**< Power duty cycle percentage (10-100). */
|
|
} edge_config_t;
|
|
|
|
/**
|
|
* Initialize the edge processing pipeline.
|
|
* Creates the SPSC ring buffer and starts the DSP task on Core 1.
|
|
*
|
|
* @param cfg Edge configuration (from NVS or defaults).
|
|
* @return ESP_OK on success.
|
|
*/
|
|
esp_err_t edge_processing_init(const edge_config_t *cfg);
|
|
|
|
/**
|
|
* Enqueue a CSI frame from the WiFi callback (Core 0).
|
|
* Lock-free SPSC push — safe to call from ISR context.
|
|
*
|
|
* @param iq_data Raw I/Q data from wifi_csi_info_t.buf.
|
|
* @param iq_len Length of I/Q data in bytes.
|
|
* @param rssi RSSI from rx_ctrl.
|
|
* @param channel WiFi channel number.
|
|
* @return true if enqueued, false if ring buffer is full (frame dropped).
|
|
*/
|
|
bool edge_enqueue_csi(const uint8_t *iq_data, uint16_t iq_len,
|
|
int8_t rssi, uint8_t channel);
|
|
|
|
/**
|
|
* Get the latest vitals packet (thread-safe copy).
|
|
*
|
|
* @param pkt Output vitals packet.
|
|
* @return true if valid vitals data is available.
|
|
*/
|
|
bool edge_get_vitals(edge_vitals_pkt_t *pkt);
|
|
|
|
/**
|
|
* Get multi-person vitals array.
|
|
*
|
|
* @param persons Output array (must be EDGE_MAX_PERSONS elements).
|
|
* @param n_active Output: number of active persons.
|
|
*/
|
|
void edge_get_multi_person(edge_person_vitals_t *persons, uint8_t *n_active);
|
|
|
|
/**
|
|
* Get pointer to the phase history ring buffer and its state.
|
|
* Used by WASM runtime (ADR-040) to expose phase history to modules.
|
|
*
|
|
* @param out_buf Output: pointer to phase history array.
|
|
* @param out_len Output: number of valid entries.
|
|
* @param out_idx Output: current write index.
|
|
*/
|
|
void edge_get_phase_history(const float **out_buf, uint16_t *out_len,
|
|
uint16_t *out_idx);
|
|
|
|
/**
|
|
* Get per-subcarrier Welford variance array.
|
|
* Used by WASM runtime (ADR-040) to expose variances to modules.
|
|
*
|
|
* @param out_variances Output array (must be EDGE_MAX_SUBCARRIERS elements).
|
|
* @param n_subcarriers Number of subcarriers to fill.
|
|
*/
|
|
void edge_get_variances(float *out_variances, uint16_t n_subcarriers);
|
|
|
|
#endif /* EDGE_PROCESSING_H */
|