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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user