fix ghost target
This commit is contained in:
1 parent
31c9d332ba
commit
bf0efba9dc
3 files changed
+90
-32
No files matched your search
@@ -14,27 +14,37 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// LD2450 TARGET REPORT FORMAT (confirmed vs LD2450 libs on GitHub)
|
// LD2450 / LD2453 TARGET REPORT FORMAT.
|
||||||
// Header : [0xAA, 0xFF, 0x03, 0x00] — 4 bytes (little-endian type+len)
|
// Header : [0xAA, 0xFF, 0x03, 0x00] — 4 bytes (sync + little-endian
|
||||||
// Payload: 3 targets x 8 bytes each — 24 bytes, little-endian int16 per field
|
// frame type = 0x0003)
|
||||||
|
// Payload: 3 targets x 8 bytes each — 24 bytes, little-endian int16
|
||||||
// Tail : [0x55, 0xCC] — 2 bytes
|
// Tail : [0x55, 0xCC] — 2 bytes
|
||||||
// Total : 30 bytes per target report frame.
|
// Total : 30 bytes per report frame.
|
||||||
//
|
//
|
||||||
// Per target 8-byte record:
|
// NOTE ON FRAME-TYPE AMBIGUITY FOR THE LD2453:
|
||||||
// [0..1] X = (int16) LE, millimetres (signed, left/right)
|
// On the LD2450, frame type 0x0003 is the "target tracking report"
|
||||||
// [2..3] Y = (int16) LE, millimetres (signed, forward = +Y)
|
// that carries real T1..T3 positions. On the LD2453, however, some
|
||||||
// [4..5] Speed = (int16) LE, cm/s. Conventional sign: positive =
|
// units ALSO send a periodic 30-byte 0x0003 frame that looks like a
|
||||||
// moving toward radar (decreasing Y distance).
|
// configuration/self-test ACK: the byte positions are identical but
|
||||||
// [6..7] Resolution= (uint16) LE, distance resolution of the tracker
|
// slot 1 carries garbage (coordinates like X=901 Y=-26735, i.e. a
|
||||||
// for this slot, in millimetres. NOT a valid flag.
|
// point 27 m BEHIND the radar) while slots 2..3 remain 0. This is
|
||||||
|
// physically impossible, so instead of switching the accepted frame
|
||||||
|
// type (which would break real LD2450 0x0003 traffic) we gate the
|
||||||
|
// per-target validity with HARD PHYSICAL LIMITS (see below). That
|
||||||
|
// single filter reliably kills ACK ghosts while keeping real targets.
|
||||||
//
|
//
|
||||||
// VALIDITY RULE (industry standard for LD2450 — used by every public
|
// Per target 8-byte record (LD2450 + LD2453):
|
||||||
// parser we cross-checked against the original ESP32-LD2450 repo):
|
// [0..1] int16 X mm, signed; <0 left, >0 right
|
||||||
// Target is VALID <=> (X != 0 OR Y != 0).
|
// [2..3] int16 Y mm, signed; ALWAYS >0 for real targets
|
||||||
// There is NO "valid byte" at offset 6 — that byte is the low byte of
|
// [4..5] int16 Speed cm/s, signed; >0 = moving TOWARD radar
|
||||||
// Resolution, which can be non-zero even for idle (all-zero) slots when
|
// [6..7] uint16 DistanceRes. mm, unsigned. NOT a "valid flag"
|
||||||
// the DSP has no track to report. Treating it as a valid flag caused
|
//
|
||||||
// the classic "radar reports 1 stationary ghost at origin" bug.
|
// PHYSICAL REALITY CHECKS (applied after the basic X/Y != 0 rule):
|
||||||
|
// MAX_RANGE_MM = 6000 (LD2450/LD2453 rated range = 0.75 .. 6.0 m)
|
||||||
|
// MAX_X_MM = 6000 (±60° cone at 6 m gives ~ |X| ≤ ~5200 mm;
|
||||||
|
// use 6000 for a small safety margin)
|
||||||
|
// Y_MIN_MM = 1 (Y must be IN FRONT of the radar — negative Y
|
||||||
|
// means "behind the antenna" and is ACK noise)
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
static const uint8_t FRAME_HEADER[] = { 0xAA, 0xFF, 0x03, 0x00 };
|
static const uint8_t FRAME_HEADER[] = { 0xAA, 0xFF, 0x03, 0x00 };
|
||||||
static const uint8_t FRAME_TAIL[] = { 0x55, 0xCC };
|
static const uint8_t FRAME_TAIL[] = { 0x55, 0xCC };
|
||||||
@@ -45,6 +55,12 @@ static const size_t DATA_SIZE = FRAME_LENGTH - HEADER_SIZE - TAIL_SIZE;
|
|||||||
static const size_t TARGET_RECORD_SIZE = 8;
|
static const size_t TARGET_RECORD_SIZE = 8;
|
||||||
static const size_t MAX_TARGETS = 3;
|
static const size_t MAX_TARGETS = 3;
|
||||||
|
|
||||||
|
/* Hard physical limits — anything outside these bounds is discarded as
|
||||||
|
* a spurious ACK/register dump rather than a real target report. */
|
||||||
|
static const int32_t Y_MIN_MM = 1;
|
||||||
|
static const int32_t Y_MAX_MM = 6000;
|
||||||
|
static const int32_t X_ABS_MAX_MM = 6000;
|
||||||
|
|
||||||
void LD2453Parser::begin(HardwareSerial &serial, int rxPin, int txPin, uint32_t baud,
|
void LD2453Parser::begin(HardwareSerial &serial, int rxPin, int txPin, uint32_t baud,
|
||||||
const char *nodeId, bool rawDebug) {
|
const char *nodeId, bool rawDebug) {
|
||||||
_serial = &serial;
|
_serial = &serial;
|
||||||
@@ -221,17 +237,37 @@ bool LD2453Parser::tryParseFrame() {
|
|||||||
uint16_t resolution = (uint16_t)(targetData[6] | (targetData[7] << 8));
|
uint16_t resolution = (uint16_t)(targetData[6] | (targetData[7] << 8));
|
||||||
(void)resolution;
|
(void)resolution;
|
||||||
|
|
||||||
// VALIDITY — authoritative rule (matches eric-rolph / dimch / Fiooodooor
|
// ============================================================
|
||||||
// public LD2450 parsers, and the behaviour demonstrated in the user's
|
// VALIDITY DECISION — 2 stages, tuned to eliminate the two
|
||||||
// reference GitHub repo main_zone.cpp):
|
// most common LD245x false-positive sources:
|
||||||
// Target is VALID <=> (X != 0) OR (Y != 0)
|
|
||||||
//
|
//
|
||||||
// This is the single rule that kills the "stationary ghost at (0,0)"
|
// STAGE 1. Position-exists rule (LD2450 standard):
|
||||||
// symptom. Resolution bytes can legitimately be non-zero in an idle
|
// (x != 0) || (y != 0)
|
||||||
// slot when the DSP has nothing tracked but last-cycle metadata is
|
//
|
||||||
// still sitting in the output shift register, so we MUST NOT treat
|
// This kills the "valid-flag byte at offset 6"
|
||||||
// any other byte as a "valid flag".
|
// false-positive ghost at (0,0).
|
||||||
const bool valid = (x != 0) || (y != 0);
|
//
|
||||||
|
// STAGE 2. HARD PHYSICAL LIMITS (the new part needed for
|
||||||
|
// LD2453 units that leak ACK bytes on frame-type
|
||||||
|
// 0x0003):
|
||||||
|
// Y ∈ [1, 6000] (always IN FRONT of radar,
|
||||||
|
// inside rated range)
|
||||||
|
// |X| ≤ 6000 (inside ±60° detection
|
||||||
|
// cone + safety margin)
|
||||||
|
// (x != 0) || (y != 0)
|
||||||
|
//
|
||||||
|
// Stage 2 alone is sufficient to drop the exact ACK pattern
|
||||||
|
// the user captured: X=901 Y=-26735 (Y negative, 27 m behind
|
||||||
|
// radar → impossible). Stage 1 + 2 together cover all
|
||||||
|
// currently-known LD2450 / LD2453 false-positives.
|
||||||
|
// ============================================================
|
||||||
|
const bool position_exists = (x != 0) || (y != 0);
|
||||||
|
const bool y_in_front_range =
|
||||||
|
((int32_t)y >= Y_MIN_MM) && ((int32_t)y <= Y_MAX_MM);
|
||||||
|
const bool x_in_cone =
|
||||||
|
((int32_t)x >= -X_ABS_MAX_MM) && ((int32_t)x <= X_ABS_MAX_MM);
|
||||||
|
|
||||||
|
const bool valid = position_exists && y_in_front_range && x_in_cone;
|
||||||
|
|
||||||
_lastFrame.targets[t].valid = valid;
|
_lastFrame.targets[t].valid = valid;
|
||||||
_lastFrame.targets[t].x_mm = x;
|
_lastFrame.targets[t].x_mm = x;
|
||||||
|
|||||||
Binary file not shown.
@@ -135,21 +135,39 @@ class RadarState:
|
|||||||
self.target_count = frame.get("target_count", 0)
|
self.target_count = frame.get("target_count", 0)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
|
# Belt-and-suspenders physical-limits filter. The ESP32 side
|
||||||
|
# should already have gated these, but in case old firmware is
|
||||||
|
# still running or an ACK frame leaks through, we re-apply the
|
||||||
|
# same sanity rules on the Orange Pi side:
|
||||||
|
# Y in front of radar (y >= 1 mm), <= 6000 mm rated range
|
||||||
|
# |X| <= 6000 mm (±60° cone + safety margin)
|
||||||
|
PHYS_Y_MIN = 1
|
||||||
|
PHYS_Y_MAX = 6000
|
||||||
|
PHYS_X_ABS = 6000
|
||||||
|
valid_targets_this_frame = 0
|
||||||
|
|
||||||
targets_data = frame.get("targets", [])
|
targets_data = frame.get("targets", [])
|
||||||
for t_data in targets_data:
|
for t_data in targets_data:
|
||||||
slot = t_data.get("id", 0)
|
slot = t_data.get("id", 0)
|
||||||
if 1 <= slot <= 3:
|
if 1 <= slot <= 3:
|
||||||
t = self.targets[slot - 1]
|
t = self.targets[slot - 1]
|
||||||
t.slot = slot
|
t.slot = slot
|
||||||
t.valid = t_data.get("valid", False)
|
t.valid = bool(t_data.get("valid", False))
|
||||||
t.x_mm = t_data.get("x", 0)
|
t.x_mm = int(t_data.get("x", 0))
|
||||||
t.y_mm = t_data.get("y", 0)
|
t.y_mm = int(t_data.get("y", 0))
|
||||||
t.speed_cm_s = t_data.get("speed", 0)
|
t.speed_cm_s = int(t_data.get("speed", 0))
|
||||||
|
|
||||||
# If ESP32 parser already gives distance/angle, use them
|
# If ESP32 parser already gives distance/angle, use them
|
||||||
t.distance_mm = float(t_data.get("distance", 0.0))
|
t.distance_mm = float(t_data.get("distance", 0.0))
|
||||||
t.angle_deg = float(t_data.get("angle", 0.0))
|
t.angle_deg = float(t_data.get("angle", 0.0))
|
||||||
|
|
||||||
|
# Sanity overrides: strip validity if coordinates lie
|
||||||
|
# outside the radar's measurable physical envelope.
|
||||||
|
y_ok = PHYS_Y_MIN <= t.y_mm <= PHYS_Y_MAX
|
||||||
|
x_ok = -PHYS_X_ABS <= t.x_mm <= PHYS_X_ABS
|
||||||
|
if not (y_ok and x_ok):
|
||||||
|
t.valid = False
|
||||||
|
|
||||||
# Derive distance / angle if missing, for convenience
|
# Derive distance / angle if missing, for convenience
|
||||||
if t.valid and (t.distance_mm == 0 or t.angle_deg == 0):
|
if t.valid and (t.distance_mm == 0 or t.angle_deg == 0):
|
||||||
t.distance_mm = math.hypot(t.x_mm, t.y_mm)
|
t.distance_mm = math.hypot(t.x_mm, t.y_mm)
|
||||||
@@ -158,7 +176,11 @@ class RadarState:
|
|||||||
|
|
||||||
if t.valid:
|
if t.valid:
|
||||||
t.last_seen = now
|
t.last_seen = now
|
||||||
|
valid_targets_this_frame += 1
|
||||||
|
|
||||||
|
# Keep the browser-side counters honest even if the ESP32 told
|
||||||
|
# us a target_count that included the now-filtered ACK ghost.
|
||||||
|
self.target_count = valid_targets_this_frame
|
||||||
self.frames_received += 1
|
self.frames_received += 1
|
||||||
self.last_update = now
|
self.last_update = now
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user