firmware: verify against actual target hardware, fix real conflicts

Researched the exact modules the user is building with and fixed three
concrete issues the earlier speculative firmware got wrong:

1. WiFi: confirmed the target board (Waveshare ESP32-P4-Module-DEV-KIT,
   chip ESP32-P4NRW32) bridges WiFi through an onboard ESP32-C6
   co-processor over a fixed 7-pin SDIO link (CLK18/CMD19/D0-14/D1-15/
   D2-16/D3-17/RESET54, cross-confirmed against Espressif's own
   esp-hosted-mcu docs). wifi_manager.c's esp_wifi_init()/esp_wifi_start()
   calls don't need to change — esp_wifi_remote/esp_hosted provide a
   drop-in-compatible API — but the component manifest (new
   main/idf_component.yml) and sdkconfig.defaults were missing entirely.

2. Real pin conflict: the presence sensor's original UART pins (17/18)
   directly collided with the SDIO CLK/D3 pins above — wiring it there
   would have broken WiFi, the sensor, or both. Moved to GPIO4/5.

3. Swapped placeholder parts for the user's actual hardware:
   - BME280 -> BMP280 (GY-BMP280 module): temp+pressure only, no humidity.
     Rewrote the driver rather than just renaming it — the old code would
     have read nonexistent humidity registers and reported garbage
     forever. 3.3V-only wiring note added (the BME280 assumption of
     5V-tolerant logic doesn't hold for this specific breakout).
   - LD2410 -> RD-03E (Ai-Thinker, not Hi-Link — a different manufacturer
     with a different, incompatible UART protocol). Rewrote the frame
     parser against the RD-03E's actual (if less-documented) 5-byte
     simple-report format. Reports numeric distance instead of a boolean,
     which better fits both the hardware's actual output and the backend's
     statistical anomaly detector.

README, CMakeLists.txt, and all cross-references updated to match.
This commit is contained in:
Indiana
2026-07-24 21:30:37 +00:00
parent f87e9ab3f5
commit 6e627bae15
15 changed files with 521 additions and 513 deletions

View File

@@ -9,13 +9,20 @@ backend's `POST /api/device/telemetry` endpoint, which feeds them into the
séance's live anomaly-detection pipeline as a sixth signal source alongside
`wire`/`evp`/`radio`/`emf`.
**Target board:** [Waveshare ESP32-P4-Module-DEV-KIT](https://www.waveshare.com/esp32-p4-module-dev-kit.htm)
(chip: **ESP32-P4NRW32**, 16MB NOR flash, onboard **ESP32-C6** Wi-Fi
6/Bluetooth 5 co-processor over SDIO, USB OTG 2.0 HS, 40-pin 2×20 header
with 28 programmable GPIOs). All pin assignments and the Wi-Fi bring-up
approach below are specific to this board — see the Wi-Fi section for the
reserved SDIO pins and [Wiring / pinout](#wiring--pinout) for sensor pins.
## Honesty policy — READ THIS FIRST
> This app's whole ethos is "real signal processing on real data, and it
> says so when something is unverified."
**Nobody working on this had physical ESP32-P4 hardware, a BME280, or an
LD2410 module to flash and test against.** Everything in this directory is
**Nobody working on this had physical ESP32-P4 hardware, a BMP280, or an
RD-03E module to flash and test against.** Everything in this directory is
real, structurally-correct ESP-IDF C, written against ESP-IDF's documented
APIs and each sensor's public datasheet/protocol documentation, and reasoned
about carefully — but it has **never been compiled with a real ESP-IDF
@@ -33,17 +40,21 @@ firmware/esp32p4-sensor-node/
├── CMakeLists.txt top-level ESP-IDF project file
├── sdkconfig.defaults seed config (idf.py generates the real sdkconfig)
├── README.md this file
├── components/ reserved for Workstream J (RTL-SDR), empty here
├── components/
│ ├── README.md
│ └── rtlsdr_experimental/ Workstream J — opt-in USB-host RTL-SDR module
└── main/
├── CMakeLists.txt component registration
├── idf_component.yml managed deps: esp_wifi_remote, esp_hosted
├── app_main.c entry point / boot sequence
├── device_config.h.example template you copy to device_config.h
├── wifi_manager.{h,c} Wi-Fi station mode connect/reconnect
├── wifi_manager.{h,c} Wi-Fi station mode connect/reconnect (via
│ the onboard ESP32-C6 co-processor — see below)
├── telemetry_client.{h,c} HTTP POST task -> /api/device/telemetry
├── sensor_driver.h the sensor_driver_t registry interface
├── sensor_registry.{h,c} the concrete list of compiled-in drivers
├── bme280.{h,c} temperature/humidity/pressure over I2C
└── ld2410.{h,c} presence/distance over UART
├── bmp280.{h,c} temperature/pressure over I2C
└── rd03e.{h,c} presence/distance/gesture over UART
```
## Build instructions
@@ -108,21 +119,23 @@ consistent; no known syntax errors or obviously-wrong API usage):
- HTTP client (`telemetry_client.c`) builds the exact JSON shape the spec's
contract defines and POSTs it via `esp_http_client` with
`Authorization: Bearer <token>` and `Content-Type: application/json`.
- BME280 driver (`bme280.c`): register map and the double-precision
compensation formulas are transcribed from Bosch's public BME280
datasheet (rev 1.23, §4.2.2–4.2.3) — this is well-trodden, publicly
- BMP280 driver (`bmp280.c`): register map and the double-precision
compensation formulas are transcribed from Bosch's public BMP280
datasheet (rev 1.23, §3.11.1–3.11.3) — this is well-trodden, publicly
documented territory, and the formulas are checkable line-by-line against
the datasheet. Uses ESP-IDF's newer `driver/i2c_master.h` API (the
current idiomatic choice; the older `driver/i2c.h` is being phased out).
- LD2410 driver (`ld2410.c`): UART frame envelope (header/footer magic
bytes, length-prefixed payload) follows the shape consistently reported
across public LD2410 protocol write-ups. **The exact payload byte offsets
for target state / distances / energies are the single least-certain
piece of code in this entire firmware** — see the detailed note in
`ld2410_parse_payload()`. The driver defends itself with a head/tail
marker sanity check (`0xAA`/`0x55`) and silently skips anything that
doesn't match rather than reporting garbage, but that check catches
gross corruption, not subtle off-by-one offset errors.
Temperature + pressure only — the part in use (a GY-BMP280 breakout) has
no humidity sensor, unlike its BME280 sibling.
- RD-03E driver (`rd03e.c`): the 5-byte "simple report" UART frame
(`0xAA` header, gesture byte, little-endian distance, `0x55 0x55`
footer) is reconstructed from a third-party bring-up write-up, not
Ai-Thinker's own datasheet (not available while writing this) — **the
single least-certain piece of code in this entire firmware.** The
gesture byte's exact value-to-meaning mapping is unconfirmed, so the
driver reports it as a raw code in `metadata` rather than guessing at a
translated label. Cross-confirmed from multiple sources: 256000 baud,
8N1 UART framing.
- Sensor driver registry (`sensor_driver.h`, `sensor_registry.c`): a
`sensor_driver_t { name, init, read }` struct, a compile-time array of
them, and generic init/collect functions that `app_main.c` and
@@ -136,15 +149,16 @@ consistent; no known syntax errors or obviously-wrong API usage):
version you build with that only a real compile will surface.
- I2C timing/electricals: pull-up resistor values, bus speed headroom,
cable length — none of this has been bench-tested.
- BME280 compensation formula correctness in practice: the math is
- BMP280 compensation formula correctness in practice: the math is
transcribed carefully, but "matches the datasheet" and "produces a
plausible number when this exact C runs on this exact silicon" are
different claims until someone compares a real reading to a reference
thermometer/barometer.
- LD2410 frame parsing, as above — verify against a logic analyzer capture
or a known-good reference implementation (e.g. the `ncmreynolds/ld2410`
or `iavorvel/MyLD2410` Arduino libraries, cross-checked) before trusting
field values.
- RD-03E frame parsing, as above — this one especially, since the frame
format itself (not just the implementation) is reconstructed from a
third-party source rather than an official datasheet. Verify against a
logic analyzer capture before trusting field values, and treat the
gesture code's meaning as genuinely unknown until cross-checked.
- Wi-Fi reconnect behavior under real-world conditions (router reboot,
weak signal, captive portals) — the backoff logic is reasoned about, not
soak-tested.
@@ -154,78 +168,114 @@ consistent; no known syntax errors or obviously-wrong API usage):
but that's untested against the live deploy.
- Timing/power: task stack sizes (`telemetry_task`'s 8192 words, etc.) are
reasonable guesses, not measured high-water-marks from a real run.
- **The ESP32-P4-has-no-integrated-Wi-Fi caveat below** — this is a real
hardware architecture question, not just an untested detail.
- Everything under [Wiring / pinout](#wiring--pinout) and the Wi-Fi section
below — confirmed against the actual target board's published specs, but
never checked against the physical hardware itself.
## Important hardware caveat: ESP32-P4 has no integrated Wi-Fi radio
## Wi-Fi: ESP32-P4 has no integrated radio — confirmed target hardware
The ESP32-P4 SoC (per Espressif's own published specs) has **no built-in
2.4GHz radio**. A real deployment needs one of:
The ESP32-P4 SoC has **no built-in 2.4GHz radio**. This was flagged
speculatively in an earlier pass; it's now confirmed against the actual
target hardware — **Waveshare ESP32-P4-Module-DEV-KIT**, chip
**ESP32-P4NRW32** — which solves this the way Espressif's own reference
design does: an onboard **ESP32-C6** co-processor, wired to the P4 over a
**fixed 7-pin SDIO link**, running Wi-Fi 6 + Bluetooth 5 on the seeker's
behalf. These 7 pins are physically fixed by the board's own PCB routing —
**do not reuse them for sensors**:
- **A companion Wi-Fi chip** (e.g. ESP32-C6) wired to the P4 via SDIO or
SPI, running Espressif's "esp-hosted" firmware/driver stack. Critically,
esp-hosted presents the *same* `esp_wifi`/`esp_netif` API this firmware
already uses — so `wifi_manager.c` should not need to change, only board
wiring and `sdkconfig` (host-side esp-hosted config) would.
- **Building this same code against a Wi-Fi-native target instead**, e.g.
`idf.py set-target esp32s3` or `esp32c6`. The application code
(`wifi_manager.c`, `telemetry_client.c`, the sensor drivers) is written
against the standard API surface and doesn't reference P4-specific
peripherals for anything except I2C/UART GPIO numbers, so it should be
largely target-portable.
| Signal | GPIO |
|---|---|
| SDIO CLK | 18 |
| SDIO CMD | 19 |
| SDIO D0 | 14 |
| SDIO D1 | 15 |
| SDIO D2 | 16 |
| SDIO D3 | 17 |
| C6 Reset | 54 |
This wasn't in the original spec's framing but matters enough for a real
build that it's called out here explicitly, in the honesty-policy spirit —
better to flag a real hardware-architecture gap than let someone discover
it after ordering a bare P4 dev board expecting it to just join Wi-Fi.
(Sourced from Waveshare's own documentation and independently cross-checked
against Espressif's `esp-hosted-mcu` reference docs for their nearly
identical ESP32-P4-Function-EV-Board, which uses the same 7 pins — strong
agreement across two independent sources, though still not a substitute for
checking the physical board's silkscreen.)
Wi-Fi is brought up via two managed components — `espressif/esp_wifi_remote`
and `espressif/esp_hosted` — declared in `main/idf_component.yml` and
configured in `sdkconfig.defaults` (`CONFIG_SLAVE_IDF_TARGET_ESP32C6`,
`CONFIG_ESP_HOSTED_CP_TARGET_ESP32C6`, `CONFIG_ESP_HOSTED_P4_DEV_BOARD_FUNC_BOARD`,
plus buffer/window tuning). Critically, these components provide a
**drop-in-compatible** `esp_wifi_*`/`esp_netif_*` API — `wifi_manager.c`'s
`esp_wifi_init()`/`esp_wifi_start()` calls are exactly what they'd be on a
Wi-Fi-native chip; only the component manifest and sdkconfig differ, not
the application code. `idf.py build` will need network access the first
time to fetch these two components from the ESP Component Registry.
**What's still unverified here specifically:** whether
`CONFIG_ESP_HOSTED_P4_DEV_BOARD_FUNC_BOARD` (a preset named for Espressif's
own eval board) configures correctly for Waveshare's board given they
share the same SDIO pin assignment — likely fine, but the first thing to
check in `idf.py menuconfig` if Wi-Fi bring-up misbehaves is the Wi-Fi
Remote / ESP-Hosted config screens directly rather than trusting the preset
blindly.
## Wiring / pinout
### BME280 (I2C) — temperature, humidity, pressure
Confirmed against the target board (Waveshare ESP32-P4-Module-DEV-KIT):
GPIOs 14-19 and 54 are reserved for the onboard Wi-Fi co-processor's SDIO
link (see the Wi-Fi section above) — never wire a sensor to those pins on
this board.
Chosen as the concrete default sensor per the spec ("a common,
well-documented sensor... pick this as the concrete default since no
specific part number was given").
### BMP280 (I2C) — temperature, pressure
| BME280 pin | Connects to |
Confirmed against the actual part in use: a **GY-BMP280** breakout module
(Bosch BMP280 — temperature + pressure only, no humidity). GPIO8/9 don't
conflict with the board's reserved SDIO range. **3.3V only** — the
GY-BMP280 module is not 5V tolerant, unlike the RD-03E below.
| BMP280 pin | Connects to |
|------------|---------------------------------------|
| VCC | 3V3 |
| VCC | 3V3 (3.3V ONLY — do not connect to 5V)|
| GND | GND |
| SDA | GPIO8 (`BME280_I2C_SDA_GPIO`) |
| SCL | GPIO9 (`BME280_I2C_SCL_GPIO`) |
| SDA | GPIO8 (`BMP280_I2C_SDA_GPIO`) |
| SCL | GPIO9 (`BMP280_I2C_SCL_GPIO`) |
| CSB | VCC (selects I2C mode, not SPI) |
| SDO | GND → I2C address `0x76` (default assumed; tie to VCC + change `BME280_I2C_ADDR` for `0x77`) |
| SDO | GND → I2C address `0x76` (default assumed; tie to VCC + change `BMP280_I2C_ADDR` for `0x77`) |
GPIO numbers are `#define`s at the top of `bme280.h` — override them there
GPIO numbers are `#define`s at the top of `bmp280.h` — override them there
(or via a future `idf.py menuconfig` entry) to match your actual wiring.
100kHz I2C clock by default (`BME280_I2C_CLK_HZ`); the part supports faster
100kHz I2C clock by default (`BMP280_I2C_CLK_HZ`); the part supports faster
modes if your wiring/pull-ups support it.
### LD2410 (UART) — presence, distance, motion
### RD-03E (UART) — presence, distance, gesture
**Chosen over a plain PIR** — see the rationale in `ld2410.h`'s header
comment: the LD2410 reports moving-target and stationary-target distance
and energy separately, not just a boolean, which is richer signal for the
anomaly pipeline and better matches this app's "believable" ethos (it can
distinguish "someone crossed the room" from "the sitter shifted in their
chair" in a way a boolean PIR cannot). The tradeoff is a materially more
complex protocol than a PIR's single GPIO pin — see the honesty note in
[What's verified vs. not](#whats-verified-vs-not) about the LD2410 frame
parser being the least-certain code in this firmware. If you'd rather start
with a boolean PIR for a faster, more certain first bring-up, it fits the
same `sensor_driver_t` interface — see
[Adding a new sensor](#adding-a-new-sensor) below.
Confirmed against the actual part in use: an **Ai-Thinker RD-03E** 24GHz
mmWave radar ("Human Gesture Recognition... Precise Ranging & Positioning
Radar Sensor Module"). Originally speculatively planned as a Hi-Link
LD2410 — a **different manufacturer with a different, incompatible UART
protocol** — so this driver was rewritten from scratch against the RD-03E's
actual (if less-documented) frame format rather than adapted from the
LD2410 code. Reports ranged distance (not just a boolean), fitting a
handheld "sense a presence at a distance" device well — see the honesty
note in [What's verified vs. not](#whats-verified-vs-not) about the frame
parser being the single least-certain code in this firmware.
| LD2410 pin | Connects to |
| RD-03E pin | Connects to |
|------------|----------------------------------------|
| VCC | 5V (sensor front-end runs at 5V; confirm your board revision's UART logic level before wiring directly to a 3.3V-only UART pin) |
| VCC | 5V (module power; UART logic is 0-3.3V, ESP32-safe) |
| GND | GND |
| TX | GPIO17 (`LD2410_UART_RX_GPIO`, ESP32 RX) |
| RX | GPIO18 (`LD2410_UART_TX_GPIO`, ESP32 TX) |
| OT1 | GPIO4 (`RD03E_UART_RX_GPIO`, ESP32 RX) — module's UART TX output |
| RX | GPIO5 (`RD03E_UART_TX_GPIO`, ESP32 TX) — module's UART RX input |
| OT2 | not connected (reserved on the module, unused here) |
Default UART settings: 256000 baud, 8N1 (module factory default), reporting
in "basic" (non-engineering) mode. GPIO numbers and baud rate are
`#define`s at the top of `ld2410.h`.
(GPIO4/5 avoid this board's reserved SDIO pins and the BMP280's I2C pins —
a reasonable, currently-unused pick, not verified against the physical
board's full pin map beyond confirming it's outside those known-reserved
ranges.)
Default UART settings: 256000 baud, 8N1 (module factory default), reading
only the module's free-running "simple report" frames — no configuration
handshake is sent or required. GPIO numbers and baud rate are `#define`s
at the top of `rd03e.h`.
## Sensor driver registry — the extensibility pattern
@@ -246,10 +296,10 @@ typedef struct sensor_driver {
} sensor_driver_t;
```
`sensor_registry.c` holds a compile-time array of these (currently BME280
and LD2410) and two generic functions, `sensor_registry_init_all()` and
`sensor_registry.c` holds a compile-time array of these (currently BMP280
and RD-03E) and two generic functions, `sensor_registry_init_all()` and
`sensor_registry_collect()`, that `app_main.c` and `telemetry_client.c`
call without ever referencing `bme280.c`/`ld2410.c` directly. One driver
call without ever referencing `bmp280.c`/`rd03e.c` directly. One driver
failing `init()` or `read()` is logged and skipped — it doesn't take the
whole node offline.
@@ -287,11 +337,12 @@ Content-Type: application/json
}
```
This firmware's BME280 driver emits `temperature`/`humidity`/`pressure`
exactly as shown; its LD2410 driver emits `presence` as a `0`/`1` boolean
in `value` with the richer distance/energy data folded into `metadata`
(`moving_distance_cm`, `moving_energy`, `stationary_distance_cm`,
`stationary_energy`, `detection_distance_cm`, `target_state`).
This firmware's BMP280 driver emits `temperature`/`pressure` (no
`humidity` — see above); its RD-03E driver emits `presence` as a numeric
distance in centimeters (`unit: "cm"`, not a `0`/`1` boolean) so the
backend's statistical anomaly detector can treat "something got suddenly
close" as the signal, with the module's raw gesture code folded into
`metadata` (`gesture_code`) rather than a boolean transition detector.
## Out of scope here
@@ -520,7 +571,7 @@ likely to break first":
here for whoever does the merge.
9. **Memory footprint.** Multiple in-flight 16KB bulk transfer buffers
plus a forward queue plus `esp_http_client` buffers, alongside whatever
Workstream I's WiFi/HTTP/BME280/presence stack already needs, on a
Workstream I's WiFi/HTTP/BMP280/RD-03E stack already needs, on a
single ESP32-P4's RAM — not sized or measured against a real linker
map.