Files
esp32s3-ld2453/README-en.md
T
2026-09-11 17:28:32 +07:00

11 KiB
Raw Blame History

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 /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

#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)

{
  "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:

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 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.