272 lines
11 KiB
Markdown
272 lines
11 KiB
Markdown
# ESP32-S3 + HLK-LD2453 — Zone Presence Detection System
|
||
|
||
> **Vietnamese version** available at: [README.md](README.md)
|
||
|
||
This project is ported from [thanhtantran/ESP32-LD2450](https://github.com/thanhtantran/ESP32-LD2450), targeting the following hardware:
|
||
|
||
- **MCU:** ESP32-S3 (Arduino framework, plain Arduino IDE — no PlatformIO)
|
||
- **Radar:** Hi-Link **HLK-LD2453** (24 GHz, 1TX+2RX, up to 3 targets simultaneously)
|
||
|
||
---
|
||
|
||
## ⚠️ Current status: Radar parser is a stub
|
||
|
||
Byte-level frame decoding in `LD2453Parser.cpp` is **not implemented yet**.
|
||
A verifiable source for the official HLK-LD2453 "Communication Protocol" section (frame header/tail, per-target byte layout, sign convention, speed encoding) could not be found 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 from the original
|
||
|
||
- Removed LD2450's `rbegamer/HLK-LD2450` library dependency; parsing is now handled by our own `LD2453Parser`, because the byte format is different for this sensor.
|
||
- From `Serial2` with fixed pins → `HardwareSerial` on **configurable** pins (`RADAR_RX_PIN` / `RADAR_TX_PIN` in `config.h`).
|
||
- Removed the blocking `delay()`-based sensor "reopen on failure" hack; radar acquisition now runs as 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 `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.
|
||
- From PlatformIO `.cpp`/`lib_deps` project → standard Arduino IDE sketch folder.
|
||
|
||
---
|
||
|
||
## Wiring
|
||
|
||
### LD2453 Radar
|
||
|
||
```
|
||
LD2453 ESP32-S3
|
||
V -> 3V3
|
||
G -> GND
|
||
T/TX -> RADAR_RX_PIN (default GPIO18)
|
||
R/RX -> RADAR_TX_PIN (default GPIO17)
|
||
```
|
||
|
||
### OLED Display I2C 128x64
|
||
|
||
```
|
||
OLED ESP32-S3
|
||
VCC -> 3V3
|
||
GND -> GND
|
||
SCL -> GPIO7 (OLED_SCL_PIN)
|
||
SDA -> GPIO6 (OLED_SDA_PIN)
|
||
```
|
||
|
||
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 (search for these exact names):
|
||
|
||
| 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 |
|
||
| `Adafruit GFX Library` | Graphics library for OLED | Used with Adafruit SSD1306 |
|
||
| `Adafruit SSD1306` | Driver for OLED 128x64 I2C display | Required if `OLED_ENABLED 1` |
|
||
|
||
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
|
||
|
||
```
|
||
esp32s3-ld2453/
|
||
├── esp32s3-ld2453/
|
||
│ ├── esp32s3-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
|
||
│ ├── OLEDDisplay.h/.cpp — OLED I2C 128x64 display manager
|
||
│ ├── config.example.h — Copy to config.h and edit
|
||
│ └── config.h — Configuration file (not committed to git)
|
||
├── orangepi-server/
|
||
│ ├── ORANGE_PI_GUIDE.md — Detailed guide for Orange Pi
|
||
│ ├── orange_pi_visualizer.py — Python script to visualize radar data
|
||
│ └── orange_pi_requirements.txt
|
||
└── README-en.md
|
||
```
|
||
|
||
---
|
||
|
||
## OLED Display Features
|
||
|
||
The OLED 128x64 display shows the following information:
|
||
|
||
### When DIAGNOSTIC_MODE = 1 (Serial Debug Mode)
|
||
- **Boot screen**: Displays logo and Node ID
|
||
- **Diagnostic mode notification**: Indicates Serial-only mode is active
|
||
- **Radar statistics every 5 seconds**:
|
||
- Number of bytes received from LD2453
|
||
- Number of parsed frames
|
||
- Number of parse errors
|
||
- Packets per second (PPS)
|
||
- Number of detected targets
|
||
|
||
### When DIAGNOSTIC_MODE = 0 (Full WiFi Mode)
|
||
- **Connecting WiFi**: Shows SSID being connected
|
||
- **WiFi connected**: Shows IP address
|
||
- **Realtime detection**:
|
||
- Number of detected targets (T:0-3)
|
||
- Status of 3 zones (YES/NO)
|
||
|
||
### OLED configuration in config.h
|
||
|
||
```cpp
|
||
#define OLED_ENABLED 1 // 1 = enable OLED, 0 = disable
|
||
#define OLED_SDA_PIN 6 // SDA pin
|
||
#define OLED_SCL_PIN 7 // SCL pin
|
||
#define OLED_I2C_ADDR 0x3C // I2C address (typically 0x3C or 0x3D)
|
||
```
|
||
|
||
**Note**: If the display does not work, try changing `OLED_I2C_ADDR` from `0x3C` to `0x3D`.
|
||
|
||
---
|
||
|
||
## 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 }
|
||
]
|
||
}
|
||
```
|
||
|
||
Field explanations:
|
||
- `sensor`: Sensor type name
|
||
- `node_id`: Radar node ID (from `NODE_ID` in `config.h`)
|
||
- `sequence`: Frame sequence number, incrementing per frame
|
||
- `timestamp`: Time in millis() at frame completion
|
||
- `targets[]`: Array of detected targets (max 3):
|
||
- `id`: The radar's target *slot* (1–3), **NOT** a persistent person identity — a target disappearing and reappearing may land in a different slot
|
||
- `valid`: true if this target has valid data
|
||
- `x`, `y`: Coordinates in millimeters (radar coordinate system)
|
||
- `speed`: Speed in cm/s (+/− sign indicates direction)
|
||
|
||
### REST API
|
||
|
||
| Endpoint | Method | Description |
|
||
|---|---|---|
|
||
| `/zones` | GET | Returns current zone rectangles + live occupancy status |
|
||
| `/updateZones` | POST | Update one or more zones (same format as `GET /zones`, partial updates supported |
|
||
| `/status` | GET | Basic health check (uptime) |
|
||
|
||
Example zone update with `curl`:
|
||
|
||
```bash
|
||
curl -X POST http://192.168.1.100/updateZones \
|
||
-H "Content-Type: application/json" \
|
||
-d '[
|
||
{"id":1,"x1":-3000,"y1":500,"x2":-1,"y2":3000},
|
||
{"id":2,"x1":1,"y1":500,"x2":3000,"y2":3000},
|
||
{"id":3,"x1":-3000,"y1":3001,"x2":3000,"y2":5000}
|
||
]'
|
||
```
|
||
|
||
---
|
||
|
||
## Orange Pi Visualizer
|
||
|
||
The `orangepi-server/` folder contains a Python script to receive data from the ESP32 via WebSocket and display it in realtime on an Orange Pi (or any computer with Python 3.8+).
|
||
|
||
### Key features
|
||
- Connect to ESP32 via WebSocket, receive realtime radar frames
|
||
- Draw zones (3 rectangles) and target positions (max 3)
|
||
- Display coordinates, speed, distance, and angle for each target
|
||
- Supports 2 modes:
|
||
- **GUI (Tkinter)**: Graphics display, requires a monitor
|
||
- **Headless (Console)**: No monitor required, runs over SSH
|
||
|
||
### Quick setup
|
||
|
||
```bash
|
||
sudo apt update
|
||
sudo apt install -y python3-tk python3-pip python3-venv
|
||
|
||
cd ~/orangepi-server
|
||
python3 -m venv venv
|
||
source venv/bin/activate
|
||
pip install -r orange_pi_requirements.txt
|
||
```
|
||
|
||
### Running the program
|
||
|
||
GUI mode (with monitor):
|
||
```bash
|
||
source venv/bin/activate
|
||
python3 orange_pi_visualizer.py <ESP32_IP>
|
||
```
|
||
|
||
Headless mode (SSH, no monitor needed):
|
||
```bash
|
||
python3 orange_pi_visualizer.py <ESP32_IP> --headless
|
||
```
|
||
|
||
See detailed guide at: [ORANGE_PI_GUIDE.md](orangepi-server/ORANGE_PI_GUIDE.md)
|
||
|
||
---
|
||
|
||
## 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 originally.
|
||
|
||
---
|
||
|
||
## 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.
|