Back to Ruview

Signal Intelligence Modules -- WiFi-DensePose Edge Intelligence

docs/edge-modules/signal-intelligence.md

0.7.021.1 KB
Original Source

Signal Intelligence Modules -- WiFi-DensePose Edge Intelligence

Real-time WiFi signal analysis and enhancement running directly on the ESP32 chip. These modules clean, compress, and extract features from raw WiFi channel data so that higher-level modules (health, security, etc.) get better input.

Overview

ModuleFileWhat It DoesEvent IDsBudget
Flash Attentionsig_flash_attention.rsFocuses processing on the most informative subcarrier groups700-702S (<5ms)
Coherence Gatesig_coherence_gate.rsFilters out noisy/corrupted CSI frames using phase coherence710-712L (<2ms)
Temporal Compresssig_temporal_compress.rsStores CSI history in 3-tier compressed circular buffer705-707S (<5ms)
Sparse Recoverysig_sparse_recovery.rsRecovers dropped subcarriers using ISTA sparse optimization715-717H (<10ms)
Min-Cut Person Matchsig_mincut_person_match.rsMaintains stable person IDs across frames using bipartite matching720-722H (<10ms)
Optimal Transportsig_optimal_transport.rsDetects subtle motion via sliced Wasserstein distance725-727S (<5ms)

How Signal Processing Fits In

The signal intelligence modules form a processing pipeline between raw CSI data and application-level modules:

  Raw CSI from WiFi chipset (Tier 0-2 firmware DSP)
       |
       v
  +---------------------+     +---------------------+
  | Coherence Gate       | --> | Sparse Recovery      |
  | Reject noisy frames, |     | Fill in dropped      |
  | gate quality levels  |     | subcarriers via ISTA  |
  +---------------------+     +---------------------+
       |                              |
       v                              v
  +---------------------+     +---------------------+
  | Flash Attention      |     | Temporal Compress    |
  | Focus on informative |     | Store CSI history    |
  | subcarrier groups    |     | at 3 quality tiers   |
  +---------------------+     +---------------------+
       |                              |
       v                              v
  +---------------------+     +---------------------+
  | Min-Cut Person Match |     | Optimal Transport    |
  | Track person IDs     |     | Detect subtle motion |
  | across frames        |     | via distribution     |
  +---------------------+     +---------------------+
       |                              |
       v                              v
  Application modules: Health, Security, Smart Building, etc.

The Coherence Gate acts as a quality filter at the top of the pipeline. Frames that pass the gate feed into the Sparse Recovery module (if subcarrier dropout is detected) and then into downstream analysis. Flash Attention identifies which spatial regions carry the most signal, while Temporal Compress maintains an efficient rolling history. Min-Cut Person Match and Optimal Transport extract higher-level features (person identity and motion) that application modules consume.

Shared Utilities (vendor_common.rs)

All signal intelligence modules share these utilities from vendor_common.rs:

UtilityPurpose
CircularBuffer<N>Fixed-size ring buffer for phase history, stack-allocated
EmaExponential moving average with configurable alpha
WelfordStatsOnline mean/variance/stddev in O(1) memory
dot_product, l2_norm, cosine_similarityFixed-size vector math
dtw_distance, dtw_distance_bandedDynamic Time Warping for gesture/pattern matching
FixedPriorityQueue<CAP>Top-K selection without heap allocation

Modules

Flash Attention (sig_flash_attention.rs)

What it does: Focuses processing on the WiFi channels that carry the most useful information -- ignores noise. Divides 32 subcarriers into 8 groups and computes attention weights showing where signal activity is concentrated.

Algorithm: Tiled attention (Q*K/sqrt(d)) over 8 subcarrier groups with softmax normalization and Shannon entropy tracking.

  1. Compute group means: Q = current phase per group, K = previous phase per group, V = amplitude per group
  2. Score each group: score[g] = Q[g] * K[g] / sqrt(8)
  3. Softmax normalization (numerically stable: subtract max before exp)
  4. Track entropy H = -sum(p * ln(p)) via EMA smoothing

Low entropy means activity is focused in one spatial zone (a Fresnel region); high entropy means activity is spread uniformly.

