From 04da39494d54d1336eb1dd6237b925f556512729 Mon Sep 17 00:00:00 2001 From: Tony Tran Date: Sun, 13 Sep 2026 18:27:17 +0700 Subject: [PATCH] fix bug --- .../__pycache__/visualizer.cpython-313.pyc | Bin 41909 -> 41911 bytes orangepi-server/visualizer.py | 207 ++++++++---------- 2 files changed, 93 insertions(+), 114 deletions(-) diff --git a/orangepi-server/__pycache__/visualizer.cpython-313.pyc b/orangepi-server/__pycache__/visualizer.cpython-313.pyc index 3363a4313e860badc4129d13c1c3f61d7fd07b0f..256f1e5696c7937966a52304e104ec5228a70359 100644 GIT binary patch delta 2405 zcmZWrYj6|S71rIAWZ6PSmi4e@0kR$2Z2SU>5higHKL{CPWE+x$pqH!U+wn-=THG5c+dNUen<4KL@m z6TzO9G6XHe0n!q$Dem{sG8nZY$9En5fVIvR{u-i?9Kq6=<_m zrY-4@j65TTsTh@LkCwS{I4TY(aVaTe({>EohOia>-TV{@!Bd+XihYYp85-ze6@`1L z)XyN&FCJ#mvIIZb+|AFs+_19c0)N9_0ykUA`8iJ^IJeaBbGc3kZi#+@tfTM;&{jkKAcHxuIqMZiYHJP3@tO!C$WYHJf ztj$TTV^PYh%sV-7!a(%PozJV?9 zn8Q!rgl@-k#0~E`D%~#XjAaQP&Ds&6tnVR3qT<3uXv&==Bk=3oRT=4?j)2KoO@6A@ zIM11w<3@g&S;oEhLZ2VCnS+swZ9)1hn_x;{No0&Hl-%Kr zr0Mf4MB}%zx5G-M-QnIyl=W#uPp4UWMJ2i;l1Na6MF}R2j}B3d(8Xk!LpCw+`^Uz{ z(p67Gb;(LDi@^4hfiFH_IK!go83di{*HOZ}X&xt0I>kV+d{!nJkh^A#O35UBRTmjY znVv=0G!&NxI6~B}Qq5FNThO+Vf#A|C_#)t|N8M=M$3Wu;qyg$ewVMHm6^+i9(4HBW zrCy2lM-|GL3!+0HRhaM0J$G7FCN6(Vg# z`Ar7tyQ4=;C=t3Hb#r83Kuq?~VpNQMQank&fuczn8c2w=4rN{*>eu*$m=yBTP5c3~ z(nZ^%`aO7UeY;)ntOeT{mIo8`99U}$T)#p8yO@bQ!xBphtMM#6QXAqn5cPQN-?;sP zF+FZLqk3cM2$KXp7+ko=248T|73lFJ6e1KMoJYt&*pF}&0pATOBiumH55t&<%Sw7E zur`{DP>O&pW~IaGR^QTHC?i~CU-=SrwZ4^e-}ZTS9~c0 zu*sWPbf>(IWjrF+{Ws>SyoT^9r~JxD_Q2I)59x$k!)0lL8aPL4NwFFnd4iA$_}XX% z39FYzd445UuVpkfgxl&no}^*iUT>;i*HR>+vpESL?p>9Soo46OgywmC(4<7%SW@*m$fYA5TSF!hTG}_%1Q5h{FnX zVh_4u;#ennS-pO2+)93|e(5C#=hd&DT=b42L@@6Glv?5ND{oXCMtux*yf5`G(s5KL z5cDxEN5@)(UC?v7k~MVd^j2~Z-a9Q69zydb4AtvvK?#pTGxf7SDhgv%J+p({gcCE* qm+T3LCbp>n delta 2393 zcmZXVeQXrR6~K4)Zts(G{B?J>0dsdg&YW$GF<*uxm|zo(gmc0=j%7J zsA!|b+@d+`g;OPIFb7M>F<28kX1Zba!`ne0`7``I_#;vXPp@8EZ9nMRC`Q~xLbsXb zM?A*JYmD5H0v^*5E@IBM810g^h^A2ab^qwUlp-U2< z*;Y|%Y2*pe2y|5hZTq+w7HK;`#a5EmXDhaf<$FQFkKRG3{;qS_HE#}Z-leSb7E!YPouVH0^tZ`km;snb~8 zYIxHQZo*9almX_X%0RR;7Uvr^WURq#ow61`5=*6+IgyNOVTqaI{ge^9G)3;ON*q4l z$jHcI*$T!ifvFmIe&r)6B_2zu+bg5pO2-v6{sEk;_Q33x{RbYDG{7h5Ai^ka2&G{J zZV>H5P>!(edv!5$TuG3^wtS6X*l#JOD@ zVK=iSVw$Wadt~ZCZ5szO^(wv8ZAiRh;Qj4&9u-ISq^fi(v?r!9i7Nf+1bAvg9^-`S z(0R;oi6%5gG-#_`?|K0hl2JK7!&$Y^wX7#L)FDTy99L)puGSWbZwUGyYX|I>OBmrY zH0)_4mjL#(kSv(nQ|^2j)vq94g~G;1UC(hboJ+@1a~0x^6`PsaNdFAi@T2FIB;=G9 zTNG6-*3-)@vEE)eouDOXV(wOwX*vS$HLfT!Qa_KOmMnwA7h$1s?+)x3X2b1_s(mSn zOEcRMrJv%EIXn8}M`cZo(%1m~C3@*-KsuPDDOxN=pMjH4M8r=8ojviDsJG_aGl}$h z*zGI#*;wGt33b?4ry4d3JtA62VgvdCtCgnHV1!c0X-F zG$0<7YgCFghfU4Rw-(O5kQldQsBh&!2M7EL3`b1=eSZ<-F53<`gkZHZX{Y5Kj)^8rP zi6jc29j+r^LoixO`k*NqAh$q?)?18Ioq@Na<>Ytzm(d|R`2$3g_xKIp-uTYfwQ8)956Y8V?vzb+^Nx^Tl56omQ%no|V zb@+0ybTPlTA$MpisnMH;9w+1gz;FfmM4uTpnbzQrFl+tJXzOEb4fGRS-Pm`dtcNfU zBbh$<>UdQl9tQs{)T4R^9VIQM9##2GLbYw)o2A7ghQkM;VB##3y?AW z_*fYse}dOeRFRM1Zzqb$xc=`G!-UtOp4>*x!mm!A4`0OKA``o0CZ=TiFFs6Wm-3aa za%t%uqxYTqG4W2JxtV_d0)m|JU7L(DoaknB!Gl_s^UTB2|3yU088|&2B$N8D$A4dve#!~eWi)S2cp;4r4{cC=O2`j3zEe-9|(cbX-`=H}mX;edHj_T?qRRp?Wu( z8uc}ygh#=o9{xYkct&fdj_^ dict: + """Serialize to something the browser can render.""" + now = time.time() + return { + "node_id": self.node_id, + "sequence": self.sequence, + "timestamp_ms": self.timestamp_ms, + "target_count": self.target_count, + "targets": [ + { + "slot": t.slot, + "valid": t.valid, + "x_mm": t.x_mm, + "y_mm": t.y_mm, + "speed_cm_s": t.speed_cm_s, + "distance_mm": round(t.distance_mm, 1), + "angle_deg": round(t.angle_deg, 2), + "last_seen": ( + round(now - t.last_seen, 2) + if t.valid or (t.last_seen and t.last_seen > 0) + else None + ), + } + for t in self.targets + ], + "zones": [ + { + "id": z.id, + "x1": z.x1, + "y1": z.y1, + "x2": z.x2, + "y2": z.y2, + "occupied": z.occupied, + } + for z in self.zones + ], + "frames_received": self.frames_received, + "last_update": ( + round(now - self.last_update, 2) + if self.last_update and self.last_update > 0 + else None + ), + "esp32_connected": self.esp32_connected, + "server_time": round(now, 2), + } + + async def update_from_frame(self, frame: dict): + self.node_id = frame.get("node_id", self.node_id) + self.sequence = frame.get("sequence", 0) + self.timestamp_ms = frame.get("timestamp", 0) + self.target_count = frame.get("target_count", 0) + now = time.time() + + PHYS_Y_MIN = 1 + PHYS_Y_MAX = 6500 + PHYS_X_ABS = 6500 + 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 = 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)) + t.distance_mm = float(t_data.get("distance", 0.0)) + t.angle_deg = float(t_data.get("angle", 0.0)) + 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 + if t.valid and (t.distance_mm == 0 or t.angle_deg == 0): + t.distance_mm = math.hypot(t.x_mm, t.y_mm) + if t.distance_mm > 0: + t.angle_deg = math.degrees(math.atan2(t.x_mm, t.y_mm)) + if t.valid: + t.last_seen = now + valid_targets_this_frame += 1 + + self.target_count = valid_targets_this_frame + self.frames_received += 1 + self.last_update = now + + store = globals().get("STORE") + if store is not None: + try: + await store.maybe_record(self.targets, now) + except Exception: + pass + # ============================================================ # SQLite-backed detection history store @@ -283,120 +376,6 @@ class TargetStore: } - - def to_json(self) -> dict: - """Serialize to something the browser can render.""" - now = time.time() - return { - "node_id": self.node_id, - "sequence": self.sequence, - "timestamp_ms": self.timestamp_ms, - "target_count": self.target_count, - "targets": [ - { - "slot": t.slot, - "valid": t.valid, - "x_mm": t.x_mm, - "y_mm": t.y_mm, - "speed_cm_s": t.speed_cm_s, - "distance_mm": round(t.distance_mm, 1), - "angle_deg": round(t.angle_deg, 2), - # seconds since this target was last valid; None if never seen - "last_seen": ( - round(now - t.last_seen, 2) - if t.valid or (t.last_seen and t.last_seen > 0) - else None - ), - } - for t in self.targets - ], - "zones": [ - { - "id": z.id, - "x1": z.x1, - "y1": z.y1, - "x2": z.x2, - "y2": z.y2, - "occupied": z.occupied, - } - for z in self.zones - ], - "frames_received": self.frames_received, - "last_update": ( - round(now - self.last_update, 2) - if self.last_update and self.last_update > 0 - else None - ), - "esp32_connected": self.esp32_connected, - "server_time": round(now, 2), - } - - async def update_from_frame(self, frame: dict): - self.node_id = frame.get("node_id", self.node_id) - self.sequence = frame.get("sequence", 0) - self.timestamp_ms = frame.get("timestamp", 0) - self.target_count = frame.get("target_count", 0) - now = time.time() - - # 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 = 6500 - PHYS_X_ABS = 6500 - 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 = 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) - if t.distance_mm > 0: - t.angle_deg = math.degrees(math.atan2(t.x_mm, t.y_mm)) - - 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 - - # Persist valid targets into the SQLite history (dedup per slot). - # We reference the singleton STORE from the FastAPI lifecycle; this - # indirection keeps RadarState pure (no knowledge of DB engine). - store = globals().get("STORE") - if store is not None: - try: - await store.maybe_record(self.targets, now) - except Exception: - # Never let a DB write failure break the realtime radar loop. - pass - - # ============================================================ # Global app state # ============================================================