From 8f9a7bae623d74bee788f79839f3fd4b9936e681 Mon Sep 17 00:00:00 2001 From: Tony Tran Date: Fri, 11 Sep 2026 22:09:39 +0700 Subject: [PATCH] fix radar web --- orangepi-server/README.md | 60 ++++++++++++++++++++++++-------- orangepi-server/static/app.js | 64 +++++++++++++++++++++++++++-------- orangepi-server/visualizer.py | 54 +++++++++++++++++++++++------ 3 files changed, 139 insertions(+), 39 deletions(-) diff --git a/orangepi-server/README.md b/orangepi-server/README.md index 4ddd12b..1b2c1fe 100644 --- a/orangepi-server/README.md +++ b/orangepi-server/README.md @@ -249,28 +249,60 @@ journalctl -u radar-web -f # Xem log realtime --- -## 7. Troubleshooting +## 7. Troubleshooting — Kiểm tra lỗi nhanh -### Không mở được trang web +### 7.1 Quick check trong 60 giây +Nếu backend log "Đã kết nối WebSocket tới ESP32" nhưng web không thấy dữ liệu, làm theo thứ tự: + +1. **Mở DevTools (F12) → tab Console**: + - Gõ: `await fetch('/api/state').then(r=>r.json()).then(d=>console.log(d))` + - Kết quả đúng phải là object có `zones` và `targets` (3 phần tử mỗi mảng) + - Nếu nhận được object: WebSocket frontend đang có vấn đề (hãy xem log có chữ `[Radar] WebSocket closed?`) + - Nếu không nhận được: backend có vấn đề (xem log terminal backend) + +2. **Xem 2 chip trạng thái trên header (góc trên phải)**: + - `ESP32: Đã kết nối` (xanh) — backend ↔ ESP32 OK + - `WS: Đã kết nối` (xanh) — trình duyệt ↔ backend OK + - Nếu WS màu đỏ: mở DevTools → Network → WS → chọn `/ws` → xem "Messages" có dòng nào không. + +3. **Hiển thị gì phải có ngay cả khi parser ESP32 là stub?** + - Grid, vòng tròn khoảng cách, radar (điểm đỏ dưới cùng), **3 zones theo mặc định phải hiện rõ ràng** + - Chip `ESP32` phải xanh, `Frames` trong Thống kê = 0 là bình thường (parser stub) + - **Target không thấy là Bình thường** nếu parser chưa implement — nhưng zones & grid PHẢI hiện. + +--- + +### 7.2 Không mở được trang web 1. **Ping Orange Pi**: `ping ` -2. **Kiểm tra port đang mở**: `ss -tulpn | grep 8000` -3. **Tường lửa?** `sudo ufw allow 8000/tcp` hoặc tạm thời `sudo ufw disable` +2. **Kiểm tra port đang mở**: `ss -tulpn | grep 9000` (thay `9000` bằng port bạn dùng) +3. **Tường lửa?** `sudo ufw allow 9000/tcp` hoặc tạm thời `sudo ufw disable` 4. **Visualizer đang chạy?** `ps aux | grep visualizer` -### Không thấy dữ liệu radar (không có target) -1. **Kiểm tra trạng thái ESP32** (chip màu đỏ/xanh trên cùng bên phải trang web) -2. **Ping ESP32**: `ping 192.168.1.100` -3. **Test REST ESP32**: `curl http://192.168.1.100/status` → phải có JSON `{"status":"ok",...}` -4. **ESP32 đang DIAGNOSTIC_MODE 1?** → phải là `0` mới có WiFi/WebSocket -5. **Parser ESP32 vẫn là stub?** → Target luôn `INVALID` nếu parser chưa triển khai (thông báo ở README gốc) +--- -### Tốc độ cập nhật chậm +### 7.3 Không thấy dữ liệu radar / không có target +1. **Kiểm tra 2 chip trạng thái** (section 7.1) +2. **Ping ESP32**: `ping 192.168.88.32` (thay IP của bạn) +3. **Test REST ESP32**: `curl http://192.168.88.32/status` → phải trả JSON `{"status":"ok",...}` +4. **Test state từ backend (ngoài trình duyệt)**: `curl http://:9000/api/state` → object JSON 2 mảng zones + targets +5. **ESP32 đang `DIAGNOSTIC_MODE 1`?** → phải là `0` mới có WiFi/WebSocket +6. **Parser ESP32 vẫn là stub?** → **Target luôn = 0 / INVALID là BÌNH THƯỜNG** cho đến khi bạn implement `tryParseFrame()` thật trong `LD2453Parser.cpp`. Grid + zones vẫn phải hiện. + +--- + +### 7.4 Tốc độ cập nhật chậm - Mặc định radar LD2453 gửi ~2 frame/giây. Đó là giới hạn phần cứng. +- **Bảo đảm Orange Pi gửi state liên tục**: heartbeat 1 giây luôn gửi ngay cả khi ESP32 không gửi frame mới. - Đảm bảo Orange Pi và ESP32 có tín hiệu WiFi tốt. -### WebSocket không kết nối (trình duyệt báo lỗi) -- Mở DevTools (F12) → Console xem lỗi chi tiết -- Kiểm tra trình duyệt truy cập đúng `http://:8000` (không phải `https://`) +--- + +### 7.5 WebSocket không kết nối (trình duyệt báo lỗi) +- Mở DevTools (F12) → **Console** sẽ log rõ ràng: `[Radar] WebSocket closed (code=...)` + - `code=1006`: mạng bị ngắt giữa chừng → tự động reconnect 2s sau + - `code=403/404`: mở sai port hoặc sai path +- Kiểm tra trình duyệt truy cập đúng `http://:9000` (**KHÔNG** `https://`) +- Nếu mở trên **mobile cùng WiFi** mà không được: tắt "VPN / Private DNS / Use random MAC" trên điện thoại thử lại - Nếu dùng reverse proxy (Caddy/Nginx): cấu hình WebSocket upgrade (header `Upgrade: websocket`) --- diff --git a/orangepi-server/static/app.js b/orangepi-server/static/app.js index 74f412d..fd15361 100644 --- a/orangepi-server/static/app.js +++ b/orangepi-server/static/app.js @@ -664,38 +664,67 @@ function collectEditorPayload() { return zones; } -/* ---------- WebSocket to backend ---------- */ +/* ---------- WebSocket to backend + Polling fallback ---------- */ let wsReconnectTimer = null; +let fallbackPollTimer = null; +let _activeWs = null; + +async function pollFallbackOnce() { + /* Try REST /api/state as a safety net even while WebSocket might be blocked */ + try { + const r = await fetch("/api/state", { cache: "no-store" }); + if (r.ok) { + const d = await r.json(); + onBackendFrame(d); + } + } catch (_) {} +} + +function startFallbackPoller() { + if (fallbackPollTimer) return; + /* Every 2 s also poll via REST, to cover browser cases where WS silently drops */ + fallbackPollTimer = setInterval(pollFallbackOnce, 2000); +} function connectWS() { const proto = (location.protocol === "https:") ? "wss" : "ws"; const url = `${proto}://${location.host}/ws`; updateWsStatus("connecting"); + console.info("[Radar] Connecting WebSocket:", url); - let ws; - try { ws = new WebSocket(url); } - catch (e) { scheduleReconnect(); return; } + try { + _activeWs = new WebSocket(url); + } catch (e) { + console.warn("[Radar] WebSocket construct error:", e); + scheduleReconnect(); + return; + } - ws.onopen = () => { + _activeWs.onopen = () => { State.browserWsConnected = true; updateWsStatus("ok"); + console.info("[Radar] WebSocket connected"); + /* Ask for first state immediately via REST too, in case the WS initial send was lost */ + pollFallbackOnce(); }; - ws.onmessage = (ev) => { + _activeWs.onmessage = (ev) => { try { const data = JSON.parse(ev.data); onBackendFrame(data); - } catch (e) { console.warn("WS parse error", e); } + } catch (e) { console.warn("[Radar] WS parse error", e); } }; - ws.onclose = () => { + _activeWs.onclose = (ev) => { + console.warn("[Radar] WebSocket closed (code=" + ev.code + " reason=" + ev.reason + ")"); State.browserWsConnected = false; updateWsStatus("err"); - toast(t("ws_reconnecting"), "warning"); + if (wsReconnectTimer == null) toast(t("ws_reconnecting"), "warning"); scheduleReconnect(); }; - ws.onerror = () => { - try { ws.close(); } catch (_) {} + _activeWs.onerror = (ev) => { + console.warn("[Radar] WebSocket error, will reconnect shortly"); + try { if (_activeWs) _activeWs.close(); } catch (_) {} }; } @@ -841,10 +870,15 @@ document.addEventListener("DOMContentLoaded", () => { applyI18n(); initUI(); refreshPanels(); - /* Try to pre-load state via REST in case WS is slow */ - fetch("/api/state").then(r => r.ok ? r.json() : null).then(data => { - if (data) onBackendFrame(data); - }).catch(() => {}); + /* Pre-load state via REST immediately (before WS even connects) */ + pollFallbackOnce(); + /* Start safety-net REST poller every 2 s in addition to WebSocket */ + startFallbackPoller(); connectWS(); loop(); + + console.info("%c[Radar Visualizer ready]", + "color:#89b4fa; font-weight:bold; padding:4px 8px; border-radius:6px; background:#181825;"); + console.info(" - Open DevTools → Network → WS → /ws to see live messages"); + console.info(" - If canvas is empty: check Console for errors, then check /api/state in browser"); }); diff --git a/orangepi-server/visualizer.py b/orangepi-server/visualizer.py index 99ca17e..3bf5e64 100644 --- a/orangepi-server/visualizer.py +++ b/orangepi-server/visualizer.py @@ -83,6 +83,7 @@ class RadarState: def to_json(self) -> dict: """Serialize to something the browser can render.""" + now = time.time() return { "node_id": self.node_id, "sequence": self.sequence, @@ -97,7 +98,12 @@ class RadarState: "speed_cm_s": t.speed_cm_s, "distance_mm": round(t.distance_mm, 1), "angle_deg": round(t.angle_deg, 2), - "last_seen": round(time.time() - t.last_seen, 2) if t.last_seen else None, + # 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 ], @@ -113,8 +119,13 @@ class RadarState: for z in self.zones ], "frames_received": self.frames_received, - "last_update": round(time.time() - self.last_update, 2) if self.last_update else None, + "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), } def update_from_frame(self, frame: dict): @@ -187,6 +198,19 @@ def _update_zone_occupancy_from_targets(state: RadarState): break +# ============================================================ +# Heartbeat: broadcast last known state to browsers every 1s +# even if ESP32 hasn't sent a new frame yet (parser stub scenario). +# ============================================================ +async def heartbeat_broadcast(): + while True: + try: + await broadcast_to_browsers() + except Exception: + pass + await asyncio.sleep(1.0) + + # ============================================================ # ESP32 WebSocket client — runs as a background task # ============================================================ @@ -199,14 +223,20 @@ async def esp32_ws_forever(esp32_host: str): try: STATE.esp32_connected = False log.info(f"Đang kết nối ESP32 tại {uri} ...") + # Push initial "not connected" state to any browser that joined early + await broadcast_to_browsers() + async with websockets.connect( - uri, ping_interval=30, ping_timeout=10, close_timeout=5 + uri, ping_interval=30, ping_timeout=10, close_timeout=5, + open_timeout=8, max_size=1_000_000, ) as ws: STATE.esp32_connected = True log.info(f"Đã kết nối WebSocket tới ESP32 ({esp32_host})") # Fetch zones from ESP32 after successful connect await fetch_zones_from_esp32(esp32_host) + # Broadcast immediately so browsers see zones right after connect + await broadcast_to_browsers() async for message in ws: try: @@ -217,9 +247,12 @@ async def esp32_ws_forever(esp32_host: str): STATE.update_from_frame(frame) _update_zone_occupancy_from_targets(STATE) + # Always push the latest state await broadcast_to_browsers() log.info("ESP32 WebSocket đóng, đang kết nối lại...") + STATE.esp32_connected = False + await broadcast_to_browsers() except websockets.exceptions.ConnectionClosed as e: STATE.esp32_connected = False @@ -325,14 +358,15 @@ TEMPLATES_DIR.mkdir(exist_ok=True) @asynccontextmanager async def lifespan(app: FastAPI): - """Start ESP32 client task when app starts, cancel it on shutdown.""" - task = asyncio.create_task(esp32_ws_forever(ESP32_HOST)) + """Start background tasks when app starts, cancel them on shutdown.""" + tasks = [ + asyncio.create_task(esp32_ws_forever(ESP32_HOST)), + asyncio.create_task(heartbeat_broadcast()), + ] yield - task.cancel() - try: - await task - except asyncio.CancelledError: - pass + for t in tasks: + t.cancel() + await asyncio.gather(*tasks, return_exceptions=True) app = FastAPI(