fix radar web
This commit is contained in:
1 parent
f5b4ef4e90
commit
8f9a7bae62
3 files changed
+139
-39
No files matched your search
+46
-14
@@ -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>`
|
||||
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://<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.
|
||||
- **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://<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`)
|
||||
|
||||
---
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user