diff --git a/orangepi-server/__pycache__/visualizer.cpython-313.pyc b/orangepi-server/__pycache__/visualizer.cpython-313.pyc index 3363a43..256f1e5 100644 Binary files a/orangepi-server/__pycache__/visualizer.cpython-313.pyc and b/orangepi-server/__pycache__/visualizer.cpython-313.pyc differ diff --git a/orangepi-server/visualizer.py b/orangepi-server/visualizer.py index 62811fb..2a734d1 100644 --- a/orangepi-server/visualizer.py +++ b/orangepi-server/visualizer.py @@ -86,6 +86,99 @@ class RadarState: last_update: float = 0.0 esp32_connected: bool = False + 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), + "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 # ============================================================