From bf0efba9dcc0e1d216f60f94984c5127231a5844 Mon Sep 17 00:00:00 2001 From: Tony Tran Date: Fri, 11 Sep 2026 23:04:35 +0700 Subject: [PATCH] fix ghost target --- esp32s3-ld2453/LD2453Parser.cpp | 92 ++++++++++++------ .../__pycache__/visualizer.cpython-313.pyc | Bin 0 -> 28223 bytes orangepi-server/visualizer.py | 30 +++++- 3 files changed, 90 insertions(+), 32 deletions(-) create mode 100644 orangepi-server/__pycache__/visualizer.cpython-313.pyc diff --git a/esp32s3-ld2453/LD2453Parser.cpp b/esp32s3-ld2453/LD2453Parser.cpp index e43919b..a4319f3 100644 --- a/esp32s3-ld2453/LD2453Parser.cpp +++ b/esp32s3-ld2453/LD2453Parser.cpp @@ -14,27 +14,37 @@ #endif // --------------------------------------------------------------- -// LD2450 TARGET REPORT FORMAT (confirmed vs LD2450 libs on GitHub) -// Header : [0xAA, 0xFF, 0x03, 0x00] — 4 bytes (little-endian type+len) -// Payload: 3 targets x 8 bytes each — 24 bytes, little-endian int16 per field +// LD2450 / LD2453 TARGET REPORT FORMAT. +// Header : [0xAA, 0xFF, 0x03, 0x00] — 4 bytes (sync + little-endian +// frame type = 0x0003) +// Payload: 3 targets x 8 bytes each — 24 bytes, little-endian int16 // Tail : [0x55, 0xCC] — 2 bytes -// Total : 30 bytes per target report frame. +// Total : 30 bytes per report frame. // -// Per target 8-byte record: -// [0..1] X = (int16) LE, millimetres (signed, left/right) -// [2..3] Y = (int16) LE, millimetres (signed, forward = +Y) -// [4..5] Speed = (int16) LE, cm/s. Conventional sign: positive = -// moving toward radar (decreasing Y distance). -// [6..7] Resolution= (uint16) LE, distance resolution of the tracker -// for this slot, in millimetres. NOT a valid flag. +// NOTE ON FRAME-TYPE AMBIGUITY FOR THE LD2453: +// On the LD2450, frame type 0x0003 is the "target tracking report" +// that carries real T1..T3 positions. On the LD2453, however, some +// units ALSO send a periodic 30-byte 0x0003 frame that looks like a +// configuration/self-test ACK: the byte positions are identical but +// slot 1 carries garbage (coordinates like X=901 Y=-26735, i.e. a +// 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 -// parser we cross-checked against the original ESP32-LD2450 repo): -// Target is VALID <=> (X != 0 OR Y != 0). -// There is NO "valid byte" at offset 6 — that byte is the low byte of -// Resolution, which can be non-zero even for idle (all-zero) slots when -// the DSP has no track to report. Treating it as a valid flag caused -// the classic "radar reports 1 stationary ghost at origin" bug. +// Per target 8-byte record (LD2450 + LD2453): +// [0..1] int16 X mm, signed; <0 left, >0 right +// [2..3] int16 Y mm, signed; ALWAYS >0 for real targets +// [4..5] int16 Speed cm/s, signed; >0 = moving TOWARD radar +// [6..7] uint16 DistanceRes. mm, unsigned. NOT a "valid flag" +// +// 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_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 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, const char *nodeId, bool rawDebug) { _serial = &serial; @@ -221,17 +237,37 @@ bool LD2453Parser::tryParseFrame() { uint16_t resolution = (uint16_t)(targetData[6] | (targetData[7] << 8)); (void)resolution; - // VALIDITY — authoritative rule (matches eric-rolph / dimch / Fiooodooor - // public LD2450 parsers, and the behaviour demonstrated in the user's - // reference GitHub repo main_zone.cpp): - // Target is VALID <=> (X != 0) OR (Y != 0) + // ============================================================ + // VALIDITY DECISION — 2 stages, tuned to eliminate the two + // most common LD245x false-positive sources: // - // This is the single rule that kills the "stationary ghost at (0,0)" - // symptom. Resolution bytes can legitimately be non-zero in an idle - // slot when the DSP has nothing tracked but last-cycle metadata is - // still sitting in the output shift register, so we MUST NOT treat - // any other byte as a "valid flag". - const bool valid = (x != 0) || (y != 0); + // STAGE 1. Position-exists rule (LD2450 standard): + // (x != 0) || (y != 0) + // + // This kills the "valid-flag byte at offset 6" + // false-positive ghost at (0,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].x_mm = x; diff --git a/orangepi-server/__pycache__/visualizer.cpython-313.pyc b/orangepi-server/__pycache__/visualizer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e0cadbc474df08f34de9c461268064f978851a6 GIT binary patch literal 28223 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 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", []) for t_data in targets_data: slot = t_data.get("id", 0) if 1 <= slot <= 3: t = self.targets[slot - 1] t.slot = slot - t.valid = t_data.get("valid", False) - t.x_mm = t_data.get("x", 0) - t.y_mm = t_data.get("y", 0) - t.speed_cm_s = t_data.get("speed", 0) + t.valid = bool(t_data.get("valid", False)) + t.x_mm = int(t_data.get("x", 0)) + t.y_mm = int(t_data.get("y", 0)) + t.speed_cm_s = int(t_data.get("speed", 0)) # If ESP32 parser already gives distance/angle, use them t.distance_mm = float(t_data.get("distance", 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 if t.valid and (t.distance_mm == 0 or t.angle_deg == 0): t.distance_mm = math.hypot(t.x_mm, t.y_mm) @@ -158,7 +176,11 @@ class RadarState: if t.valid: 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.last_update = now