Tables ▾

M5Stack Cardputer ADV · Volume 2

RF Object Detection (WiFi CSI) on the Cardputer ADV — Volume 2: Firmware Review, Per-MAC CSI Separation, and Multi-AP Feasibility

What the upstream Cardputer-CSI-Human-Detector firmware is and isn't, the code-review verdicts, per-MAC CSI separation as the immediate next development step, multi-AP and multi-channel feasibility as a general technique, and the PlatformIO build and flash workflow

2.1 What the upstream firmware actually is

Cardputer-CSI-Human-Detector (MIT, skizzophrenic, commit 28c9502) is a heuristic single-device CSI motion/presence detector purpose-built for the Cardputer ADV with an external 2.8″ ILI9341 screen. It is not a multi-node system, not a machine-learning inference pipeline, and not a vitals monitor — it is a lean, well-crafted embedded detector that turns the Wi-Fi CSI stream on one channel into a 0..1 motion scalar and renders it live.

The firmware joins one Wi-Fi network as STA, locks to that AP’s channel, and enables both promiscuous mode and CSI reporting simultaneously. csiCallback() (1,754-line main.cpp, lines 67–133) collects the raw CSI buffer on every received frame, computes mean amplitude and mean sin(phase), maintains 50-sample sliding windows for each, takes variance over the window, normalizes via adaptive EMA floor and running-max, and blends 60% amplitude / 40% phase into a single motion scalar. Two external display modes are provided on the ILI9341 (a PPI-sweep style and a vaporwave 3D render); the built-in 1.14″ screen shows a presence banner and motion graph. A bonus feature: a spy-cam OUI detector (main.cpp lines 845–929) plots known camera-vendor MACs as annotated blips, reusing the CSI-capable promiscuous frame path.

The most important architectural fact for everything that follows: the callback does not filter by source MAC. It discards info->mac and blends all CSI-bearing frames on the channel into the same amplitude/phase buffers. This is the key fact behind the per-MAC CSI separation direction below.

2.2 Code-review verdicts

A review of main.cpp, radar_link.h, and ext_panel.h produced the following verdicts:

2.2.1 Keep as-is

  • CSI motion heuristic itself — the variance-of-amplitude + variance-of-sin(phase) blend is a reasonable, lightweight motion detector that works without calibration. The adaptive normalization is a genuine engineering contribution.
  • WiFi AP picker + NVS credential storage via Preferences — clean and reusable.
  • Spy-cam OUI detector (main.cpp:845–929) — the OUI table and the frame-level MAC display logic are directly useful for RF recon.
  • radar_link.h ring-buffer parser + 240-sample ring — well-designed, reusable.
  • Two-sprite “render small + pushRotateZoom” display discipline — this is the correct answer to the no-PSRAM RAM constraint (see Vol 1 §4). Keep this pattern in any fork.
  • Settings/brightness menu — functional and non-invasive.

2.2.2 Change or fix

  • CSI math runs inside the Wi-Fi RX callback. The csiCallback() function contains two O(50) loops marked IRAM_ATTR — this executes on the Wi-Fi task (core 1), blocking it for the duration. The correct fix before adding any additional processing load is to push (mac, amplitude, sinphase) onto a FreeRTOS queue and do all variance/normalization math in a task on core 0.

  • Threshold disconnect. The keyboard ,// keys adjust gThreshold, but the CSI presence decision is gated by a fixed kCsiThresh = 0.15 at main.cpp:603. gThreshold only recolours the graph in CSI mode — it does not change the real detection gate. The UI control needs to be wired to kCsiThresh to be meaningful.

  • c calibrate is a no-op in CSI mode. The c key sends "CAL" over a UART that nothing is listening on. It should trigger a real baseline-capture snapshot (reset the EMA floor and running-max to the current idle state) or be hidden from the CSI-mode menu.

  • Dead code: enableCsi() callback overwrite. main.cpp:356–368 registers promiscuousRxCb and then immediately overwrites it with sniffCallback. One of these registrations is dead. Confirm which path is actually live before forking.

  • Bearing / RSSI→radius are cosmetic. The firmware plots a “radar blip” with bearing and distance derived from RSSI. A single antenna gives no real direction information and RSSI-to-distance on a cluttered indoor channel is noisy; these are visualization aesthetics, not localization. Do not present them as real direction or ranging.

  • Stale CLAUDE.md in the upstream repo. The upstream’s own CLAUDE.md describes a two-device architecture (S3 access point named SQUACHNET + UDP link) that no longer exists in the codebase. Trust the README.md, the platformio.ini (RADAR_CSI=1 build flag), and main.cpp — not the upstream’s CLAUDE.md.

