From e76a9c1e9bae2271fec0b4dfb204aec4962126ad Mon Sep 17 00:00:00 2001 From: Tony Tran Date: Fri, 11 Sep 2026 12:04:46 +0700 Subject: [PATCH] add files --- LD2453Parser.cpp | 133 +++++++++++++++++++++++++++++++++++++++++++ LD2453Parser.h | 110 +++++++++++++++++++++++++++++++++++ WebServerManager.cpp | 119 ++++++++++++++++++++++++++++++++++++++ WebServerManager.h | 45 +++++++++++++++ ZoneManager.cpp | 82 ++++++++++++++++++++++++++ ZoneManager.h | 46 +++++++++++++++ config.example.h | 51 +++++++++++++++++ config.h | 51 +++++++++++++++++ esp32-s3-ld2453.ino | 115 +++++++++++++++++++++++++++++++++++++ 9 files changed, 752 insertions(+) create mode 100644 LD2453Parser.cpp create mode 100644 LD2453Parser.h create mode 100644 WebServerManager.cpp create mode 100644 WebServerManager.h create mode 100644 ZoneManager.cpp create mode 100644 ZoneManager.h create mode 100644 config.example.h create mode 100644 config.h create mode 100644 esp32-s3-ld2453.ino diff --git a/LD2453Parser.cpp b/LD2453Parser.cpp new file mode 100644 index 0000000..4c5b10d --- /dev/null +++ b/LD2453Parser.cpp @@ -0,0 +1,133 @@ +// ===================================================================== +// LD2453Parser.cpp +// +// See the big comment at the top of LD2453Parser.h before touching +// this file. The acquisition/debug layer below is fully functional; +// tryParseFrame() is intentionally stubbed pending the official +// HLK-LD2453 protocol. +// ===================================================================== +#include "LD2453Parser.h" + +#if !defined(ARDUINO_ARCH_ESP32) +#warning "LD2453Parser was written/tested against the ESP32 Arduino core. Verify HardwareSerial pin-remap behavior on other cores." +#endif + +// --------------------------------------------------------------- +// PLACEHOLDER PROTOCOL CONSTANTS +// +// None of the values below are confirmed. They exist only so the +// state machine in tryParseFrame() has something concrete to compare +// against once you fill them in from the real manual or from a +// DEBUG_RADAR_RAW capture. Do not rely on these for real parsing. +// --------------------------------------------------------------- +// static const uint8_t FRAME_HEADER[] = { 0x00, 0x00, 0x00, 0x00 }; // TODO: confirm +// static const uint8_t FRAME_TAIL[] = { 0x00, 0x00 }; // TODO: confirm +// static const size_t TARGET_RECORD_SIZE = 0; // bytes per target, TODO: confirm +// static const size_t MAX_TARGETS = 3; + +void LD2453Parser::begin(HardwareSerial &serial, int rxPin, int txPin, uint32_t baud, + const char *nodeId, bool rawDebug) { + _serial = &serial; + _rawDebug = rawDebug; + _nodeId = nodeId; + _bufferLen = 0; + _sequenceCounter = 0; + _stats = LD2453Stats{}; + _ppsWindowStart = millis(); + _ppsWindowCount = 0; + + // HardwareSerial::begin(baud, config, rxPin, txPin) — ESP32 Arduino + // core supports remapping RX/TX to any capable GPIO. 8N1 assumed; + // confirm against the official manual. + _serial->begin(baud, SERIAL_8N1, rxPin, txPin); +} + +void LD2453Parser::dumpRawHex(const uint8_t *data, size_t len) { + Serial.print(F("[RAW] ")); + for (size_t i = 0; i < len; i++) { + if (data[i] < 0x10) Serial.print('0'); + Serial.print(data[i], HEX); + Serial.print(' '); + } + Serial.print(F(" (len=")); + Serial.print(len); + Serial.println(')'); +} + +void LD2453Parser::feedByte(uint8_t b) { + if (_bufferLen >= RING_BUFFER_SIZE) { + // Buffer overrun with no recognized frame boundary — drop the + // oldest half so we can keep tolerating a stuck/garbage stream + // without ever blocking or allocating. + memmove(_ringBuffer, _ringBuffer + RING_BUFFER_SIZE / 2, RING_BUFFER_SIZE / 2); + _bufferLen = RING_BUFFER_SIZE / 2; + _stats.parseErrors++; + } + _ringBuffer[_bufferLen++] = b; +} + +bool LD2453Parser::update() { + if (!_serial) return false; + + bool gotFrame = false; + + // Drain whatever is available right now — non-blocking, no delay(). + // While rawDebug is on, dump exactly the newly-arrived bytes as + // hex so you can capture real traffic to reverse-engineer the + // frame format, independent of whether tryParseFrame() (still a + // stub) recognizes anything. + size_t newBytesStart = _bufferLen; + while (_serial->available() > 0) { + uint8_t b = (uint8_t)_serial->read(); + feedByte(b); + _stats.bytesReceived++; + } + if (_rawDebug && _bufferLen > newBytesStart) { + dumpRawHex(_ringBuffer + newBytesStart, _bufferLen - newBytesStart); + } + + // Try to pull as many complete frames out of the buffer as + // possible in this call (in case several arrived since the last + // loop() iteration). + while (tryParseFrame()) { + gotFrame = true; + _stats.framesParsed++; + _ppsWindowCount++; + } + + // Update packets-per-second once a second, non-blocking. + uint32_t now = millis(); + if (now - _ppsWindowStart >= 1000) { + _stats.packetsPerSecond = _ppsWindowCount * 1000.0f / (now - _ppsWindowStart); + _ppsWindowStart = now; + _ppsWindowCount = 0; + } + + return gotFrame; +} + +bool LD2453Parser::tryParseFrame() { + // ----------------------------------------------------------- + // STUB: real frame sync/parse goes here once the official + // HLK-LD2453 protocol is confirmed. Suggested shape, mirroring + // the LD2410/LD2450 family's approach (adapt once verified): + // + // 1. Scan _ringBuffer for FRAME_HEADER. + // 2. If found but fewer than (header + declared length + tail) + // bytes are available yet, return false and wait for more + // bytes next call (do NOT block). + // 3. Verify FRAME_TAIL at the expected offset. + // 4. If rawDebug, dumpRawHex() the whole candidate frame. + // 5. Decode each target record into RadarTarget (x_mm, y_mm, + // speed, and distance/angle only if the protocol actually + // carries them). + // 6. Fill _lastFrame (timestamp_ms = millis(), sequence_number + // = ++_sequenceCounter, node_id = _nodeId, targetCount). + // 7. memmove() the consumed bytes out of _ringBuffer. + // 8. Return true. + // + // Until then, this always returns false so callers never receive + // fabricated coordinates. + // ----------------------------------------------------------- + return false; +} diff --git a/LD2453Parser.h b/LD2453Parser.h new file mode 100644 index 0000000..3168c8d --- /dev/null +++ b/LD2453Parser.h @@ -0,0 +1,110 @@ +// ===================================================================== +// LD2453Parser.h +// +// UART frame acquisition + parsing for the Hi-Link HLK-LD2453 radar. +// +// *** IMPORTANT / READ THIS FIRST *** +// The byte-level frame format implemented in LD2453Parser.cpp is a +// PLACEHOLDER. As of this writing, the official HLK-LD2453 "5. +// Communication Protocol" manual section (frame header, frame tail, +// per-target record layout, byte order, sign convention, speed +// encoding) could not be sourced from a verifiable primary source. +// +// Rather than guess and silently emit plausible-but-wrong X/Y/speed +// numbers, tryParseFrame() is stubbed to always fail until the real +// constants below are filled in from the official manual or from a +// reverse-engineered capture (see DEBUG_RADAR_RAW in config.h and the +// README "Confirming the protocol" section). +// +// What IS implemented and fully working right now: +// - Non-blocking UART byte acquisition (ring buffer, no delay()) +// - Raw hex frame dumping (DEBUG_RADAR_RAW) for protocol capture +// - Packets-per-second / parse-error counters +// - The public data structures your application code should use +// (RadarTarget, RadarFrame) so ZoneManager / WebServerManager / +// your .ino never need to change once the real parser is dropped +// in below. +// ===================================================================== +#pragma once + +#include + +// --------------------------------------------------------------- +// Public data structures +// --------------------------------------------------------------- + +struct RadarTarget { + bool valid; + int16_t x_mm; + int16_t y_mm; + int16_t speed; // cm/s, sign = direction (towards/away), per LD2450-family convention — UNCONFIRMED for LD2453 + float distance_mm; // only populated if the confirmed protocol provides it directly; else derived from x/y + float angle_deg; // only populated if the confirmed protocol provides it directly +}; + +struct RadarFrame { + uint32_t timestamp_ms; // millis() at time of frame completion + uint32_t sequence_number; // increments per frame, for multi-radar fusion later + const char *node_id; // NODE_ID from config.h + RadarTarget targets[3]; + uint8_t targetCount; // number of *valid* targets in this frame (0-3) +}; + +// Simple acquisition/parse statistics, useful for the diagnostic +// serial output requested for stage 1. +struct LD2453Stats { + uint32_t framesParsed = 0; + uint32_t parseErrors = 0; + uint32_t bytesReceived = 0; + uint32_t lastFrameLength = 0; + float packetsPerSecond = 0.0f; +}; + +// --------------------------------------------------------------- +// Parser class +// --------------------------------------------------------------- +class LD2453Parser { +public: + // rawDebug: when true, every candidate frame's raw bytes are + // printed as hex to Serial. Drive this from DEBUG_RADAR_RAW. + void begin(HardwareSerial &serial, int rxPin, int txPin, uint32_t baud, + const char *nodeId, bool rawDebug = false); + + // Call every loop() iteration. Non-blocking: reads whatever bytes + // are available, feeds the internal state machine, and returns + // true exactly once when a *new, fully parsed* frame is ready + // (retrieve it with getLastFrame()). + // + // NOTE: with tryParseFrame() stubbed out (see .cpp), this will + // currently never return true — see the header comment above. + bool update(); + + const RadarFrame &getLastFrame() const { return _lastFrame; } + const LD2453Stats &getStats() const { return _stats; } + +private: + HardwareSerial *_serial = nullptr; + bool _rawDebug = false; + const char *_nodeId = "RADAR_A"; + + static const size_t RING_BUFFER_SIZE = 256; + uint8_t _ringBuffer[RING_BUFFER_SIZE]; + size_t _bufferLen = 0; + + RadarFrame _lastFrame{}; + LD2453Stats _stats{}; + uint32_t _sequenceCounter = 0; + + uint32_t _ppsWindowStart = 0; + uint32_t _ppsWindowCount = 0; + + void feedByte(uint8_t b); + void dumpRawHex(const uint8_t *data, size_t len); + + // Returns true and consumes `len` bytes from _ringBuffer if a + // valid frame was found and parsed into _lastFrame. Returns + // false (and consumes 0 bytes) otherwise. + // + // *** STUBBED — see LD2453Parser.cpp *** + bool tryParseFrame(); +}; diff --git a/WebServerManager.cpp b/WebServerManager.cpp new file mode 100644 index 0000000..2d18db3 --- /dev/null +++ b/WebServerManager.cpp @@ -0,0 +1,119 @@ +// ===================================================================== +// WebServerManager.cpp +// ===================================================================== +#include "WebServerManager.h" +#include + +WebServerManager::WebServerManager(ZoneManager &zoneManager) + : _zoneManager(zoneManager), _server(80), _ws("/ws") { +} + +void WebServerManager::onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, + AwsEventType type, void *arg, uint8_t *data, size_t len) { + if (type == WS_EVT_CONNECT) { + Serial.printf("[WS] client #%u connected from %s\n", client->id(), + client->remoteIP().toString().c_str()); + } else if (type == WS_EVT_DISCONNECT) { + Serial.printf("[WS] client #%u disconnected\n", client->id()); + } +} + +void WebServerManager::setupRoutes() { + _ws.onEvent(onWsEvent); + _server.addHandler(&_ws); + + // GET /zones -> current zone definitions + occupancy + _server.on("/zones", HTTP_GET, [this](AsyncWebServerRequest *request) { + request->send(200, "application/json", _zoneManager.zonesToJson()); + }); + + // POST /updateZones -> replace zone definitions (partial updates + // allowed; see ZoneManager::zonesFromJson) + _server.on( + "/updateZones", HTTP_POST, [](AsyncWebServerRequest *request) {}, + nullptr, + [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + if (_zoneManager.zonesFromJson(data, len)) { + request->send(200, "application/json", + "{\"status\":\"success\",\"message\":\"Zones updated\"}"); + } else { + request->send(400, "application/json", + "{\"status\":\"error\",\"message\":\"Invalid JSON\"}"); + } + }); + + // Basic health check, handy when bringing up a new node in the + // future multi-radar setup. + _server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) { + StaticJsonDocument<128> doc; + doc["status"] = "ok"; + doc["uptime_ms"] = millis(); + String out; + serializeJson(doc, out); + request->send(200, "application/json", out); + }); +} + +void WebServerManager::begin(const char *ssid, const char *password) { + Serial.print("Connecting to "); + Serial.println(ssid); + WiFi.begin(ssid, password); + + // Matches the original project's blocking connect-on-boot + // behavior. This only runs once at startup, not in the main + // loop, so it doesn't violate the "no long delay() in the + // acquisition path" requirement — radar acquisition hasn't + // started yet at this point in setup(). + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println(); + Serial.print("WiFi connected, IP: "); + Serial.println(WiFi.localIP()); + + setupRoutes(); + _server.begin(); + Serial.println("HTTP server started."); +} + +void WebServerManager::loop() { + if (WiFi.status() != WL_CONNECTED) { + // Non-blocking reconnect kick. We don't retry-loop with + // delay() here (unlike begin()) so radar acquisition in the + // main loop is never stalled by a flaky network. + static uint32_t lastAttempt = 0; + uint32_t now = millis(); + if (now - lastAttempt > 5000) { + Serial.println("WiFi disconnected, retrying..."); + WiFi.reconnect(); + lastAttempt = now; + } + } + _ws.cleanupClients(); +} + +void WebServerManager::broadcastFrame(const RadarFrame &frame) { + if (_ws.count() == 0) return; // skip serialization work if nobody's listening + + StaticJsonDocument<512> doc; + doc["sensor"] = "LD2453"; + doc["node_id"] = frame.node_id; + doc["sequence"] = frame.sequence_number; + doc["timestamp"] = frame.timestamp_ms; + + JsonArray targets = doc.createNestedArray("targets"); + for (uint8_t i = 0; i < frame.targetCount && i < 3; i++) { + const RadarTarget &t = frame.targets[i]; + JsonObject jt = targets.createNestedObject(); + jt["id"] = i + 1; // radar slot index, NOT a persistent person ID + jt["valid"] = t.valid; + jt["x"] = t.x_mm; + jt["y"] = t.y_mm; + jt["speed"] = t.speed; + } + + String out; + serializeJson(doc, out); + _ws.textAll(out); +} diff --git a/WebServerManager.h b/WebServerManager.h new file mode 100644 index 0000000..7e9139f --- /dev/null +++ b/WebServerManager.h @@ -0,0 +1,45 @@ +// ===================================================================== +// WebServerManager.h +// +// Wi-Fi + HTTP REST API + WebSocket live feed, ported from the +// ESP32-LD2450 project's AsyncWebServer/AsyncWebSocket setup. +// Protocol-agnostic: only ever touches RadarFrame/RadarTarget and +// ZoneManager, never raw radar bytes. +// ===================================================================== +#pragma once + +#include +#include +#include +#include "LD2453Parser.h" +#include "ZoneManager.h" + +class WebServerManager { +public: + WebServerManager(ZoneManager &zoneManager); + + // Blocks briefly (via delay in the WiFi connect retry loop only, + // as in the original project) until Wi-Fi is up, then starts the + // HTTP + WebSocket server. Call once from setup(). + void begin(const char *ssid, const char *password); + + // Non-blocking. Call every loop() iteration: reconnects Wi-Fi if + // dropped, and prunes disconnected WebSocket clients. + void loop(); + + // Broadcasts one normalized JSON payload (see README "Web Data + // Format") to all connected WebSocket clients for this frame. + // No-op if no clients are connected. + void broadcastFrame(const RadarFrame &frame); + + bool isWifiConnected() const { return WiFi.status() == WL_CONNECTED; } + +private: + ZoneManager &_zoneManager; + AsyncWebServer _server; + AsyncWebSocket _ws; + + void setupRoutes(); + static void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, + AwsEventType type, void *arg, uint8_t *data, size_t len); +}; diff --git a/ZoneManager.cpp b/ZoneManager.cpp new file mode 100644 index 0000000..db3ea01 --- /dev/null +++ b/ZoneManager.cpp @@ -0,0 +1,82 @@ +// ===================================================================== +// ZoneManager.cpp +// ===================================================================== +#include "ZoneManager.h" +#include + +ZoneManager::ZoneManager() { + // Same default layout as the original ESP32-LD2450 project + // (three thirds of the room, roughly), in millimeters. Adjust to + // taste or override via POST /updateZones once the web UI is up. + _zones[0] = { -4000, 1, -1, 4000 }; + _zones[1] = { 1, 1, 4000, 4000 }; + _zones[2] = { -4001, 4001, 4001, 6000 }; + for (uint8_t i = 0; i < MAX_ZONES; i++) _occupied[i] = false; +} + +bool ZoneManager::pointInZone(int x, int y, const Zone &z) { + return x >= z.x1 && x <= z.x2 && y >= z.y1 && y <= z.y2; +} + +void ZoneManager::update(const RadarFrame &frame) { + bool temp[MAX_ZONES] = { false, false, false }; + + for (uint8_t t = 0; t < frame.targetCount && t < 3; t++) { + const RadarTarget &target = frame.targets[t]; + if (!target.valid) continue; + + for (uint8_t z = 0; z < MAX_ZONES; z++) { + if (pointInZone(target.x_mm, target.y_mm, _zones[z])) { + temp[z] = true; + } + } + } + + for (uint8_t z = 0; z < MAX_ZONES; z++) _occupied[z] = temp[z]; +} + +bool ZoneManager::isZoneOccupied(uint8_t zoneIndex) const { + if (zoneIndex >= MAX_ZONES) return false; + return _occupied[zoneIndex]; +} + +Zone ZoneManager::getZone(uint8_t zoneIndex) const { + if (zoneIndex >= MAX_ZONES) return Zone{0, 0, 0, 0}; + return _zones[zoneIndex]; +} + +void ZoneManager::setZone(uint8_t zoneIndex, const Zone &zone) { + if (zoneIndex >= MAX_ZONES) return; + _zones[zoneIndex] = zone; +} + +String ZoneManager::zonesToJson() const { + StaticJsonDocument<512> doc; + JsonArray arr = doc.to(); + for (uint8_t i = 0; i < MAX_ZONES; i++) { + JsonObject z = arr.createNestedObject(); + z["id"] = i + 1; + z["x1"] = _zones[i].x1; + z["y1"] = _zones[i].y1; + z["x2"] = _zones[i].x2; + z["y2"] = _zones[i].y2; + z["occupied"] = _occupied[i]; + } + String out; + serializeJson(doc, out); + return out; +} + +bool ZoneManager::zonesFromJson(const uint8_t *data, size_t len) { + StaticJsonDocument<512> doc; + DeserializationError err = deserializeJson(doc, data, len); + if (err) return false; + + for (uint8_t i = 0; i < MAX_ZONES; i++) { + _zones[i].x1 = doc[i]["x1"] | _zones[i].x1; + _zones[i].y1 = doc[i]["y1"] | _zones[i].y1; + _zones[i].x2 = doc[i]["x2"] | _zones[i].x2; + _zones[i].y2 = doc[i]["y2"] | _zones[i].y2; + } + return true; +} diff --git a/ZoneManager.h b/ZoneManager.h new file mode 100644 index 0000000..a94df7f --- /dev/null +++ b/ZoneManager.h @@ -0,0 +1,46 @@ +// ===================================================================== +// ZoneManager.h +// +// Rectangular detection-zone logic, ported from the ESP32-LD2450 +// project's main_zone.cpp. Works entirely on RadarTarget X/Y values, +// so it is unaffected by however LD2453Parser ends up decoding raw +// frames — nothing here needs to change once the parser is confirmed. +// ===================================================================== +#pragma once + +#include +#include "LD2453Parser.h" + +static const uint8_t MAX_ZONES = 3; + +struct Zone { + int x1, y1, x2, y2; // millimeters, same coordinate system as RadarTarget +}; + +class ZoneManager { +public: + ZoneManager(); + + // Re-evaluate occupancy for all zones against the targets in + // `frame`. Call this once per new radar frame. + void update(const RadarFrame &frame); + + bool isZoneOccupied(uint8_t zoneIndex) const; + Zone getZone(uint8_t zoneIndex) const; + void setZone(uint8_t zoneIndex, const Zone &zone); + + // Serialize current zone definitions to JSON, e.g. for GET /zones. + // Returns the JSON string. + String zonesToJson() const; + + // Parse a JSON body (as sent by POST /updateZones) and update + // zones in place. Missing fields keep their previous value. + // Returns false on JSON parse failure. + bool zonesFromJson(const uint8_t *data, size_t len); + +private: + Zone _zones[MAX_ZONES]; + bool _occupied[MAX_ZONES]; + + static bool pointInZone(int x, int y, const Zone &z); +}; diff --git a/config.example.h b/config.example.h new file mode 100644 index 0000000..3e98efb --- /dev/null +++ b/config.example.h @@ -0,0 +1,51 @@ +// ===================================================================== +// config.example.h +// +// Copy this file to "config.h" (same folder) and edit the values +// below. "config.h" is meant to be gitignored so you don't commit +// your Wi-Fi password. +// ===================================================================== +#pragma once + +// --------------------------------------------------------------- +// Wi-Fi credentials +// --------------------------------------------------------------- +#define WIFI_SSID "your_ssid" +#define WIFI_PASSWORD "your_password" + +// --------------------------------------------------------------- +// Radar UART pins (ESP32-S3, HardwareSerial — NOT SoftwareSerial) +// Change these to match your wiring. Any two free GPIOs that +// support UART RX/TX on your ESP32-S3 board will work. +// --------------------------------------------------------------- +#define RADAR_RX_PIN 18 // ESP32-S3 RX <- LD2453 TX +#define RADAR_TX_PIN 17 // ESP32-S3 TX -> LD2453 RX +#define RADAR_UART_PORT 1 // which HardwareSerial to use (Serial1 or Serial2) + +// Default LD2453 UART settings. 8N1 is assumed by analogy with the +// rest of the Hi-Link 24xx radar family; this has NOT been confirmed +// against the official LD2453 manual. Verify with DEBUG_RADAR_RAW +// before trusting any parsed output. +#define RADAR_BAUD_RATE 256000 + +// --------------------------------------------------------------- +// Node identity — for the future multi-radar (LD2453 A/B/C -> UDP +// -> Orange Pi) architecture. Not used for networking yet, but every +// frame is tagged with this so the plumbing is ready later. +// --------------------------------------------------------------- +#define NODE_ID "RADAR_A" + +// --------------------------------------------------------------- +// Debug / diagnostic switches +// --------------------------------------------------------------- +// 1 = print every raw UART frame candidate as hex on the Serial +// Monitor (115200). Use this to capture real traffic from the +// module so the frame format can be confirmed/reverse-engineered. +#define DEBUG_RADAR_RAW 0 + +// 1 = diagnostic mode only: Serial-only target printout, no Wi-Fi, +// no web server, no WebSocket. This is the recommended first +// stage — get the parser confirmed against real hardware before +// bringing up networking. +// 0 = full stage-2 firmware: Wi-Fi + REST API + WebSocket + zones. +#define DIAGNOSTIC_MODE 1 diff --git a/config.h b/config.h new file mode 100644 index 0000000..a9d4c23 --- /dev/null +++ b/config.h @@ -0,0 +1,51 @@ +// ===================================================================== +// config.example.h +// +// Copy this file to "config.h" (same folder) and edit the values +// below. "config.h" is meant to be gitignored so you don't commit +// your Wi-Fi password. +// ===================================================================== +#pragma once + +// --------------------------------------------------------------- +// Wi-Fi credentials +// --------------------------------------------------------------- +#define WIFI_SSID "OrangePiVietnam" +#define WIFI_PASSWORD "orangepi.vn" + +// --------------------------------------------------------------- +// Radar UART pins (ESP32-S3, HardwareSerial — NOT SoftwareSerial) +// Change these to match your wiring. Any two free GPIOs that +// support UART RX/TX on your ESP32-S3 board will work. +// --------------------------------------------------------------- +#define RADAR_RX_PIN 18 // ESP32-S3 RX <- LD2453 TX +#define RADAR_TX_PIN 17 // ESP32-S3 TX -> LD2453 RX +#define RADAR_UART_PORT 1 // which HardwareSerial to use (Serial1 or Serial2) + +// Default LD2453 UART settings. 8N1 is assumed by analogy with the +// rest of the Hi-Link 24xx radar family; this has NOT been confirmed +// against the official LD2453 manual. Verify with DEBUG_RADAR_RAW +// before trusting any parsed output. +#define RADAR_BAUD_RATE 256000 + +// --------------------------------------------------------------- +// Node identity — for the future multi-radar (LD2453 A/B/C -> UDP +// -> Orange Pi) architecture. Not used for networking yet, but every +// frame is tagged with this so the plumbing is ready later. +// --------------------------------------------------------------- +#define NODE_ID "RADAR_A" + +// --------------------------------------------------------------- +// Debug / diagnostic switches +// --------------------------------------------------------------- +// 1 = print every raw UART frame candidate as hex on the Serial +// Monitor (115200). Use this to capture real traffic from the +// module so the frame format can be confirmed/reverse-engineered. +#define DEBUG_RADAR_RAW 0 + +// 1 = diagnostic mode only: Serial-only target printout, no Wi-Fi, +// no web server, no WebSocket. This is the recommended first +// stage — get the parser confirmed against real hardware before +// bringing up networking. +// 0 = full stage-2 firmware: Wi-Fi + REST API + WebSocket + zones. +#define DIAGNOSTIC_MODE 1 diff --git a/esp32-s3-ld2453.ino b/esp32-s3-ld2453.ino new file mode 100644 index 0000000..d50c5cd --- /dev/null +++ b/esp32-s3-ld2453.ino @@ -0,0 +1,115 @@ +// ===================================================================== +// ESP32_LD2453.ino +// +// ESP32-S3 + Hi-Link HLK-LD2453 radar firmware. +// Ported from https://github.com/thanhtantran/ESP32-LD2450 +// +// *** LD2453Parser's byte-level decoding is currently a STUB *** +// See the comment block at the top of LD2453Parser.h for why, and +// the README for how to help confirm the real protocol. Everything +// else in this sketch (Wi-Fi, REST API, WebSocket, zones, diagnostic +// printing) is fully functional today. +// +// Copy config.example.h to config.h and edit it before building. +// ===================================================================== +#include +#include "config.h" +#include "LD2453Parser.h" +#include "ZoneManager.h" + +#if !DIAGNOSTIC_MODE +#include "WebServerManager.h" +#endif + +// --------------------------------------------------------------- +// Pick the HardwareSerial instance for the radar based on +// RADAR_UART_PORT in config.h. Serial0/USB is left free for the +// Serial Monitor. +// --------------------------------------------------------------- +#if RADAR_UART_PORT == 1 +HardwareSerial &RadarSerial = Serial1; +#else +HardwareSerial &RadarSerial = Serial2; +#endif + +LD2453Parser radar; +ZoneManager zoneManager; + +#if !DIAGNOSTIC_MODE +WebServerManager webServer(zoneManager); +#endif + +void printDiagnostics(const RadarFrame &frame, const LD2453Stats &stats) { + Serial.println(); + Serial.println(F("LD2453 connected")); + Serial.print(F("UART: ")); + Serial.println(RADAR_BAUD_RATE); + Serial.print(F("Frame length (last): ")); + Serial.println(stats.lastFrameLength); + Serial.print(F("Parse errors: ")); + Serial.println(stats.parseErrors); + Serial.print(F("Packets/sec: ")); + Serial.println(stats.packetsPerSecond, 1); + Serial.print(F("Target count: ")); + Serial.println(frame.targetCount); + + for (uint8_t i = 0; i < 3; i++) { + Serial.println(); + Serial.print(F("Target ")); + Serial.print(i + 1); + Serial.println(':'); + + const RadarTarget &t = frame.targets[i]; + if (!t.valid) { + Serial.println(F("INVALID")); + continue; + } + Serial.print(F("X: ")); + Serial.print(t.x_mm); + Serial.println(F(" mm")); + Serial.print(F("Y: ")); + Serial.print(t.y_mm); + Serial.println(F(" mm")); + Serial.print(F("Speed: ")); + Serial.println(t.speed); + } +} + +void setup() { + Serial.begin(115200); + delay(1500); // lets the Serial Monitor catch up without blocking the app loop later + + Serial.println(F("=== ESP32_LD2453 booting ===")); + Serial.print(F("Node ID: ")); + Serial.println(NODE_ID); + + radar.begin(RadarSerial, RADAR_RX_PIN, RADAR_TX_PIN, RADAR_BAUD_RATE, + NODE_ID, DEBUG_RADAR_RAW); + +#if !DIAGNOSTIC_MODE + webServer.begin(WIFI_SSID, WIFI_PASSWORD); +#else + Serial.println(F("DIAGNOSTIC_MODE=1: Wi-Fi/web server disabled. Serial-only output.")); +#endif +} + +void loop() { + // Radar acquisition/parsing is non-blocking and runs every + // iteration regardless of mode, independent of Wi-Fi state. + bool gotFrame = radar.update(); + + if (gotFrame) { + const RadarFrame &frame = radar.getLastFrame(); + zoneManager.update(frame); + +#if DIAGNOSTIC_MODE + printDiagnostics(frame, radar.getStats()); +#else + webServer.broadcastFrame(frame); +#endif + } + +#if !DIAGNOSTIC_MODE + webServer.loop(); +#endif +}