11 KiB
ESP32-S3 + HLK-LD2453 — Zone Presence Detection System
Vietnamese version available at: README.md
This project is ported from 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 /updateZonesREST endpoints,ArduinoJsonfor (de)serialization — seeZoneManager.cpp. - The WebSocket broadcast pattern (
AsyncWebSocketon/ws,ws.textAll()per frame) — seeWebServerManager.cpp. - The overall
setup()/loop()shape and Wi-Fi reconnect handling.
What changed from the original
- Removed LD2450's
rbegamer/HLK-LD2450library dependency; parsing is now handled by our ownLD2453Parser, because the byte format is different for this sensor. - From
Serial2with fixed pins →HardwareSerialon configurable pins (RADAR_RX_PIN/RADAR_TX_PINinconfig.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 callingdelay()in the acquisition path. - Every frame now carries
node_idandsequence_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_depsproject → 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
#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)
- Copy
config.example.htoconfig.h. - Leave
DIAGNOSTIC_MODE 1(the default). This disables Wi-Fi/web entirely — Serial-only. - Flash, open Serial Monitor at 115200.
- You should see:
Target lines will currently always print
LD2453 connected UART: 256000 ...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:
- In
config.h, setDEBUG_RADAR_RAW 1. - 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) - 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()inLD2453Parser.cpp. The exact insertion point is marked with a comment block.
Stage 2: Full firmware (once the parser is confirmed)
- Set
DIAGNOSTIC_MODE 0inconfig.h. - Fill in
WIFI_SSID/WIFI_PASSWORD. - 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)
{
"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 namenode_id: Radar node ID (fromNODE_IDinconfig.h)sequence: Frame sequence number, incrementing per frametimestamp: Time in millis() at frame completiontargets[]: 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 slotvalid: true if this target has valid datax,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:
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
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):
source venv/bin/activate
python3 orange_pi_visualizer.py <ESP32_IP>
Headless mode (SSH, no monitor needed):
python3 orange_pi_visualizer.py <ESP32_IP> --headless
See detailed guide at: 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 inWebServerManager::begin(), which runs once duringsetup()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
.inowires them together.