fix ghost target

This commit is contained in:
admin committed 2026-09-11 23:04:35 +07:00
1 parent 31c9d332ba
commit bf0efba9dc
3 files changed
+90 -32

No files matched your search

+64 -28
View File
@@ -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;
+26 -4
View File
@@ -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