Public API

rust
pub struct FlashAttention { /* ... */ }

impl FlashAttention {
    pub const fn new() -> Self;
    pub fn process_frame(&mut self, phases: &[f32], amplitudes: &[f32]) -> &[(i32, f32)];
    pub fn weights() -> &[f32; 8];       // Current attention weights per group
    pub fn entropy() -> f32;             // EMA-smoothed entropy [0, ln(8)]
    pub fn peak_group() -> usize;        // Group index with highest weight
    pub fn centroid() -> f32;            // Weighted centroid position [0, 7]
    pub fn frame_count() -> u32;
    pub fn reset(&mut self);
}

Events

IDNameValueMeaning
700ATTENTION_PEAK_SCGroup index (0-7)Which subcarrier group has the strongest attention weight
701ATTENTION_SPREADEntropy (0 to ~2.08)How spread out the attention is (low = focused, high = uniform)
702SPATIAL_FOCUS_ZONECentroid (0.0-7.0)Weighted center of attention across groups

Configuration

ConstantValuePurpose
N_GROUPS8Number of subcarrier groups (tiles)
MAX_SC32Maximum subcarriers processed
ENTROPY_ALPHA0.15EMA smoothing factor for entropy

Tutorial: Understanding Attention Weights

The 8 attention weights sum to 1.0. When a person stands in a particular area of the room, the WiFi signal changes most in the subcarrier group(s) whose Fresnel zones intersect that area.

  • All weights near 0.125 (= 1/8): Uniform attention. No localized activity -- either an empty room or whole-body motion affecting all subcarriers equally.
  • One weight near 1.0, others near 0.0: Highly focused. Activity concentrated in one spatial zone. The peak_group index tells you which zone.
  • Two adjacent groups elevated: Activity at the boundary between two spatial zones, or a person moving between them.
  • Entropy below 1.0: Strong spatial focus. Good for zone-level localization.
  • Entropy above 1.8: Nearly uniform. Hard to localize activity.

The centroid value (0.0 to 7.0) gives a weighted average position. Tracking centroid over time reveals motion direction across the room.


Coherence Gate (sig_coherence_gate.rs)

What it does: Decides whether each incoming CSI frame is trustworthy enough to use for sensing, or should be discarded. Uses the statistical consistency of phase changes across subcarriers to measure signal quality.

Algorithm: Per-subcarrier phase deltas form unit phasors (cos + i*sin). The magnitude of the mean phasor is the coherence score [0,1]. Welford online statistics track mean/variance for Z-score computation. A hysteresis state machine prevents rapid oscillation between states.

State transitions:

  • Accept -> PredictOnly: 5 consecutive frames below LOW_THRESHOLD (0.40)
  • PredictOnly -> Reject: single frame below threshold
  • Reject/PredictOnly -> Accept: 10 consecutive frames above HIGH_THRESHOLD (0.75)
  • Any -> Recalibrate: running variance exceeds 4x the initial snapshot

Public API

rust
pub struct CoherenceGate { /* ... */ }

impl CoherenceGate {
    pub const fn new() -> Self;
    pub fn process_frame(&mut self, phases: &[f32]) -> &[(i32, f32)];
    pub fn gate() -> GateDecision;       // Accept/PredictOnly/Reject/Recalibrate
    pub fn coherence() -> f32;           // Last coherence score [0, 1]
    pub fn zscore() -> f32;              // Z-score of last coherence
    pub fn variance() -> f32;            // Running variance of coherence
    pub fn frame_count() -> u32;
    pub fn reset(&mut self);
}

pub enum GateDecision { Accept, PredictOnly, Reject, Recalibrate }

Events

IDNameValueMeaning
710GATE_DECISION2/1/0/-1Accept(2), PredictOnly(1), Reject(0), Recalibrate(-1)
711COHERENCE_SCORE[0.0, 1.0]Phase phasor coherence magnitude
712RECALIBRATE_NEEDEDVarianceEnvironment has changed significantly -- retrain baseline

Configuration

