From 1682808956641e4c99ee37594bf54073ab1330a9 Mon Sep 17 00:00:00 2001 From: Tony Tran Date: Sun, 13 Sep 2026 16:42:32 +0700 Subject: [PATCH] =?UTF-8?q?fix=20l=E1=BB=97i=20gi=E1=BA=A3i=20m=C3=A3=20by?= =?UTF-8?q?te=20encoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esp32s3-ld2453/LD2453Parser.cpp | 76 +++++++++++++----- .../__pycache__/visualizer.cpython-313.pyc | Bin 28223 -> 28223 bytes orangepi-server/visualizer.py | 14 ++-- 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/esp32s3-ld2453/LD2453Parser.cpp b/esp32s3-ld2453/LD2453Parser.cpp index 99a36bb..5152f52 100644 --- a/esp32s3-ld2453/LD2453Parser.cpp +++ b/esp32s3-ld2453/LD2453Parser.cpp @@ -16,21 +16,44 @@ // --------------------------------------------------------------- // LD2450 / LD2453 SERIAL FRAMING — PER-USER-DIRECTED POLICY. // +// (0) CRITICAL DIFFERENCE LD2450 vs LD2453 (ROOT CAUSE of "hand wave +// still invalid" reported by the user): +// * LD2450 coordinate encoding per slot = standard 2's complement +// signed int16 little-endian. +// * LD2453 coordinate encoding per slot = 16-bit SIGN-MAGNITUDE, +// little-endian byte order. +// bit 15 (MSB) = sign flag. +// 1 ⇒ POSITIVE. +// 0 ⇒ NEGATIVE. +// bits 14..0 = 15-bit unsigned magnitude (absolute value). +// +// Evidence from the user's 2026-09-13 hand-wave capture: +// Raw slot-1 bytes = [3B 80 BF 80 00 00 4A 01] +// Previous (LD2450-style) 2's-complement cast → X=-32709 Y=-32577 +// → false "implausible WARN" even on a real target. +// New LD2453 sign-magnitude decode: +// X = 0x803B → sign=+ magnitude=0x003B = 59 mm +// Y = 0x80BF → sign=+ magnitude=0x00BF = 191 mm +// → distance ≈ 200 mm ≈ hand 20 cm in front of the antenna, +// perfectly plausible → VALID ✔ +// +// Speed (bytes 4-5 of each slot) is still interpreted as standard +// signed int16 LE (cm/s); distance-resolution (bytes 6-7) stays as +// standard uint16 LE (mm). Speed and resolution encodings are +// IDENTICAL between LD2450 and LD2453. +// // The user explicitly asked for the following behaviour — we follow it // exactly below and do NOT invent anything else: // (a) NEVER reject a syntactically-valid 30-byte 0x0003 frame as a // whole. Every [AA FF 03 00 .. .. 55 CC] gets counted in // framesParsed and promoted to _lastFrame. -// (b) Per-slot sanity is the ONLY filtering point. If a slot's decoded -// coordinates are physically implausible (Y < 0, Y > 6m, -// |X| > 6m, …): +// (b) Per-slot sanity is the ONLY filtering point. If a slot's +// SIGN-MAGNITUDE decoded coords are physically implausible +// (Y <= 0, Y > ~6.5m, |X| > ~6.5m, …): // * we print a WARN line to Serial Monitor, // * mark the slot valid=false IN the promoted RadarFrame, // * increment badSlotWarnings stat, -// * do NOT send it up to the WebSocket / OLED / UI. -// -// This "trust the wire unless proven-impossible per-slot" policy maximises -// recall — zero whole-frame dropped data loss. +// * do NOT expose it to WebSocket / OLED / UI. // // (1) WIRE LAYOUT (byte-level, both chips): // Sync 2 bytes : 0xAA, 0xFF @@ -40,16 +63,16 @@ // // (2) ACCEPTED FRAME TYPES FOR PARSING: // * 0x0003 — LD2450/LD2453 3-slot tracking report (30 bytes -// total / 24 payload / 3 × 8 B/slot). This is the only -// format we know how to interpret today. Frames with any -// other frame-type words are skipped byte-by-byte until a -// fresh AA FF sync is found. +// total / 24 payload / 3 × 8 B/slot). Only difference +// vs LD2450 = X/Y sign-magnitude encoding (see §0). // // (3) 8-BYTE SLOT LAYOUT (0x0003 frame payload slots): -// [0..1] int16 X_mm signed; <0 left, >0 right -// [2..3] int16 Y_mm signed; ALWAYS >0 for a real person -// [4..5] int16 Speed_cm_s signed; +ve = closer -// [6..7] uint16 DistanceRes_mm unsigned; NOT a "valid flag" +// [0..1] X_mm LD2453 = sign-magnitude uint16 LE +// LD2450 = 2's-compl signed int16 LE +// Use decodeCoordSM16() below (LD2453 rule). +// [2..3] Y_mm SAME ENCODING AS X. Y >= 1 for real target. +// [4..5] Speed_cm_s standard signed int16 LE, cm/s. +// [6..7] DistRes_mm standard uint16 LE, mm. // --------------------------------------------------------------- static const uint8_t FRAME_SYNC[] = { 0xAA, 0xFF }; static const uint8_t FRAME_TAIL[] = { 0x55, 0xCC }; @@ -71,10 +94,23 @@ static const uint16_t accepted_target_types[] = { static const size_t accepted_target_types_count = sizeof(accepted_target_types) / sizeof(accepted_target_types[0]); -// Per-slot physical plausibility bounds (used for the WARN step (b) above). +/* LD2453 SIGN-MAGNITUDE coordinate decoder (§0 above). + * Takes the 2 little-endian slot bytes as [lo, hi] and returns a + * signed mm coordinate. */ +static inline int32_t decodeCoordSM16(uint8_t lo, uint8_t hi) { + const uint16_t raw = (uint16_t)lo | ((uint16_t)hi << 8); + const bool positive = (raw & 0x8000u) != 0u; /* bit15 = sign flag */ + const uint32_t magnitude = (uint32_t)(raw & 0x7FFFu); /* bits 0..14 */ + const int32_t sign = positive ? 1 : -1; + return sign * (int32_t)magnitude; +} + +// Per-slot physical plausibility bounds. LD2453 nominal range = 0.5–6m, +// use 6.5 m as an upper-bound ceiling so values just inside max range +// (like the earlier ACK-like 6033 mm case) do not get false-WARNed. 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; +static const int32_t Y_MAX_MM = 6500; +static const int32_t X_ABS_MAX_MM = 6500; // Returns true iff decoded (x,y) sit inside the radar's measurable // operational envelope (in front of the antenna and inside rated @@ -272,8 +308,8 @@ bool LD2453Parser::tryParseFrame() { } } - int16_t x = (int16_t)(targetData[0] | (targetData[1] << 8)); - int16_t y = (int16_t)(targetData[2] | (targetData[3] << 8)); + int32_t x = decodeCoordSM16(targetData[0], targetData[1]); + int32_t y = decodeCoordSM16(targetData[2], targetData[3]); int16_t speed = (int16_t)(targetData[4] | (targetData[5] << 8)); uint16_t resolution = (uint16_t)(targetData[6] | (targetData[7] << 8)); (void)resolution; diff --git a/orangepi-server/__pycache__/visualizer.cpython-313.pyc b/orangepi-server/__pycache__/visualizer.cpython-313.pyc index 6e0cadbc474df08f34de9c461268064f978851a6..b54a4347f022bf936b0662946b520d667cdc5b6a 100644 GIT binary patch delta 1305 zcmZ9MU2GIp6vy|@>@20*Q6moxq-)o1XuDWjH#9M6szK>WD7aACih;ytx;rh?{Tk2A zQrjXW82W@(IBJ@Z5E={y1XdXJMNH!d8v9atsmTOlC1OIN2=t*wAM~8NQ4;szmvjDe z@11l1=giJs7jxHzzO3uj0s6J(&x~xmq_2&rR%WdikK7WX#(TJaSP@wqeY{-NuGOD% zR|4(W+K^N&yVl#^Ft3ObKDU08Hb(oec~k4>gf>axPhNM^_-ZkMSD$)0lqCViFqUWt zz0To5+?I%l1861c#37tY?9$#R`8V%IqOW?*D-`-6$2HuN1Ml+kY21=*!fa=C2MFO&uW2jU z8N)UA6bq&UM|jBxjI)GPT*x}&3Vet&T~CWLe&5y8c%GkMU|b~lLA&UrHw+`%=w{V!`){>T!jVPlxorz>EQ(CQf0B|)$QmKB9F$-W^IY& zWBAt26)}xdJu&Sj)nN3od-b&uYL-o7R4EfV=n}?~z^PN-Blv9DPTK2Z3FMj|sk-cd&c!2JH?dm%XvQ zP$ll;uj$RrQSJwT> z`{cJLE&7|&0mdq9v0f6#@eQkYQcko+n}9za2ys-!c$Cu(jExNGZIU8ibxc><3NcQD z+l%RJ#cSEdJd5E$!?Dtvi-jR;*bi<^y|4oUe5mXJ|7*wrqj`IXLiuq@yC*O`5*Ih{ z&5_z94;d`*VOqXeqHf8qEU|i%!NUY|jBgk`EB@+HbI_r3s6cKD=y-diLBy~od$iUT cqW*4xxvU11@Lh$&dpWP1eO3|ucqiZU4_Uo#QUCw| delta 1305 zcmZ9MUu;u#6vzAP?Je87P9hJBWOa3gjn2tzG?*;YU}z-_EQ}2ic$jy$ccW{2+nsy6 zNwYDKxF<1_6BR-XiX#}O47{i>kZA~N;K3OmX0G^$EQ?uU96A#;d-0rKqa^m>lXJem z-~HY5`<`<5Ms4+Uwyw55Hi z{oD3k&nDd1kWgTs#~Et4;T1#p^!m+eiJt$~8DBpm)FTwmI~`4>N-=`hAAR1RB>_qp zi8c5Sb2x_EV<9n$=~$gOisP}}>N%2^oF8J{Rcm@Ebp4jC*+mQ9;O!H*HPM8b_NvC$ zI6c7|l&oRGDNaWT(JDA9)gokJ3QxB`9GRrt;#-CP`@sfE;q`yf>@2pwwCa)bl$WYo zi-nYC8~gL7VZk_Wd5`fvAsH32j;I$tz?1PO#6_HqcQ$^=>lYZ)1lL!{Tej=7jTddW z$SU8XTzYbZ!vI4T8z>hWEb+^F3fiG;UQ5A8ygtQn7?&AW7@sgcWpptn@k(N&dX2)k zvy}M8SIZu`YO0tkSTMmmx*3Bw*AXF)S2`N&&XQm$1hS%Rf~<&->=yR4Tvf*i<^^8>KrYOVm3J~=A62naUo39c5P5^ zlZ^Q4uDfCa$2%kHJmqgW7dm5tgr9fE)ddpX#c0>Cxaqvp37p|Ae=?S_Ewz`_6RB$t@(oIB!r{F}Z+OqPaJVge(6$S0t*x$TgYVHY z2E{2{FfjNBZFE)3hI9cee7Ub>^B$m^N1*RqW!i-BIvO6yzlW?#DB^=(T&vl9d#Dto~H8gjsB-fAe6AE&f?1oeTa z_z{l})FybyV3rS4bNM25OLpZptMd#VCisl;C4*i!= 1 mm), <= 6000 mm rated range - # |X| <= 6000 mm (±60° cone + safety margin) + # Belt-and-suspenders physical-limits filter, kept in sync with + # the LD2453 ESP32 parser (sign-magnitude 15-bit magnitude + + # nominal range extended to 6.5 m ceiling to avoid false drops + # on real near-max-range targets, see LD2453Parser.cpp §0 and §2). PHYS_Y_MIN = 1 - PHYS_Y_MAX = 6000 - PHYS_X_ABS = 6000 + PHYS_Y_MAX = 6500 + PHYS_X_ABS = 6500 valid_targets_this_frame = 0 targets_data = frame.get("targets", [])