fix radar web

This commit is contained in:
admin committed 2026-09-11 22:09:39 +07:00
1 parent f5b4ef4e90
commit 8f9a7bae62
3 files changed
+139 -39

No files matched your search

+46 -14
View File
@@ -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 <OrangePi_IP>` 1. **Ping Orange Pi**: `ping <OrangePi_IP>`
2. **Kiểm tra port đang mở**: `ss -tulpn | grep 8000` 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 8000/tcp` hoặc tạm thời `sudo ufw disable` 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` 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://<OrangePi_IP>: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. - 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. - Đả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://<IP>: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://<IP>: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`) - Nếu dùng reverse proxy (Caddy/Nginx): cấu hình WebSocket upgrade (header `Upgrade: websocket`)
--- ---
+49 -15
View File
@@ -664,38 +664,67 @@ function collectEditorPayload() {
return zones; return zones;
} }
/* ---------- WebSocket to backend ---------- */ /* ---------- WebSocket to backend + Polling fallback ---------- */
let wsReconnectTimer = null; 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() { function connectWS() {
const proto = (location.protocol === "https:") ? "wss" : "ws"; const proto = (location.protocol === "https:") ? "wss" : "ws";
const url = `${proto}://${location.host}/ws`; const url = `${proto}://${location.host}/ws`;
updateWsStatus("connecting"); updateWsStatus("connecting");
console.info("[Radar] Connecting WebSocket:", url);
let ws; try {
try { ws = new WebSocket(url); } _activeWs = new WebSocket(url);
catch (e) { scheduleReconnect(); return; } } catch (e) {
console.warn("[Radar] WebSocket construct error:", e);
scheduleReconnect();
return;
}
ws.onopen = () => { _activeWs.onopen = () => {
State.browserWsConnected = true; State.browserWsConnected = true;
updateWsStatus("ok"); 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 { try {
const data = JSON.parse(ev.data); const data = JSON.parse(ev.data);
onBackendFrame(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; State.browserWsConnected = false;
updateWsStatus("err"); updateWsStatus("err");
toast(t("ws_reconnecting"), "warning"); if (wsReconnectTimer == null) toast(t("ws_reconnecting"), "warning");
scheduleReconnect(); scheduleReconnect();
}; };
ws.onerror = () => { _activeWs.onerror = (ev) => {
try { ws.close(); } catch (_) {} console.warn("[Radar] WebSocket error, will reconnect shortly");
try { if (_activeWs) _activeWs.close(); } catch (_) {}
}; };
} }
@@ -841,10 +870,15 @@ document.addEventListener("DOMContentLoaded", () => {
applyI18n(); applyI18n();
initUI(); initUI();
refreshPanels(); refreshPanels();
/* Try to pre-load state via REST in case WS is slow */ /* Pre-load state via REST immediately (before WS even connects) */
fetch("/api/state").then(r => r.ok ? r.json() : null).then(data => { pollFallbackOnce();
if (data) onBackendFrame(data); /* Start safety-net REST poller every 2 s in addition to WebSocket */
}).catch(() => {}); startFallbackPoller();
connectWS(); connectWS();
loop(); 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");
}); });
+44 -10
View File
@@ -83,6 +83,7 @@ class RadarState:
def to_json(self) -> dict: def to_json(self) -> dict:
"""Serialize to something the browser can render.""" """Serialize to something the browser can render."""
now = time.time()
return { return {
"node_id": self.node_id, "node_id": self.node_id,
"sequence": self.sequence, "sequence": self.sequence,
@@ -97,7 +98,12 @@ class RadarState:
"speed_cm_s": t.speed_cm_s, "speed_cm_s": t.speed_cm_s,
"distance_mm": round(t.distance_mm, 1), "distance_mm": round(t.distance_mm, 1),
"angle_deg": round(t.angle_deg, 2), "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 for t in self.targets
], ],
@@ -113,8 +119,13 @@ class RadarState:
for z in self.zones for z in self.zones
], ],
"frames_received": self.frames_received, "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, "esp32_connected": self.esp32_connected,
"server_time": round(now, 2),
} }
def update_from_frame(self, frame: dict): def update_from_frame(self, frame: dict):
@@ -187,6 +198,19 @@ def _update_zone_occupancy_from_targets(state: RadarState):
break 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 # ESP32 WebSocket client — runs as a background task
# ============================================================ # ============================================================
@@ -199,14 +223,20 @@ async def esp32_ws_forever(esp32_host: str):
try: try:
STATE.esp32_connected = False STATE.esp32_connected = False
log.info(f"Đang kết nối ESP32 tại {uri} ...") 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( 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: ) as ws:
STATE.esp32_connected = True STATE.esp32_connected = True
log.info(f"Đã kết nối WebSocket tới ESP32 ({esp32_host})") log.info(f"Đã kết nối WebSocket tới ESP32 ({esp32_host})")
# Fetch zones from ESP32 after successful connect # Fetch zones from ESP32 after successful connect
await fetch_zones_from_esp32(esp32_host) 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: async for message in ws:
try: try:
@@ -217,9 +247,12 @@ async def esp32_ws_forever(esp32_host: str):
STATE.update_from_frame(frame) STATE.update_from_frame(frame)
_update_zone_occupancy_from_targets(STATE) _update_zone_occupancy_from_targets(STATE)
# Always push the latest state
await broadcast_to_browsers() await broadcast_to_browsers()
log.info("ESP32 WebSocket đóng, đang kết nối lại...") 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: except websockets.exceptions.ConnectionClosed as e:
STATE.esp32_connected = False STATE.esp32_connected = False
@@ -325,14 +358,15 @@ TEMPLATES_DIR.mkdir(exist_ok=True)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Start ESP32 client task when app starts, cancel it on shutdown.""" """Start background tasks when app starts, cancel them on shutdown."""
task = asyncio.create_task(esp32_ws_forever(ESP32_HOST)) tasks = [
asyncio.create_task(esp32_ws_forever(ESP32_HOST)),
asyncio.create_task(heartbeat_broadcast()),
]
yield yield
task.cancel() for t in tasks:
try: t.cancel()
await task await asyncio.gather(*tasks, return_exceptions=True)
except asyncio.CancelledError:
pass
app = FastAPI( app = FastAPI(