# 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:///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 ``` Headless mode (SSH, no monitor needed): ```bash python3 orange_pi_visualizer.py --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.