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