2.3 Multi-AP / multi-channel feasibility (the big discussion)

The question: would listening to multiple routers / channels — the connected AP plus visible neighbor APs — give more information: better presence robustness, coarser localization, movement direction?

The answer: yes in principle, but it is a multi-radio + per-link-separation + known-geometry problem — not “listen harder on one radio.”

Each transmitter–receiver pair is a bistatic radar path. One link on one channel gives one “is the room changing?” scalar — what the upstream firmware delivers. Multiple links from different angles each sense different regions of the room. With enough links and known transmitter positions, this enables:

  • Presence robustness — proven, easy to achieve even with two links (redundancy, no blind spots from geometry).
  • Coarse localization and movement direction — proven in literature, moderate effort. Triangulate which links are disturbed; expect room-zone resolution, not centimetre precision.
  • Object count / rough size discrimination — plausible, harder. “One vs. several” or “small vs. large” is achievable; silhouette reconstruction is not.

What CSI radio tomography does not give: camera-like spatial resolution, or reliable vital-sign extraction (that claim appears in overstated literature; the well-documented CSI limitation is that respiration and heart-rate extraction requires controlled conditions and proximity that a single handheld rarely achieves).

  1. One radio = one channel. An ESP32-S3 cannot hear channels 1, 6, 11, and 5 GHz simultaneously. Channel-hopping fragments the CSI stream (bad for motion detection). The real fix for multi-channel is multiple radios, one per channel or AP, fused by a hub — an effort that belongs on a more capable platform (e.g. a Linux SBC running multiple ESP32 nodes via ESP-NOW or Wi-Fi).

  2. Must separate links — and ideally know geometry. The current firmware blends by discarding source MAC. Placed nodes at known positions enable real triangulation. Neighbor APs are free illuminators but have unknown geometry, bursty traffic (CSI only when they transmit — mostly ~10 Hz beacons — not continuous), and may change channel or power at any time. Use neighbor AP CSI to enrich, not anchor.

  3. CoTS phase is noisy. Commercial-off-the-shelf ESP32 CSI phase carries CFO/SFO jitter that precludes precision ranging or AoA from a single antenna. The upstream firmware correctly uses variance (not phase ranging) for this reason. Real angle-of-arrival or ranging needs multiple synchronized antennas or nodes.

2.3.2 The iteration ladder

  1. Now — single node, single channel, all-sources-blended: the upstream firmware. Presence and motion on one channel. No new hardware needed.
  2. Cheap software win (next step, below): split CSI by source MAC on the current channel. Gives independent per-AP motion streams. Pure code change, no new parts. First taste of multi-link on real hardware.
  3. Multi-radio bench kit: N × ESP32-C5/C6 each pinned to a different AP/channel, fused by a hub. Zone localization and movement direction become accessible. The real “many routers, different angles” payoff — worth a separate design pass (node count, channel plan, fusion architecture) before writing firmware.
  4. Known-geometry anchors: own TX nodes at surveyed positions → tomographic localization, improved object count/size discrimination.

2.4 Next concrete step — per-MAC CSI separation on the Cardputer

Goal: prove multi-link on the existing single Cardputer by separating CSI by transmitter MAC on the current channel, and visualizing each AP as its own independent motion stream. No new hardware required.

