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 +}