add files
This commit is contained in:
1 parent
3eaff3a647
commit
e76a9c1e9b
9 files changed
+752
No files matched your search
@@ -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;
|
||||
}
|
||||
+110
@@ -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 <Arduino.h>
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 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();
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
// =====================================================================
|
||||
// WebServerManager.cpp
|
||||
// =====================================================================
|
||||
#include "WebServerManager.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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 <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#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);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
// =====================================================================
|
||||
// ZoneManager.cpp
|
||||
// =====================================================================
|
||||
#include "ZoneManager.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
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<JsonArray>();
|
||||
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;
|
||||
}
|
||||
@@ -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 <Arduino.h>
|
||||
#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);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 <Arduino.h>
|
||||
#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
|
||||
}
|
||||
Reference in new issue
Block a user