ConstantValuePurpose
HIGH_THRESHOLD0.75Coherence above this = good quality
LOW_THRESHOLD0.40Coherence below this = poor quality
DEGRADE_COUNT5Consecutive bad frames before degrading
RECOVER_COUNT10Consecutive good frames before recovering
VARIANCE_DRIFT_MULT4.0Variance multiplier triggering recalibrate

Tutorial: Using the Coherence Gate

The coherence gate protects downstream modules from processing garbage data. In practice:

  1. Accept (value=2): Frame is clean. Use it for all sensing tasks (vitals, presence, gestures).
  2. PredictOnly (value=1): Frame quality is marginal. Use cached predictions from previous frames; do not update models.
  3. Reject (value=0): Frame is too noisy. Skip entirely. Do not feed to any learning module.
  4. Recalibrate (value=-1): The environment has changed fundamentally (furniture moved, new AP, door opened). Reset baselines and re-learn.

Common causes of low coherence:

  • Microwave oven running (2.4 GHz interference)
  • Multiple people walking in different directions (phase cancellation)
  • Hardware glitch (intermittent antenna contact)

Temporal Compress (sig_temporal_compress.rs)

What it does: Maintains a rolling history of up to 512 CSI snapshots in compressed form. Recent data is stored at high precision; older data is progressively compressed to save memory while retaining long-term trends.

Algorithm: Three-tier quantization with automatic demotion at age boundaries.

TierAge RangeBitsQuantization LevelsMax Error
Hot0-63 (newest)8-bit256<0.5%
Warm64-2555-bit32<3%
Cold256-5113-bit8<15%

At 20 Hz, the buffer stores approximately:

  • Hot: 3.2 seconds of high-fidelity data
  • Warm: 9.6 seconds of medium-fidelity data
  • Cold: 12.8 seconds of low-fidelity data
  • Total: ~25.6 seconds, or longer at lower frame rates

Each snapshot stores 8 phase + 8 amplitude values (group means), plus a scale factor and tier tag.

Public API

rust
pub struct TemporalCompressor { /* ... */ }

impl TemporalCompressor {
    pub const fn new() -> Self;
    pub fn push_frame(&mut self, phases: &[f32], amps: &[f32], ts_ms: u32) -> &[(i32, f32)];
    pub fn on_timer() -> &[(i32, f32)];
    pub fn get_snapshot(age: usize) -> Option<[f32; 16]>;  // Decompressed 8 phase + 8 amp
    pub fn compression_ratio() -> f32;
    pub fn frame_rate() -> f32;
    pub fn total_written() -> u32;
    pub fn occupied() -> usize;
}

Events

IDNameValueMeaning
705COMPRESSION_RATIORatio (>1.0)Raw bytes / compressed bytes
706TIER_TRANSITIONTier (1 or 2)A snapshot was demoted to Warm(1) or Cold(2)
707HISTORY_DEPTH_HOURSHoursHow much wall-clock time the buffer covers

Configuration

ConstantValuePurpose
CAP512Total snapshot capacity
HOT_END64First N snapshots at 8-bit precision
WARM_END256Snapshots 64-255 at 5-bit precision
RATE_ALPHA0.05EMA alpha for frame rate estimation

Sparse Recovery (sig_sparse_recovery.rs)

What it does: When WiFi hardware drops some subcarrier measurements (nulls/zeros due to deep fades, firmware glitches, or multipath nulls), this module reconstructs the missing values using mathematical optimization.

Algorithm: Iterative Shrinkage-Thresholding Algorithm (ISTA) -- an L1-minimizing sparse recovery method.

x_{k+1} = soft_threshold(x_k + step * A^T * (b - A*x_k), lambda)

where:

  • A is a tridiagonal correlation model (diagonal + immediate neighbors, 96 f32s instead of full 32x32=1024)
  • b is the observed (non-null) subcarrier values
  • soft_threshold(x, t) = sign(x) * max(|x| - t, 0) promotes sparsity
  • Maximum 10 iterations per frame

The correlation model is learned online from valid frames using EMA-blended products.

Public API

rust
pub struct SparseRecovery { /* ... */ }

