Files
2026-09-11 17:28:32 +07:00

272 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.