# ESP32-S3 + HLK-LD2453 — Zone Presence Detection (port of ESP32-LD2450) Ported from [thanhtantran/ESP32-LD2450](https://github.com/thanhtantran/ESP32-LD2450), targeting: - **MCU:** ESP32-S3 (Arduino framework, plain Arduino IDE — no PlatformIO) - **Radar:** Hi-Link **HLK-LD2453** (24 GHz, 1TX+2RX, up to 3 targets) ## ⚠️ Current status: radar parser is a stub `LD2453Parser.cpp`'s byte-level frame decoding is **not implemented yet**. The official HLK-LD2453 "Communication Protocol" manual section (frame header/tail, per-target byte layout, sign convention, speed encoding) could not be sourced from a verifiable source at the time this was written — see "Confirming the protocol" below. Everything else (Wi-Fi, REST API, WebSocket, zone logic, non-blocking UART acquisition, raw hex capture) is fully implemented and ready to go. ## What's reused from the original ESP32-LD2450 project - The zone model: up to 3 rectangles in millimeters, `GET /zones` / `POST /updateZones` REST endpoints, `ArduinoJson` for (de)serialization — see `ZoneManager.cpp`. - The WebSocket broadcast pattern (`AsyncWebSocket` on `/ws`, `ws.textAll()` per frame) — see `WebServerManager.cpp`. - The overall setup()/loop() shape and Wi-Fi reconnect handling. ## What changed - LD2450's `rbegamer/HLK-LD2450` library dependency is gone; parsing is now our own `LD2453Parser`, because the byte format is different for this sensor. - `Serial2` with fixed pins → `HardwareSerial` on **configurable** pins (`RADAR_RX_PIN` / `RADAR_TX_PIN` in `config.h`). - Blocking `delay()`-based sensor "reopen on failure" hack removed; radar acquisition is now a non-blocking ring-buffer state machine that tolerates partial/corrupted frames without ever calling `delay()` in the acquisition path. - Every frame now carries `node_id` and a `sequence_number`, so this node can later feed a multi-radar fusion setup (`RADAR_A/B/C` → UDP → Orange Pi) without touching the parser again. - Plain PlatformIO `.cpp`/lib_deps project → standard Arduino IDE sketch folder. ## Wiring ``` LD2453 ESP32-S3 V -> 3V3 G -> GND T/TX -> RADAR_RX_PIN (default GPIO18) R/RX -> RADAR_TX_PIN (default GPIO17) ``` Change `RADAR_RX_PIN` / `RADAR_TX_PIN` in `config.h` to match your wiring — any two free GPIOs that support UART on your ESP32-S3 board will work, since the ESP32 Arduino core remaps UART pins in software. Do **not** use SoftwareSerial — `HardwareSerial` is required at 256000 baud. ## Required Arduino libraries Install via Library Manager (exact names to search for): | Library | Used for | Notes | |---|---|---| | `ArduinoJson` (by Benoit Blanchon) | REST API + WebSocket JSON | v7.x — code uses the current `JsonDocument` API (not the deprecated `StaticJsonDocument`) | | `ESP Async WebServer` (ESP32Async fork) | HTTP + WebSocket server | Only needed for stage 2 (`DIAGNOSTIC_MODE 0`) | | `AsyncTCP` | dependency of ESP Async WebServer | matching fork/version to the WebServer library above | Board package: **esp32 by Espressif Systems**, board = your specific ESP32-S3 dev board. Confirmed building clean (no warnings) with: esp32 core 3.3.11, ArduinoJson (JsonDocument API), ESP_Async_WebServer + Async_TCP. ## Project layout ``` ESP32_LD2453/ ESP32_LD2453.ino - setup()/loop(), diagnostic printing LD2453Parser.h/.cpp - UART acquisition + frame parsing (STUBBED, see above) ZoneManager.h/.cpp - rectangular zone occupancy logic WebServerManager.h/.cpp - Wi-Fi, REST API, WebSocket broadcast config.example.h - copy to config.h and edit README.md ``` ## Stage 1: diagnostic mode (do this first) 1. Copy `config.example.h` to `config.h`. 2. Leave `DIAGNOSTIC_MODE 1` (the default). This disables Wi-Fi/web entirely — Serial-only. 3. Flash, open Serial Monitor at 115200. 4. You should see: ``` LD2453 connected UART: 256000 ... ``` Target lines will currently always print `INVALID` — expected, since the parser is stubbed. This stage exists so you can validate wiring/baud/board bring-up independent of the parser. ## Confirming the protocol Until `LD2453Parser`'s real frame format is filled in, use it as a raw-capture tool: 1. In `config.h`, set `DEBUG_RADAR_RAW 1`. 2. Flash, open Serial Monitor at 115200, and let the LD2453 see some motion. You'll get a hex dump of every raw byte the module sends, e.g.: ``` [RAW] AA FF 03 00 4B 01 36 08 0C 00 ... (len=30) ``` 3. Share that capture (and, if you can get it, the actual "5. Communication Protocol" section of the official HLK-LD2453 user manual — Hi-Link's own download link for it is currently mislabeled and points at the LD2450 manual instead) so the header, tail, per-target byte layout, and sign/speed encoding can be confirmed and dropped into `tryParseFrame()` in `LD2453Parser.cpp`. The exact insertion point is marked with a comment block. ## Stage 2: full firmware (once the parser is confirmed) 1. Set `DIAGNOSTIC_MODE 0` in `config.h`. 2. Fill in `WIFI_SSID` / `WIFI_PASSWORD`. 3. Flash. The board connects to Wi-Fi, starts the HTTP server, and broadcasts one JSON message per radar frame over `/ws`. ### WebSocket data format (`ws:///ws`) ```json { "sensor": "LD2453", "node_id": "RADAR_A", "sequence": 12345, "timestamp": 12345678, "targets": [ { "id": 1, "valid": true, "x": -820, "y": 2840, "speed": 12 }, { "id": 2, "valid": true, "x": 1350, "y": 4120, "speed": -4 } ] } ``` `id` is the radar's target *slot* (1–3), not a persistent person identity — a target disappearing and reappearing may land in a different slot. ### REST API - `GET /zones` — current zone rectangles + live occupancy - `POST /updateZones` — update one or more zones (same shape as `GET /zones`, partial updates supported) - `GET /status` — basic health check (uptime) ## Multi-radar future work (not implemented yet) Every `RadarFrame` already carries `node_id` and `sequence_number` so that, later, this same parser output can be forwarded over UDP to an Orange Pi doing sensor fusion across `RADAR_A/B/C`, without touching `LD2453Parser` again. No UDP/MQTT/cloud code has been added — this firmware only does local Wi-Fi + WebSocket, as requested. ## Code quality notes - No `delay()` in the radar acquisition path (`LD2453Parser::update()`, `feedByte()`) — only the one-time Wi-Fi connect retry in `WebServerManager::begin()`, which runs once during `setup()` before acquisition starts. - No dynamic allocation in the parser (fixed-size ring buffer). - Radar acquisition, zone logic, and networking are in separate classes/files and don't call into each other directly — the `.ino` wires them together.