impl SparseRecovery {
    pub const fn new() -> Self;
    pub fn process_frame(&mut self, amplitudes: &mut [f32]) -> &[(i32, f32)];
    pub fn dropout_rate() -> f32;           // Fraction of null subcarriers
    pub fn last_residual_norm() -> f32;     // L2 residual from last recovery
    pub fn last_recovered_count() -> u32;   // How many subcarriers were recovered
    pub fn is_initialized() -> bool;        // Whether correlation model is ready
}

Note: process_frame modifies amplitudes in place -- null subcarriers are overwritten with recovered values.

Events

IDNameValueMeaning
715RECOVERY_COMPLETECountNumber of subcarriers recovered
716RECOVERY_ERRORL2 normResidual error of the recovery
717DROPOUT_RATEFraction [0,1]Fraction of null subcarriers (emitted every 20 frames)

Configuration

ConstantValuePurpose
NULL_THRESHOLD0.001Amplitude below this = dropped out
MIN_DROPOUT_RATE0.10Minimum dropout fraction to trigger recovery
MAX_ITERATIONS10ISTA iteration cap per frame
STEP_SIZE0.05Gradient descent learning rate
LAMBDA0.01L1 sparsity penalty weight
CORR_ALPHA0.05EMA alpha for correlation model updates

Tutorial: When Recovery Kicks In

  1. The module needs at least 10 fully valid frames to initialize the correlation model (is_initialized() == true).
  2. Recovery only triggers when dropout exceeds 10% (e.g., 4+ of 32 subcarriers are null).
  3. Below 10%, the nulls are too sparse to warrant recovery overhead.
  4. The tridiagonal correlation model exploits the fact that adjacent WiFi subcarriers are highly correlated. A null at subcarrier 15 can be estimated from subcarriers 14 and 16.
  5. Monitor RECOVERY_ERROR -- a rising residual suggests the correlation model is stale and the environment has changed.

Min-Cut Person Match (sig_mincut_person_match.rs)

What it does: Maintains stable identity labels for up to 4 people in the sensing area. When people move around, their WiFi signatures change position -- this module tracks which signature belongs to which person across consecutive frames.

Algorithm: Inspired by ruvector-mincut (DynamicPersonMatcher). Each frame:

  1. Feature extraction: For each detected person, extract the top-8 subcarrier variances (sorted descending) from their spatial region. This produces an 8D signature vector.
  2. Cost matrix: Compute L2 distances between all current features and all stored signatures.
  3. Greedy assignment: Pick the minimum-cost (detection, slot) pair, mark both as used, repeat. Like a simplified Hungarian algorithm, optimal for max 4 persons.
  4. Signature update: Blend new features into stored signatures via EMA (alpha=0.15).
  5. Timeout: Release slots after 100 frames of absence.

Public API

rust
pub struct PersonMatcher { /* ... */ }

impl PersonMatcher {
    pub const fn new() -> Self;
    pub fn process_frame(&mut self, amplitudes: &[f32], variances: &[f32], n_persons: usize) -> &[(i32, f32)];
    pub fn active_persons() -> u8;
    pub fn total_swaps() -> u32;
    pub fn is_person_stable(slot: usize) -> bool;
    pub fn person_signature(slot: usize) -> Option<&[f32; 8]>;
}

Events

IDNameValueMeaning
720PERSON_ID_ASSIGNEDperson_id + confidence*0.01Which slot was assigned (integer part) and match confidence (fractional part)
721PERSON_ID_SWAPprev*16 + currAn identity swap was detected (prev and curr slot indices encoded)
722MATCH_CONFIDENCE[0.0, 1.0]Average matching confidence across all detected persons (emitted every 10 frames)

Configuration

ConstantValuePurpose
MAX_PERSONS4Maximum simultaneous person tracks
FEAT_DIM8Signature vector dimension
SIG_ALPHA0.15EMA blending factor for signature updates
MAX_MATCH_DISTANCE5.0L2 distance threshold for valid match
STABLE_FRAMES10Frames before a track is considered stable
ABSENT_TIMEOUT100Frames of absence before slot release (~5s at 20Hz)

Optimal Transport (sig_optimal_transport.rs)

What it does: Detects subtle motion that traditional variance-based detectors miss. Computes how much the overall shape of the WiFi signal distribution changes between frames, even when the total power stays constant.

