174 lines
6.7 KiB
Markdown
174 lines
6.7 KiB
Markdown
# 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 |
|
||
| `ESP Async WebServer` (by me-no-dev / ESP32Async fork) | HTTP + WebSocket server | Only needed for stage 2 (`DIAGNOSTIC_MODE 0`). Check current ESP32-S3/Arduino-core-3.x compatibility for whichever fork you install — this ecosystem has had breaking forks; if it doesn't build, the `ESP32Async/ESPAsyncWebServer` fork is the actively maintained one as of this writing. |
|
||
| `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.
|
||
|
||
## 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://<ESP32_IP>/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.
|