Why this matters: the csiCallback() already receives info->mac (6 bytes — the transmitter MAC of the CSI-bearing frame). The current code ignores it. Routing each MAC into its own motion stream takes a refactor of the existing globals into per-link structs — no new radio, no new antenna, just different bookkeeping.

Sketch of the implementation:

  1. In csiCallback(), read info->mac (6 bytes).
  2. Maintain a small table of links keyed by MAC — up to 6–8 slots, each with its own 50-sample amplitude/phase windows and normalization state (refactor the current globals into a csi_link_t struct). LRU-evict the least-recently-seen slot when the table is full.
  3. Compute a per-link motion 0..1 exactly as the current blended code does, plus track last RSSI and frame rate per link.
  4. Offload math from the callback to a FreeRTOS task (the existing “heavy work in RX callback” fix): push (mac, amplitude, sinphase) onto a queue; a task on core 0 does the per-link variance.
  5. UI: display the top-N links as separate motion traces, labelled by last-3 MAC bytes + RSSI. Reuse the existing scope display with one trace per AP, colour-coded per link. Keep a combined “any link active” indicator as today’s behaviour.

RAM cost: per-link sliding windows are cheap — 50 floats × 2 × 8 links ≈ 3 KB extra heap. Fully within budget.

What this will reveal: how many distinct CSI sources are audible on one channel in a real indoor environment, how their motion responses differ across angles, and whether per-link separation already hints at movement direction. This informs whether step 3 (multi-radio hub) is worth building.

Gotchas:

  • Neighbor AP frame rate is low and bursty (beacons ~10 Hz) — per-link traces for neighbor APs will update slowly unless that AP is actively transmitting data. Expect the connected AP to dominate.
  • Still one channel — neighbor APs on other channels are invisible (that is the multi-radio problem, step 3).

2.5 Build and flash — toolchain and workflow

The upstream firmware uses PlatformIO with the environment cardputer-radar-csi (defined in upstream/platformio.ini). The board target in the upstream platformio.ini is m5stack-stamps3 (the generic StampS3 / original Cardputer target); when forking for the ADV, change this to m5stack-cardputer-adv — the ADV has a different pinout, partition layout, and bus support; binaries are not interchangeable.

2.5.1 Toolchain setup

# Install PlatformIO Core (isolated env; sidesteps system Python)
pip install -U platformio

# Build from upstream/
cd upstream/
pio run -e cardputer-radar-csi            # compile only
pio run -e cardputer-radar-csi -t upload  # compile + flash (Cardputer on USB-C)
pio device monitor -b 115200              # serial console

PlatformIO Core 6.1.19 was verified to build the upstream cleanly on this machine (2026-06-24). The isolated env lives in ~/.platformio with its own bundled Python — immune to system Python version conflicts.

2.5.2 Build results (verified)

Table 1 — Build results (verified)

MetricValue
RAM usage (static)15.5% — 50,680 / 327,680 bytes
Flash usage29% — 913,325 / 3,145,728 bytes

Substantial headroom in both dimensions. The ~151 KB allocated to the two display sprites comes off the heap at runtime (the two-sprite pattern described in Vol 1 §4); it does not appear in the static RAM figure.

2.5.3 Flash and recovery

Always flash custom builds to ota_0, never to the factory partition — M5Launcher lives on the factory slot and is the recovery lifeline. Hold Esc at power-on to force-boot back to M5Launcher if a flashed firmware crash-loops.

Easiest no-toolchain flash for others: use a pre-built .bin from the upstream Releases page via the esptool web flasher at esptool.spacehuhn.com, loading at address 0x0.

2.6 Resources

  • Upstream firmware source: code/firmware/upstream/ (MIT, commit 28c9502) — src/main.cpp, radar_link.h, ext_panel.h.
  • Espressif ESP-IDF CSI API: esp_wifi_set_csi_rx_cb, wifi_csi_info_t, wifi_csi_config_t — in the ESP-IDF Wi-Fi driver docs.
  • Cardputer ADV reference: flashing + recovery, pinout + EXT bus.