Algorithm: Sliced Wasserstein distance -- a computationally efficient approximation to the full Wasserstein (earth mover's) distance.

  1. Generate 4 fixed random projection directions (deterministic LCG PRNG, const-computed at compile time)
  2. Project both current and previous amplitude vectors onto each direction
  3. Sort the projected values (Shell sort with Ciura gaps, O(n^1.3))
  4. Compute 1D Wasserstein-1 distance between sorted projections (just mean absolute difference)
  5. Average across all 4 projections
  6. Smooth via EMA and compare against thresholds

Subtle motion detection: When the Wasserstein distance is elevated (distribution shape changed) but the variance is stable (total power unchanged), something moved without creating obvious disturbance -- e.g., slow hand motion, breathing, or a door slowly closing.

Public API

rust
pub struct OptimalTransportDetector { /* ... */ }

impl OptimalTransportDetector {
    pub const fn new() -> Self;
    pub fn process_frame(&mut self, amplitudes: &[f32]) -> &[(i32, f32)];
    pub fn distance() -> f32;            // EMA-smoothed Wasserstein distance
    pub fn variance_smoothed() -> f32;   // EMA-smoothed variance
    pub fn frame_count() -> u32;
}

Events

IDNameValueMeaning
725WASSERSTEIN_DISTANCEDistanceSmoothed sliced Wasserstein distance (emitted every 5 frames)
726DISTRIBUTION_SHIFTDistanceLarge distribution change detected (debounced, 3 consecutive frames > 0.25)
727SUBTLE_MOTIONDistanceMotion detected despite stable variance (5 consecutive frames with distance > 0.10 and variance change < 15%)

Configuration

ConstantValuePurpose
N_PROJ4Number of random projection directions
ALPHA0.15EMA alpha for distance smoothing
VAR_ALPHA0.1EMA alpha for variance smoothing
WASS_SHIFT0.25Wasserstein threshold for distribution shift event
WASS_SUBTLE0.10Wasserstein threshold for subtle motion
VAR_STABLE0.15Maximum relative variance change for "stable" classification
SHIFT_DEB3Debounce count for distribution shift
SUBTLE_DEB5Debounce count for subtle motion

Tutorial: Interpreting Wasserstein Distance

The Wasserstein distance measures the "cost" of transforming one distribution into another. Unlike variance-based metrics that only measure spread, it captures changes in shape, location, and mode structure.

Typical values:

  • 0.00-0.05: No motion. Static environment.
  • 0.05-0.15: Breathing, subtle body sway, environmental drift.
  • 0.15-0.30: Walking, arm movement, normal activity.
  • 0.30+: Large motion, multiple people moving, or sudden environmental change.

Why "subtle motion" matters: A person sitting still and slowly raising their hand creates almost no change in total signal variance, but the Wasserstein distance increases because the spatial distribution of signal strength shifts. This is critical for:

  • Fall detection (pre-fall sway)
  • Gesture recognition (micro-movements)
  • Intruder detection (someone trying to move stealthily)

Performance Budget

ModuleBudget TierTypical LatencyStack MemoryKey Bottleneck
Flash AttentionS (<5ms)~0.5ms~512 bytesSoftmax exp() over 8 groups
Coherence GateL (<2ms)~0.3ms~320 bytessin/cos per subcarrier
Temporal CompressS (<5ms)~0.8ms~12 KB512 snapshots * 24 bytes
Sparse RecoveryH (<10ms)~3ms~768 bytes10 ISTA iterations * 32 subcarriers
Min-Cut Person MatchH (<10ms)~1.5ms~640 bytes4x4 cost matrix + feature extraction
Optimal TransportS (<5ms)~1.5ms~1 KB8 Shell sorts (4 projections * 2 distributions)

All latencies are estimated for ESP32-S3 running WASM3 interpreter at 240 MHz. Actual performance varies with subcarrier count and frame complexity.

Memory Layout

All modules use fixed-size stack/static allocations. No heap, no alloc, no Vec. This is required for no_std WASM deployment on the ESP32-S3.

Total static memory for all 6 signal modules: approximately 15 KB, well within the ESP32-S3's available WASM linear memory.