diff --git a/firmware/esp32p4-sensor-node/README.md b/firmware/esp32p4-sensor-node/README.md index 103a2ab..fb1d219 100644 --- a/firmware/esp32p4-sensor-node/README.md +++ b/firmware/esp32p4-sensor-node/README.md @@ -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 ` 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. diff --git a/firmware/esp32p4-sensor-node/main/CMakeLists.txt b/firmware/esp32p4-sensor-node/main/CMakeLists.txt index d8722ed..c6ad7a6 100644 --- a/firmware/esp32p4-sensor-node/main/CMakeLists.txt +++ b/firmware/esp32p4-sensor-node/main/CMakeLists.txt @@ -12,8 +12,8 @@ idf_component_register( "wifi_manager.c" "telemetry_client.c" "sensor_registry.c" - "bme280.c" - "ld2410.c" + "bmp280.c" + "rd03e.c" INCLUDE_DIRS "." REQUIRES diff --git a/firmware/esp32p4-sensor-node/main/app_main.c b/firmware/esp32p4-sensor-node/main/app_main.c index 8cf3b5f..9ba19b0 100644 --- a/firmware/esp32p4-sensor-node/main/app_main.c +++ b/firmware/esp32p4-sensor-node/main/app_main.c @@ -1,7 +1,7 @@ // Quantumancy ESP32-P4 Sensor Node — entry point. // // UNVERIFIED AGAINST REAL HARDWARE. Nobody working on this workstream has a -// physical ESP32-P4 (or the BME280 / LD2410 modules) to flash and test +// physical ESP32-P4 (or the BMP280 / RD-03E modules) to flash and test // against. This firmware is real, structurally-sound ESP-IDF C, reasoned // about carefully against ESP-IDF's documented APIs and the public // datasheets/protocol docs for each sensor -- but "compiles and reads diff --git a/firmware/esp32p4-sensor-node/main/bme280.h b/firmware/esp32p4-sensor-node/main/bme280.h deleted file mode 100644 index 83ff6f1..0000000 --- a/firmware/esp32p4-sensor-node/main/bme280.h +++ /dev/null @@ -1,57 +0,0 @@ -// Bosch BME280 driver (temperature / humidity / pressure) over I2C. -// -// Implements the sensor_driver_t interface (see sensor_driver.h) so the -// registry can init/read it generically. Register map and compensation -// formulas are transcribed from Bosch's public BME280 datasheet (Rev 1.23, -// section 4.2.3 "Compensation formulas", double-precision reference -// implementation) — publicly documented, well-trodden territory, not -// guesswork. What IS unverified: this has never talked to a real BME280 — -// see README.md's honesty-policy section. -// -// Default wiring (override via `idf.py menuconfig` or by editing the pin -// macros below before building) — see README.md's wiring section for the -// full pinout table: -// BME280 SDA -> GPIO8 (BME280_I2C_SDA_GPIO) -// BME280 SCL -> GPIO9 (BME280_I2C_SCL_GPIO) -// BME280 VCC -> 3V3, GND -> GND -// BME280 CSB -> VCC (selects I2C mode, not SPI) -// BME280 SDO -> GND selects address 0x76 (default assumed here); tie SDO -// to VCC instead for address 0x77 and change -// BME280_I2C_ADDR accordingly. - -#pragma once - -#include "esp_err.h" -#include "sensor_driver.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef BME280_I2C_PORT -#define BME280_I2C_PORT 0 -#endif - -#ifndef BME280_I2C_SDA_GPIO -#define BME280_I2C_SDA_GPIO 8 -#endif - -#ifndef BME280_I2C_SCL_GPIO -#define BME280_I2C_SCL_GPIO 9 -#endif - -#ifndef BME280_I2C_ADDR -#define BME280_I2C_ADDR 0x76 // 0x77 if SDO is tied to VCC instead of GND -#endif - -#ifndef BME280_I2C_CLK_HZ -#define BME280_I2C_CLK_HZ 100000 // 100kHz standard mode; BME280 supports up to 3.4MHz -#endif - -// sensor_driver_t-compatible entry points. -esp_err_t bme280_init(void); -esp_err_t bme280_read(sensor_reading_t *out, size_t max_out, size_t *out_count); - -#ifdef __cplusplus -} -#endif diff --git a/firmware/esp32p4-sensor-node/main/bme280.c b/firmware/esp32p4-sensor-node/main/bmp280.c similarity index 56% rename from firmware/esp32p4-sensor-node/main/bme280.c rename to firmware/esp32p4-sensor-node/main/bmp280.c index d8dc8fa..473104c 100644 --- a/firmware/esp32p4-sensor-node/main/bme280.c +++ b/firmware/esp32p4-sensor-node/main/bmp280.c @@ -1,40 +1,42 @@ -// Bosch BME280 driver — see bme280.h for wiring and honesty notes. +// Bosch BMP280 driver — see bmp280.h for wiring and honesty notes. // // UNVERIFIED AGAINST REAL HARDWARE: this has been written against the public -// BME280 datasheet (Bosch Sensortec, document rev 1.23) and ESP-IDF's +// BMP280 datasheet (Bosch Sensortec, document rev 1.23) and ESP-IDF's // documented `driver/i2c_master.h` API surface, and reasoned about carefully, // but never compiled with a real ESP-IDF toolchain nor run against a real -// sensor. Register addresses, the calibration-word packing, and the -// compensation formulas below are transcribed as directly as possible from -// the datasheet's section 4.2.2 (register map) and 4.2.3 (double-precision -// compensation formula reference implementation) to minimize transcription -// risk, but a real bring-up should sanity-check first readings against a -// known-good reference (e.g. compare to a household thermometer/barometer). +// sensor. Register addresses and the compensation formula below are +// transcribed as directly as possible from the datasheet's section 3.11.1 +// (register map) and 3.11.3 (double-precision compensation formula +// reference implementation) to minimize transcription risk, but a real +// bring-up should sanity-check first readings against a known-good +// reference (e.g. compare to a household thermometer/barometer). #include #include #include -#include "bme280.h" +#include "bmp280.h" #include "driver/i2c_master.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" -static const char *TAG = "bme280"; +static const char *TAG = "bmp280"; -// --- Register map (BME280 datasheet section 4.2.2) ------------------------- +// --- Register map (BMP280 datasheet section 3.11.1) ------------------------ #define REG_CHIP_ID 0xD0 #define REG_RESET 0xE0 -#define REG_CTRL_HUM 0xF2 #define REG_STATUS 0xF3 #define REG_CTRL_MEAS 0xF4 #define REG_CONFIG 0xF5 -#define REG_PRESS_MSB 0xF7 // press(3) + temp(3) + hum(2) = 8 bytes, burst-read from here -#define REG_CALIB00 0x88 // dig_T1..dig_P9, 26 bytes: 0x88-0xA1 -#define REG_CALIB_H1 0xA1 // dig_H1, 1 byte -#define REG_CALIB26 0xE1 // dig_H2..dig_H6, 7 bytes: 0xE1-0xE7 +#define REG_PRESS_MSB 0xF7 // press(3) + temp(3) = 6 bytes, burst-read from here +#define REG_CALIB00 0x88 // dig_T1..dig_P9, 24 bytes: 0x88-0x9F + +#define CHIP_ID_EXPECTED 0x58 // BMP280 (vs. BME280's 0x60 — a chip-ID + // mismatch here likely means a BME280 is + // wired instead; we proceed anyway since the + // temp/pressure register map and compensation + // math is identical between the two parts) -#define CHIP_ID_EXPECTED 0x60 #define RESET_MAGIC 0xB6 #define STATUS_MEASURING_BIT 0x08 @@ -52,17 +54,11 @@ typedef struct { int16_t dig_P7; int16_t dig_P8; int16_t dig_P9; - uint8_t dig_H1; - int16_t dig_H2; - uint8_t dig_H3; - int16_t dig_H4; - int16_t dig_H5; - int8_t dig_H6; -} bme280_calib_t; +} bmp280_calib_t; static i2c_master_bus_handle_t s_bus = NULL; static i2c_master_dev_handle_t s_dev = NULL; -static bme280_calib_t s_calib; +static bmp280_calib_t s_calib; static bool s_ready = false; static esp_err_t write_reg(uint8_t reg, uint8_t val) { @@ -82,60 +78,32 @@ static uint16_t u16(uint8_t lsb, uint8_t msb) { } static esp_err_t read_calibration(void) { - uint8_t buf1[26]; // 0x88..0xA1 - uint8_t h1; - uint8_t buf2[7]; // 0xE1..0xE7 + uint8_t buf[24]; // 0x88..0x9F - esp_err_t err = read_regs(REG_CALIB00, buf1, sizeof(buf1)); - if (err != ESP_OK) return err; - err = read_regs(REG_CALIB_H1, &h1, 1); - if (err != ESP_OK) return err; - err = read_regs(REG_CALIB26, buf2, sizeof(buf2)); + esp_err_t err = read_regs(REG_CALIB00, buf, sizeof(buf)); if (err != ESP_OK) return err; - s_calib.dig_T1 = u16(buf1[0], buf1[1]); - s_calib.dig_T2 = s16(buf1[2], buf1[3]); - s_calib.dig_T3 = s16(buf1[4], buf1[5]); - s_calib.dig_P1 = u16(buf1[6], buf1[7]); - s_calib.dig_P2 = s16(buf1[8], buf1[9]); - s_calib.dig_P3 = s16(buf1[10], buf1[11]); - s_calib.dig_P4 = s16(buf1[12], buf1[13]); - s_calib.dig_P5 = s16(buf1[14], buf1[15]); - s_calib.dig_P6 = s16(buf1[16], buf1[17]); - s_calib.dig_P7 = s16(buf1[18], buf1[19]); - s_calib.dig_P8 = s16(buf1[20], buf1[21]); - s_calib.dig_P9 = s16(buf1[22], buf1[23]); - // buf1[24] is reserved (0xA0), buf1[25] would be dig_H1 duplicate on some - // parts — we read dig_H1 explicitly from 0xA1 above instead of relying - // on that, to match the datasheet's documented address exactly. - s_calib.dig_H1 = h1; - - // dig_H4/dig_H5 have an odd 12-bit packing across 3 bytes (datasheet - // 4.2.2, table 16): - // dig_H4 = (E4[11:4] << 4) | E5[3:0] - // dig_H5 = (E6[11:4] << 4) | (E5[7:4]) - uint8_t e1 = buf2[0]; // 0xE1 -> dig_H2 lsb - uint8_t e2 = buf2[1]; // 0xE2 -> dig_H2 msb - uint8_t e3 = buf2[2]; // 0xE3 -> dig_H3 - uint8_t e4 = buf2[3]; // 0xE4 - uint8_t e5 = buf2[4]; // 0xE5 - uint8_t e6 = buf2[5]; // 0xE6 - uint8_t e7 = buf2[6]; // 0xE7 -> dig_H6 - - s_calib.dig_H2 = s16(e1, e2); - s_calib.dig_H3 = e3; - s_calib.dig_H4 = (int16_t)(((int8_t)e4 << 4) | (e5 & 0x0F)); - s_calib.dig_H5 = (int16_t)(((int8_t)e6 << 4) | (e5 >> 4)); - s_calib.dig_H6 = (int8_t)e7; + s_calib.dig_T1 = u16(buf[0], buf[1]); + s_calib.dig_T2 = s16(buf[2], buf[3]); + s_calib.dig_T3 = s16(buf[4], buf[5]); + s_calib.dig_P1 = u16(buf[6], buf[7]); + s_calib.dig_P2 = s16(buf[8], buf[9]); + s_calib.dig_P3 = s16(buf[10], buf[11]); + s_calib.dig_P4 = s16(buf[12], buf[13]); + s_calib.dig_P5 = s16(buf[14], buf[15]); + s_calib.dig_P6 = s16(buf[16], buf[17]); + s_calib.dig_P7 = s16(buf[18], buf[19]); + s_calib.dig_P8 = s16(buf[20], buf[21]); + s_calib.dig_P9 = s16(buf[22], buf[23]); return ESP_OK; } -esp_err_t bme280_init(void) { +esp_err_t bmp280_init(void) { i2c_master_bus_config_t bus_cfg = { - .i2c_port = BME280_I2C_PORT, - .sda_io_num = BME280_I2C_SDA_GPIO, - .scl_io_num = BME280_I2C_SCL_GPIO, + .i2c_port = BMP280_I2C_PORT, + .sda_io_num = BMP280_I2C_SDA_GPIO, + .scl_io_num = BMP280_I2C_SCL_GPIO, .clk_source = I2C_CLK_SRC_DEFAULT, .glitch_ignore_cnt = 7, .flags.enable_internal_pullup = true, @@ -148,8 +116,8 @@ esp_err_t bme280_init(void) { i2c_device_config_t dev_cfg = { .dev_addr_length = I2C_ADDR_BIT_LEN_7, - .device_address = BME280_I2C_ADDR, - .scl_speed_hz = BME280_I2C_CLK_HZ, + .device_address = BMP280_I2C_ADDR, + .scl_speed_hz = BMP280_I2C_CLK_HZ, }; err = i2c_master_bus_add_device(s_bus, &dev_cfg, &s_dev); if (err != ESP_OK) { @@ -164,10 +132,10 @@ esp_err_t bme280_init(void) { return err; } if (chip_id != CHIP_ID_EXPECTED) { - ESP_LOGW(TAG, "unexpected chip id 0x%02x (want 0x%02x) -- wrong wiring/address, or a BMP280 (no humidity)?", chip_id, CHIP_ID_EXPECTED); - // Don't hard-fail: a BMP280 (temp+pressure only, same register map - // minus humidity) would also land here and can still usefully report - // two of the three readings. We proceed and let real data speak. + ESP_LOGW(TAG, "unexpected chip id 0x%02x (want 0x%02x) -- check wiring/address", chip_id, CHIP_ID_EXPECTED); + // Don't hard-fail: temp+pressure register map/compensation is + // identical on a BME280 too, so a mis-wired-but-present sensor of + // either part can still usefully report both readings. } err = write_reg(REG_RESET, RESET_MAGIC); @@ -180,17 +148,12 @@ esp_err_t bme280_init(void) { return err; } - // Humidity oversampling x1. Must be written before ctrl_meas for the - // change to take effect (datasheet 5.4.3). - err = write_reg(REG_CTRL_HUM, 0x01); - if (err != ESP_OK) return err; - s_ready = true; - ESP_LOGI(TAG, "BME280 init ok (chip id 0x%02x)", chip_id); + ESP_LOGI(TAG, "BMP280 init ok (chip id 0x%02x)", chip_id); return ESP_OK; } -// Bosch datasheet 4.2.3 double-precision reference compensation formulas, +// Bosch datasheet 3.11.3 double-precision reference compensation formulas, // transcribed near-verbatim (variable names kept close to the original so // it's checkable against the datasheet PDF side-by-side). @@ -220,23 +183,12 @@ static double compensate_pressure(int32_t adc_P, double t_fine) { return p; // Pa } -static double compensate_humidity(int32_t adc_H, double t_fine) { - double var_h = (t_fine - 76800.0); - var_h = (adc_H - (((double)s_calib.dig_H4) * 64.0 + ((double)s_calib.dig_H5) / 16384.0 * var_h)) * - (((double)s_calib.dig_H2) / 65536.0 * (1.0 + ((double)s_calib.dig_H6) / 67108864.0 * var_h * - (1.0 + ((double)s_calib.dig_H3) / 67108864.0 * var_h))); - var_h = var_h * (1.0 - ((double)s_calib.dig_H1) * var_h / 524288.0); - if (var_h > 100.0) var_h = 100.0; - if (var_h < 0.0) var_h = 0.0; - return var_h; // %RH -} - -esp_err_t bme280_read(sensor_reading_t *out, size_t max_out, size_t *out_count) { +esp_err_t bmp280_read(sensor_reading_t *out, size_t max_out, size_t *out_count) { *out_count = 0; if (!s_ready) { return ESP_ERR_INVALID_STATE; } - if (max_out < 3) { + if (max_out < 2) { return ESP_ERR_NO_MEM; } @@ -261,18 +213,16 @@ esp_err_t bme280_read(sensor_reading_t *out, size_t max_out, size_t *out_count) } } - uint8_t raw[8]; + uint8_t raw[6]; err = read_regs(REG_PRESS_MSB, raw, sizeof(raw)); if (err != ESP_OK) return err; int32_t adc_P = ((int32_t)raw[0] << 12) | ((int32_t)raw[1] << 4) | (raw[2] >> 4); int32_t adc_T = ((int32_t)raw[3] << 12) | ((int32_t)raw[4] << 4) | (raw[5] >> 4); - int32_t adc_H = ((int32_t)raw[6] << 8) | raw[7]; double t_fine = 0.0; double temp_c = compensate_temperature(adc_T, &t_fine); double press_pa = compensate_pressure(adc_P, t_fine); - double hum_pct = compensate_humidity(adc_H, t_fine); size_t n = 0; memset(&out[n], 0, sizeof(out[n])); @@ -282,13 +232,6 @@ esp_err_t bme280_read(sensor_reading_t *out, size_t max_out, size_t *out_count) out[n].metadata = NULL; n++; - memset(&out[n], 0, sizeof(out[n])); - strncpy(out[n].sensor_type, "humidity", SENSOR_READING_TYPE_MAXLEN - 1); - out[n].value = hum_pct; - strncpy(out[n].unit, "pct", SENSOR_READING_UNIT_MAXLEN - 1); - out[n].metadata = NULL; - n++; - memset(&out[n], 0, sizeof(out[n])); strncpy(out[n].sensor_type, "pressure", SENSOR_READING_TYPE_MAXLEN - 1); out[n].value = press_pa / 100.0; // Pa -> hPa diff --git a/firmware/esp32p4-sensor-node/main/bmp280.h b/firmware/esp32p4-sensor-node/main/bmp280.h new file mode 100644 index 0000000..27a74ac --- /dev/null +++ b/firmware/esp32p4-sensor-node/main/bmp280.h @@ -0,0 +1,63 @@ +// Bosch BMP280 driver (temperature / pressure only) over I2C. +// +// Implements the sensor_driver_t interface (see sensor_driver.h) so the +// registry can init/read it generically. Register map and compensation +// formulas are transcribed from Bosch's public BMP280 datasheet (Rev 1.23, +// section 3.11.3 "Compensation formulas", double-precision reference +// implementation) — publicly documented, well-trodden territory, not +// guesswork. What IS unverified: this has never talked to a real BMP280 — +// see README.md's honesty-policy section. +// +// Confirmed against the actual part in use: a GY-BMP280 breakout module +// (Bosch BMP280 — temperature + pressure ONLY, no humidity; do not confuse +// with the BME280, which adds a humidity sensor on an otherwise near- +// identical register map). 3.3V logic only — the GY-BMP280 module is not +// 5V tolerant, unlike the LD2410-family modules elsewhere in this project. +// +// Default wiring (override via `idf.py menuconfig` or by editing the pin +// macros below before building) — see README.md's wiring section for the +// full pinout table: +// BMP280 SDA -> GPIO8 (BMP280_I2C_SDA_GPIO) +// BMP280 SCL -> GPIO9 (BMP280_I2C_SCL_GPIO) +// BMP280 VCC -> 3V3, GND -> GND (3.3V ONLY — see above) +// BMP280 CSB -> VCC (selects I2C mode, not SPI) +// BMP280 SDO -> GND selects address 0x76 (default assumed here); tie SDO +// to VCC instead for address 0x77 and change +// BMP280_I2C_ADDR accordingly. + +#pragma once + +#include "esp_err.h" +#include "sensor_driver.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef BMP280_I2C_PORT +#define BMP280_I2C_PORT 0 +#endif + +#ifndef BMP280_I2C_SDA_GPIO +#define BMP280_I2C_SDA_GPIO 8 +#endif + +#ifndef BMP280_I2C_SCL_GPIO +#define BMP280_I2C_SCL_GPIO 9 +#endif + +#ifndef BMP280_I2C_ADDR +#define BMP280_I2C_ADDR 0x76 // 0x77 if SDO is tied to VCC instead of GND +#endif + +#ifndef BMP280_I2C_CLK_HZ +#define BMP280_I2C_CLK_HZ 100000 // 100kHz standard mode; BMP280 supports up to 3.4MHz +#endif + +// sensor_driver_t-compatible entry points. +esp_err_t bmp280_init(void); +esp_err_t bmp280_read(sensor_reading_t *out, size_t max_out, size_t *out_count); + +#ifdef __cplusplus +} +#endif diff --git a/firmware/esp32p4-sensor-node/main/idf_component.yml b/firmware/esp32p4-sensor-node/main/idf_component.yml new file mode 100644 index 0000000..4f89f8c --- /dev/null +++ b/firmware/esp32p4-sensor-node/main/idf_component.yml @@ -0,0 +1,14 @@ +# Managed component dependencies for the Quantumancy sensor node. +# +# esp_wifi_remote + esp_hosted: the ESP32-P4 has NO integrated Wi-Fi/BT +# radio (confirmed against the target hardware — Waveshare +# ESP32-P4-Module-DEV-KIT, chip ESP32-P4NRW32 — which pairs it with an +# onboard ESP32-C6 co-processor reachable over SDIO). esp_wifi_remote +# provides a drop-in-compatible esp_wifi_* API that transparently proxies +# to the C6 over that SDIO link via esp_hosted's RPC layer — wifi_manager.c +# calls the normal esp_wifi_init()/esp_wifi_start() and does not need to +# know the radio isn't local. See README.md's Wi-Fi section for the SDIO +# pin assignments this depends on and why they're reserved from sensor use. +dependencies: + espressif/esp_wifi_remote: "*" + espressif/esp_hosted: "*" diff --git a/firmware/esp32p4-sensor-node/main/ld2410.c b/firmware/esp32p4-sensor-node/main/ld2410.c deleted file mode 100644 index e70a249..0000000 --- a/firmware/esp32p4-sensor-node/main/ld2410.c +++ /dev/null @@ -1,194 +0,0 @@ -// HLK-LD2410 driver — see ld2410.h for wiring/rationale and the honesty -// note below for exactly how confident to be in this file. -// -// UNVERIFIED AGAINST REAL HARDWARE, and — unlike bme280.c, which transcribes -// formulas from Bosch's official public datasheet — this frame parser is -// reconstructed from public community reverse-engineering write-ups of the -// LD2410 UART protocol (Hi-Link's official protocol document was not -// available while writing this). The header/footer magic bytes (F4 F3 F2 F1 -// / F8 F7 F6 F5) and the general envelope shape (4-byte header, 2-byte LE -// length, payload, 4-byte footer) are consistently reported across sources -// and are probably solid. The exact payload byte offsets for target -// state / distances / energies are the single least-certain part of this -// entire firmware — see the detailed note in ld2410_parse_payload() below. -// Real bring-up should verify them against a logic analyzer capture or a -// known-good reference implementation before trusting field values. - -#include -#include -#include "ld2410.h" -#include "driver/uart.h" -#include "esp_log.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" - -static const char *TAG = "ld2410"; - -#define LD2410_RX_BUF_SIZE 512 -#define LD2410_SCRATCH_SIZE 256 - -static const uint8_t FRAME_HEADER[4] = { 0xF4, 0xF3, 0xF2, 0xF1 }; -static const uint8_t FRAME_FOOTER[4] = { 0xF8, 0xF7, 0xF6, 0xF5 }; - -static bool s_ready = false; - -typedef struct { - uint8_t target_state; // bit0 = moving target, bit1 = stationary target - uint16_t moving_distance_cm; - uint8_t moving_energy; // 0-100 - uint16_t stationary_distance_cm; - uint8_t stationary_energy; // 0-100 - uint16_t detection_distance_cm; -} ld2410_frame_t; - -esp_err_t ld2410_init(void) { - uart_config_t cfg = { - .baud_rate = LD2410_UART_BAUD, - .data_bits = UART_DATA_8_BITS, - .parity = UART_PARITY_DISABLE, - .stop_bits = UART_STOP_BITS_1, - .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, - .source_clk = UART_SCLK_DEFAULT, - }; - - esp_err_t err = uart_param_config(LD2410_UART_PORT, &cfg); - if (err != ESP_OK) { - ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); - return err; - } - - // uart_set_pin(port, tx_pin, rx_pin, rts_pin, cts_pin) -- our TX GPIO - // wires to the module's RX, and our RX GPIO wires to the module's TX. - err = uart_set_pin(LD2410_UART_PORT, LD2410_UART_TX_GPIO, LD2410_UART_RX_GPIO, - UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); - if (err != ESP_OK) { - ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); - return err; - } - - err = uart_driver_install(LD2410_UART_PORT, LD2410_RX_BUF_SIZE, 0, 0, NULL, 0); - if (err != ESP_OK) { - ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); - return err; - } - - s_ready = true; - ESP_LOGI(TAG, "LD2410 UART init ok on port %d (%d baud)", LD2410_UART_PORT, LD2410_UART_BAUD); - return ESP_OK; -} - -static bool ld2410_parse_payload(const uint8_t *p, uint16_t len, ld2410_frame_t *out) { - // Basic (engineering-mode-off) target report payload, as reported by - // public LD2410 protocol write-ups: - // p[0] data type byte (0x02 observed for normal reports; not - // strictly checked here -- see honesty note above) - // p[1] 0xAA head-of-intra-frame-data marker - // p[2] target state: 0=none, 1=moving, 2=stationary, 3=both - // p[3..4] moving target distance, cm, little-endian uint16 - // p[5] moving target energy, 0-100 - // p[6..7] stationary target distance, cm, little-endian uint16 - // p[8] stationary target energy, 0-100 - // p[9..10] detection distance, cm, little-endian uint16 - // p[11] 0x55 end-of-intra-frame-data marker - // p[12] trailing byte (ignored) - // - // We only trust a frame whose head/end markers (p[1], p[11]) match -- - // a cheap sanity check against having latched onto the wrong byte - // offsets or a corrupted frame. Anything that fails this is treated as - // "no valid frame this cycle", not a hard error. - if (len < 13) { - return false; - } - if (p[1] != 0xAA || p[11] != 0x55) { - return false; - } - - out->target_state = p[2]; - out->moving_distance_cm = (uint16_t)p[3] | ((uint16_t)p[4] << 8); - out->moving_energy = p[5]; - out->stationary_distance_cm = (uint16_t)p[6] | ((uint16_t)p[7] << 8); - out->stationary_energy = p[8]; - out->detection_distance_cm = (uint16_t)p[9] | ((uint16_t)p[10] << 8); - return true; -} - -esp_err_t ld2410_read(sensor_reading_t *out, size_t max_out, size_t *out_count) { - *out_count = 0; - if (!s_ready) { - return ESP_ERR_INVALID_STATE; - } - if (max_out < 1) { - return ESP_ERR_NO_MEM; - } - - uint8_t buf[LD2410_SCRATCH_SIZE]; - // The LD2410 free-runs, pushing a report frame roughly every 100ms, so - // as long as the driver's RX ring buffer isn't empty a short read - // should find at least one complete frame already queued. - int len = uart_read_bytes(LD2410_UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(200)); - if (len < 0) { - ESP_LOGW(TAG, "uart_read_bytes error"); - return ESP_FAIL; - } - if (len == 0) { - ESP_LOGD(TAG, "no UART data from LD2410 this cycle"); - return ESP_OK; // nothing new isn't a driver failure - } - - bool parsed_any = false; - ld2410_frame_t latest = {0}; - - // Scan for the newest complete, validated frame in whatever arrived - // this cycle; keep overwriting `latest` so we report the freshest one. - for (int i = 0; i + 4 <= len; i++) { - if (memcmp(&buf[i], FRAME_HEADER, 4) != 0) { - continue; - } - if (i + 6 > len) { - break; // not enough bytes left even for the length field - } - uint16_t data_len = (uint16_t)buf[i + 4] | ((uint16_t)buf[i + 5] << 8); - size_t frame_total = 4 + 2 + (size_t)data_len + 4; - if (i + (int)frame_total > len) { - continue; // incomplete frame in this read window, skip it - } - const uint8_t *payload = &buf[i + 6]; - const uint8_t *footer = &buf[i + 6 + data_len]; - if (memcmp(footer, FRAME_FOOTER, 4) != 0) { - ESP_LOGD(TAG, "footer mismatch at offset %d, discarding candidate frame", i); - continue; - } - if (ld2410_parse_payload(payload, data_len, &latest)) { - parsed_any = true; - i += (int)frame_total - 1; // loop's i++ moves past this frame - } - } - - if (!parsed_any) { - ESP_LOGD(TAG, "no complete/valid LD2410 frame in this read window"); - return ESP_OK; - } - - memset(&out[0], 0, sizeof(out[0])); - strncpy(out[0].sensor_type, "presence", SENSOR_READING_TYPE_MAXLEN - 1); - out[0].value = (latest.target_state != 0) ? 1.0 : 0.0; - strncpy(out[0].unit, "bool", SENSOR_READING_UNIT_MAXLEN - 1); - - // This is the whole reason to prefer the LD2410 over a plain PIR: pack - // the richer distance/energy data into metadata instead of throwing it - // away, so it's available to the backend/frontend even though the - // top-level `value` stays a simple presence boolean per the contract. - cJSON *meta = cJSON_CreateObject(); - if (meta != NULL) { - cJSON_AddNumberToObject(meta, "target_state", latest.target_state); - cJSON_AddNumberToObject(meta, "moving_distance_cm", latest.moving_distance_cm); - cJSON_AddNumberToObject(meta, "moving_energy", latest.moving_energy); - cJSON_AddNumberToObject(meta, "stationary_distance_cm", latest.stationary_distance_cm); - cJSON_AddNumberToObject(meta, "stationary_energy", latest.stationary_energy); - cJSON_AddNumberToObject(meta, "detection_distance_cm", latest.detection_distance_cm); - } - out[0].metadata = meta; - - *out_count = 1; - return ESP_OK; -} diff --git a/firmware/esp32p4-sensor-node/main/ld2410.h b/firmware/esp32p4-sensor-node/main/ld2410.h deleted file mode 100644 index 3009f7c..0000000 --- a/firmware/esp32p4-sensor-node/main/ld2410.h +++ /dev/null @@ -1,53 +0,0 @@ -// HLK-LD2410 mmWave presence/distance sensor driver, over UART. -// -// Chosen over a plain PIR for this app's "believable" ethos (per the spec): -// the LD2410 reports moving-target distance/energy AND stationary-target -// distance/energy separately, not just a boolean "something moved" — richer -// signal for the anomaly pipeline (Workstream K) to work with, and it can -// tell a séance's "someone opened a door" from "the sitter shifted in their -// chair" in a way a PIR fundamentally cannot. Tradeoff: it's a more complex -// protocol than a PIR's single GPIO pin, and this driver's frame parser is -// UNVERIFIED against a real module — see README.md's honesty section and -// the parsing notes in ld2410.c. -// -// Default wiring (3.3V logic — do NOT wire directly to a 5V-logic UART): -// LD2410 TX -> ESP32 RX (LD2410_UART_RX_GPIO) -// LD2410 RX -> ESP32 TX (LD2410_UART_TX_GPIO) -// LD2410 VCC -> 5V (module runs its sensor front-end at 5V; UART logic is -// 3.3V-tolerant per module datasheet -- double check your -// specific board revision before wiring) -// LD2410 GND -> GND -// Default UART settings: 256000 baud, 8N1 (module factory default). - -#pragma once - -#include "esp_err.h" -#include "sensor_driver.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef LD2410_UART_PORT -#define LD2410_UART_PORT 1 -#endif - -#ifndef LD2410_UART_RX_GPIO -#define LD2410_UART_RX_GPIO 17 -#endif - -#ifndef LD2410_UART_TX_GPIO -#define LD2410_UART_TX_GPIO 18 -#endif - -#ifndef LD2410_UART_BAUD -#define LD2410_UART_BAUD 256000 -#endif - -// sensor_driver_t-compatible entry points. -esp_err_t ld2410_init(void); -esp_err_t ld2410_read(sensor_reading_t *out, size_t max_out, size_t *out_count); - -#ifdef __cplusplus -} -#endif diff --git a/firmware/esp32p4-sensor-node/main/rd03e.c b/firmware/esp32p4-sensor-node/main/rd03e.c new file mode 100644 index 0000000..30444ee --- /dev/null +++ b/firmware/esp32p4-sensor-node/main/rd03e.c @@ -0,0 +1,154 @@ +// Ai-Thinker RD-03E driver — see rd03e.h for wiring and honesty notes. +// +// UNVERIFIED AGAINST REAL HARDWARE, and the frame format below is +// reconstructed from a third-party bring-up write-up (electroniclinic.com's +// RD-03E/ESP32 tutorial), not Ai-Thinker's own datasheet PDF (not available +// while writing this) — treat this as the least-certain protocol detail in +// this driver. What's cross-confirmed from multiple independent sources: +// UART is 256000 baud / 8N1, and the module also has a separate, more +// complex configuration-frame protocol (0xFD 0xFC 0xFB 0xFA header / +// 0x04 0x03 0x02 0x01 footer) for calibration and firmware queries — this +// driver does NOT implement that; it only reads the module's free-running +// "simple report" output frames, which need no configuration to start +// streaming after power-up. +// +// Simple report frame, as reconstructed (5 bytes total): +// [0] 0xAA frame header +// [1] gesture code raw value, meaning not confirmed against an +// official datasheet — reported as-is in +// metadata rather than translated to a label +// that might be wrong +// [2] distance lo byte distance_cm = lo | (hi << 8), little-endian +// [3] distance hi byte +// [4..5] 0x55 0x55 frame footer +// Real bring-up should verify this against a logic analyzer capture before +// trusting field values, same as the LD2410 driver this replaced. + +#include +#include +#include "rd03e.h" +#include "driver/uart.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "rd03e"; + +#define RD03E_RX_BUF_SIZE 512 +#define RD03E_SCRATCH_SIZE 256 +#define RD03E_FRAME_LEN 5 + +static const uint8_t FRAME_HEADER = 0xAA; +static const uint8_t FRAME_FOOTER[2] = { 0x55, 0x55 }; + +static bool s_ready = false; + +typedef struct { + uint8_t gesture; + uint16_t distance_cm; +} rd03e_frame_t; + +esp_err_t rd03e_init(void) { + uart_config_t cfg = { + .baud_rate = RD03E_UART_BAUD, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + + esp_err_t err = uart_param_config(RD03E_UART_PORT, &cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + return err; + } + + // uart_set_pin(port, tx_pin, rx_pin, rts_pin, cts_pin) -- our TX GPIO + // wires to the module's RX (labeled "RX" on the module), and our RX + // GPIO wires to the module's TX output (labeled "OT1" on the module). + err = uart_set_pin(RD03E_UART_PORT, RD03E_UART_TX_GPIO, RD03E_UART_RX_GPIO, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + if (err != ESP_OK) { + ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); + return err; + } + + err = uart_driver_install(RD03E_UART_PORT, RD03E_RX_BUF_SIZE, 0, 0, NULL, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); + return err; + } + + s_ready = true; + ESP_LOGI(TAG, "RD-03E UART init ok on port %d (%d baud)", RD03E_UART_PORT, RD03E_UART_BAUD); + return ESP_OK; +} + +esp_err_t rd03e_read(sensor_reading_t *out, size_t max_out, size_t *out_count) { + *out_count = 0; + if (!s_ready) { + return ESP_ERR_INVALID_STATE; + } + if (max_out < 1) { + return ESP_ERR_NO_MEM; + } + + uint8_t buf[RD03E_SCRATCH_SIZE]; + // The module free-runs its simple report frames, so a short read should + // find at least one complete frame already queued in the RX ring buffer. + int len = uart_read_bytes(RD03E_UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(200)); + if (len < 0) { + ESP_LOGW(TAG, "uart_read_bytes error"); + return ESP_FAIL; + } + if (len == 0) { + ESP_LOGD(TAG, "no UART data from RD-03E this cycle"); + return ESP_OK; // nothing new isn't a driver failure + } + + bool parsed_any = false; + rd03e_frame_t latest = {0}; + + // Scan for the newest complete, validated frame in whatever arrived + // this cycle; keep overwriting `latest` so we report the freshest one. + for (int i = 0; i + RD03E_FRAME_LEN <= len; i++) { + if (buf[i] != FRAME_HEADER) { + continue; + } + if (memcmp(&buf[i + 3], FRAME_FOOTER, 2) != 0) { + continue; // not a real header byte, or a corrupted frame + } + latest.gesture = buf[i + 1]; + latest.distance_cm = (uint16_t)buf[i + 2] | ((uint16_t)buf[i + 3] << 8); + parsed_any = true; + i += RD03E_FRAME_LEN - 1; // loop's i++ moves past this frame + } + + if (!parsed_any) { + ESP_LOGD(TAG, "no complete/valid RD-03E frame in this read window"); + return ESP_OK; + } + + // Reported as a numeric distance (not a boolean "presence" flag, unlike + // the LD2410 this replaced) — a handheld "sense a presence at a + // distance" device wants magnitude, and it lets the backend's + // statistical anomaly detector treat "something got suddenly close" as + // the anomaly signal, the same way it already treats a temperature + // spike, rather than only firing on a coarse absent/present transition. + memset(&out[0], 0, sizeof(out[0])); + strncpy(out[0].sensor_type, "presence", SENSOR_READING_TYPE_MAXLEN - 1); + out[0].value = (double)latest.distance_cm; + strncpy(out[0].unit, "cm", SENSOR_READING_UNIT_MAXLEN - 1); + + cJSON *meta = cJSON_CreateObject(); + if (meta != NULL) { + // Raw code, not translated to a label -- see the honesty note above + // about the gesture byte's exact meaning being unconfirmed. + cJSON_AddNumberToObject(meta, "gesture_code", latest.gesture); + } + out[0].metadata = meta; + + *out_count = 1; + return ESP_OK; +} diff --git a/firmware/esp32p4-sensor-node/main/rd03e.h b/firmware/esp32p4-sensor-node/main/rd03e.h new file mode 100644 index 0000000..388b67d --- /dev/null +++ b/firmware/esp32p4-sensor-node/main/rd03e.h @@ -0,0 +1,57 @@ +// Ai-Thinker RD-03E 24GHz mmWave human-movement/gesture radar, over UART. +// +// Confirmed against the actual part in use (not the originally-assumed +// Hi-Link LD2410 — a different manufacturer with a different, incompatible +// UART protocol; do not mix up wiring or frame parsing between the two). +// Reports distance-ranged human presence (not just a boolean, per the +// module's own "Precise Ranging & Positioning" naming) plus a gesture +// code, better suited to a handheld device meant to sense "how close" and +// "what kind of movement" rather than just "something moved." +// +// Implements the sensor_driver_t interface (see sensor_driver.h). +// +// Default wiring — see README.md's wiring section for the full pinout +// table: +// RD-03E OT1 (module's UART TX) -> ESP32 RX (RD03E_UART_RX_GPIO) +// RD-03E RX (module's UART RX) -> ESP32 TX (RD03E_UART_TX_GPIO) +// RD-03E VCC -> 5V (module power; UART logic is 0-3.3V, ESP32-safe) +// RD-03E GND -> GND +// RD-03E OT2 -> not connected (reserved on the module, unused here) +// Default UART settings: 256000 baud, 8N1 (module factory default). + +#pragma once + +#include "esp_err.h" +#include "sensor_driver.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef RD03E_UART_PORT +#define RD03E_UART_PORT 1 +#endif + +// GPIO 14-19 and 54 are reserved on the target board (Waveshare +// ESP32-P4-Module-DEV-KIT) for the onboard Wi-Fi co-processor's SDIO link +// — see README.md and sdkconfig.defaults. These pins avoid that range and +// the BMP280's I2C pins (8/9). +#ifndef RD03E_UART_RX_GPIO +#define RD03E_UART_RX_GPIO 4 +#endif + +#ifndef RD03E_UART_TX_GPIO +#define RD03E_UART_TX_GPIO 5 +#endif + +#ifndef RD03E_UART_BAUD +#define RD03E_UART_BAUD 256000 +#endif + +// sensor_driver_t-compatible entry points. +esp_err_t rd03e_init(void); +esp_err_t rd03e_read(sensor_reading_t *out, size_t max_out, size_t *out_count); + +#ifdef __cplusplus +} +#endif diff --git a/firmware/esp32p4-sensor-node/main/sensor_registry.c b/firmware/esp32p4-sensor-node/main/sensor_registry.c index 4f3eedd..11119fe 100644 --- a/firmware/esp32p4-sensor-node/main/sensor_registry.c +++ b/firmware/esp32p4-sensor-node/main/sensor_registry.c @@ -9,8 +9,8 @@ #include #include "sensor_registry.h" -#include "bme280.h" -#include "ld2410.h" +#include "bmp280.h" +#include "rd03e.h" #include "esp_log.h" static const char *TAG = "sensor_registry"; @@ -19,8 +19,8 @@ static const char *TAG = "sensor_registry"; // matters only for init() sequencing (e.g. bus setup before device use); // read() order just determines readings array ordering in the POST body. static const sensor_driver_t s_drivers[] = { - { .name = "bme280", .init = bme280_init, .read = bme280_read }, - { .name = "ld2410", .init = ld2410_init, .read = ld2410_read }, + { .name = "bmp280", .init = bmp280_init, .read = bmp280_read }, + { .name = "rd03e", .init = rd03e_init, .read = rd03e_read }, // Add new drivers here, e.g.: // { .name = "my_sensor", .init = my_sensor_init, .read = my_sensor_read }, }; diff --git a/firmware/esp32p4-sensor-node/main/wifi_manager.c b/firmware/esp32p4-sensor-node/main/wifi_manager.c index fe16b18..9bbb5b1 100644 --- a/firmware/esp32p4-sensor-node/main/wifi_manager.c +++ b/firmware/esp32p4-sensor-node/main/wifi_manager.c @@ -1,5 +1,7 @@ -// Wi-Fi station-mode connection manager — see wifi_manager.h for the -// hardware caveat about ESP32-P4 not having an integrated radio. +// Wi-Fi station-mode connection manager — see wifi_manager.h for how Wi-Fi +// actually reaches the network on this target (ESP32-C6 co-processor over +// SDIO via esp_wifi_remote/esp_hosted). The esp_wifi_*/esp_netif_* calls +// below are the same ones a Wi-Fi-native chip would use. // // Structure follows ESP-IDF's own documented Wi-Fi station example pattern // (event-driven connect via WIFI_EVENT/IP_EVENT + an event group the rest diff --git a/firmware/esp32p4-sensor-node/main/wifi_manager.h b/firmware/esp32p4-sensor-node/main/wifi_manager.h index fc24a4f..6b3f80f 100644 --- a/firmware/esp32p4-sensor-node/main/wifi_manager.h +++ b/firmware/esp32p4-sensor-node/main/wifi_manager.h @@ -1,16 +1,20 @@ // Wi-Fi station-mode connection management, using the credentials in // device_config.h. // -// IMPORTANT hardware caveat (honesty note, not code): the ESP32-P4 SoC has -// no integrated 2.4GHz radio -- real Wi-Fi on P4 hardware requires a -// companion chip (e.g. ESP32-C6) wired via SDIO/SPI running "esp-hosted", -// which presents this exact same esp_wifi/esp_netif API to application code. -// This file is written against that standard API surface, so it should be -// portable unchanged to a hosted-mode P4 board or to a Wi-Fi-native target -// (e.g. `idf.py set-target esp32s3`) -- only sdkconfig / board wiring -// differs, not this code. See README.md for the full explanation. This is -// exactly the kind of thing nobody can verify without the real board, so -// it's called out explicitly rather than glossed over. +// IMPORTANT hardware note: the ESP32-P4 SoC has no integrated 2.4GHz radio. +// Confirmed against the actual target hardware (Waveshare +// ESP32-P4-Module-DEV-KIT, chip ESP32-P4NRW32): it pairs the P4 with an +// onboard ESP32-C6 co-processor over a fixed 7-pin SDIO link, and Wi-Fi is +// brought up via the `espressif/esp_wifi_remote` + `espressif/esp_hosted` +// managed components (declared in main/idf_component.yml, configured in +// sdkconfig.defaults). Those components provide a drop-in-compatible +// esp_wifi_*/esp_netif_* API — this file's esp_wifi_init()/esp_wifi_start() +// calls are unchanged from what they'd be on a Wi-Fi-native chip; only the +// component manifest and sdkconfig differ, not this code. See README.md's +// Wi-Fi section for the reserved SDIO pins (14-19, 54) and sdkconfig.defaults +// for the exact config values — still unverified against real hardware +// (nobody in this build has the physical board), but now backed by a +// specific, sourced hardware architecture rather than a general caveat. #pragma once diff --git a/firmware/esp32p4-sensor-node/sdkconfig.defaults b/firmware/esp32p4-sensor-node/sdkconfig.defaults index bd536fa..0645e8c 100644 --- a/firmware/esp32p4-sensor-node/sdkconfig.defaults +++ b/firmware/esp32p4-sensor-node/sdkconfig.defaults @@ -8,6 +8,30 @@ CONFIG_IDF_TARGET="esp32p4" +# --- Wi-Fi via the onboard ESP32-C6 co-processor (esp_wifi_remote/esp_hosted) --- +# The ESP32-P4 has no integrated Wi-Fi radio. On the target hardware +# (Waveshare ESP32-P4-Module-DEV-KIT), an ESP32-C6 co-processor is wired to +# it over SDIO — 7 fixed pins: CLK=18, CMD=19, D0=14, D1=15, D2=16, D3=17, +# RESET=54 — and esp_hosted/esp_wifi_remote handle those transparently once +# configured; wifi_manager.c's esp_wifi_init()/esp_wifi_start() calls need +# no changes. These CONFIG values match Espressif's own reference +# ESP32-P4-Function-EV-Board, whose SDIO pin assignment is identical to +# Waveshare's board (verified against two independent sources) — still, +# this is the single most important thing to confirm against the physical +# board before flashing, since it was never run against real hardware. +CONFIG_SLAVE_IDF_TARGET_ESP32C6=y +CONFIG_ESP_HOSTED_CP_TARGET_ESP32C6=y +CONFIG_ESP_HOSTED_P4_DEV_BOARD_FUNC_BOARD=y +CONFIG_WIFI_RMT_STATIC_RX_BUFFER_NUM=16 +CONFIG_WIFI_RMT_DYNAMIC_RX_BUFFER_NUM=64 +CONFIG_WIFI_RMT_DYNAMIC_TX_BUFFER_NUM=64 +CONFIG_WIFI_RMT_AMPDU_TX_ENABLED=y +CONFIG_WIFI_RMT_TX_BA_WIN=32 +CONFIG_WIFI_RMT_AMPDU_RX_ENABLED=y +CONFIG_WIFI_RMT_RX_BA_WIN=32 +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=65534 +CONFIG_LWIP_TCP_WND_DEFAULT=65534 + # The backend is served over HTTPS in production (see deploy/), so the HTTP # client needs mbedTLS's bundled CA store to verify the TLS cert. CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y