From aca23e91d7f0716a0506ee44d5d2aaab809effce Mon Sep 17 00:00:00 2001 From: Mock Date: Wed, 1 Apr 2026 15:48:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8A=8A=20A=20=E7=AB=AF=E7=9A=84=20Se?= =?UTF-8?q?ssion/KCP/=E8=A7=86=E9=A2=91/=E6=8E=A7=E5=88=B6=20=E9=83=BD?= =?UTF-8?q?=E6=94=B6=E5=8F=A3=E5=88=B0=E4=B8=80=E4=B8=AA=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=20daemon=20=E8=BF=9B=E7=A8=8B=E9=87=8C=EF=BC=8CDjango=20?= =?UTF-8?q?=E5=92=8C=E8=BE=93=E5=85=A5=E5=8F=91=E9=80=81=E7=AB=AF=E9=83=BD?= =?UTF-8?q?=E6=94=B9=E6=88=90=E9=80=9A=E8=BF=87=E6=9C=AC=E6=9C=BA=20UDS=20?= =?UTF-8?q?HTTP=20=E5=8E=BB=E8=AE=BF=E9=97=AE=E5=AE=83=EF=BC=8C=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E8=A1=A5=E9=BD=90=E4=BA=86=E8=A7=82=E6=B5=8B=E3=80=81?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E5=92=8C=E5=8F=AF=E7=94=A8=E6=80=A7=E4=B8=8A?= =?UTF-8?q?=E7=9A=84=E5=87=A0=E4=B8=AA=E5=85=B3=E9=94=AE=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/settings.py | 1 + backend/config/urls.py | 1 + backend/control/__init__.py | 1 + backend/control/apps.py | 6 + backend/control/services.py | 51 +++ backend/control/urls.py | 9 + backend/control/views.py | 59 +++ backend/monitoring/services.py | 423 +++++------------- config/omnisocket_demo.yaml | 4 +- frontend/src/components/NetworkPanel.vue | 5 +- frontend/src/composables/useMonitoringData.ts | 2 +- frontend/src/types.ts | 5 +- 12 files changed, 242 insertions(+), 325 deletions(-) create mode 100644 backend/control/__init__.py create mode 100644 backend/control/apps.py create mode 100644 backend/control/services.py create mode 100644 backend/control/urls.py create mode 100644 backend/control/views.py diff --git a/backend/config/settings.py b/backend/config/settings.py index 0d953b6..2ad72fd 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -17,6 +17,7 @@ INSTALLED_APPS = [ 'rest_framework', 'channels', 'monitoring', + 'control', ] MIDDLEWARE = [ diff --git a/backend/config/urls.py b/backend/config/urls.py index 40d6d83..5112eba 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -4,4 +4,5 @@ from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('monitoring.urls')), + path('api/control/', include('control.urls')), ] diff --git a/backend/control/__init__.py b/backend/control/__init__.py new file mode 100644 index 0000000..ebc2bb8 --- /dev/null +++ b/backend/control/__init__.py @@ -0,0 +1 @@ +default_app_config = "control.apps.ControlConfig" diff --git a/backend/control/apps.py b/backend/control/apps.py new file mode 100644 index 0000000..d010bd7 --- /dev/null +++ b/backend/control/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ControlConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "control" diff --git a/backend/control/services.py b/backend/control/services.py new file mode 100644 index 0000000..eeed097 --- /dev/null +++ b/backend/control/services.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any + + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +WORKSPACE_ROOT = PROJECT_ROOT.parent + + +def _load_client_api(): + try: + from omnisocket_a_side.client import OmniDaemonClient, OmniDaemonError + except ImportError: + python_dir = WORKSPACE_ROOT / "OmniSocketGo" / "python" + if python_dir.exists(): + sys.path.insert(0, str(python_dir)) + from omnisocket_a_side.client import OmniDaemonClient, OmniDaemonError + return OmniDaemonClient, OmniDaemonError + + +_OmniDaemonClient, OmniDaemonError = _load_client_api() +_daemon_client = _OmniDaemonClient() + + +def get_daemon_client(): + return _daemon_client + + +class ControlProxyService: + def get_status(self) -> dict[str, Any]: + return get_daemon_client().get_control_status() + + def send_event( + self, + *, + event_code: str, + drive_value: float = 1.0, + source: str = "django-api", + client_time_ms: int | None = None, + ) -> dict[str, Any]: + return get_daemon_client().send_control_event( + source=source, + event_code=event_code, + drive_value=drive_value, + client_time_ms=client_time_ms, + ) + + +control_service = ControlProxyService() diff --git a/backend/control/urls.py b/backend/control/urls.py new file mode 100644 index 0000000..f557b60 --- /dev/null +++ b/backend/control/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from . import views + + +urlpatterns = [ + path("event/", views.control_event, name="control-event"), + path("status/", views.control_status, name="control-status"), +] diff --git a/backend/control/views.py b/backend/control/views.py new file mode 100644 index 0000000..275b641 --- /dev/null +++ b/backend/control/views.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from rest_framework.decorators import api_view +from rest_framework.response import Response + +from .services import OmniDaemonError, control_service + + +@api_view(["GET"]) +def control_status(request): + try: + return Response(control_service.get_status()) + except OmniDaemonError as error: + return Response( + { + "connected": False, + "queue_depth": 0, + "last_seq_id": None, + "last_error": str(error), + "peer_id": "", + "target_peer": "", + }, + status=503, + ) + + +@api_view(["POST"]) +def control_event(request): + event_code = str(request.data.get("event_code", "")).strip() + if not event_code: + return Response({"error": "event_code is required"}, status=400) + + try: + drive_value = float(request.data.get("drive_value", 1.0)) + except (TypeError, ValueError): + return Response({"error": "drive_value must be numeric"}, status=400) + + raw_client_time_ms = request.data.get("client_time_ms") + if raw_client_time_ms in (None, ""): + client_time_ms = None + else: + try: + client_time_ms = int(raw_client_time_ms) + except (TypeError, ValueError): + return Response({"error": "client_time_ms must be an integer"}, status=400) + + source = str(request.data.get("source", "django-api")).strip() or "django-api" + + try: + payload = control_service.send_event( + event_code=event_code, + drive_value=drive_value, + source=source, + client_time_ms=client_time_ms, + ) + except OmniDaemonError as error: + return Response({"error": str(error)}, status=503) + + return Response(payload, status=200 if payload.get("accepted") else 503) diff --git a/backend/monitoring/services.py b/backend/monitoring/services.py index 58277c1..55053f6 100644 --- a/backend/monitoring/services.py +++ b/backend/monitoring/services.py @@ -2,9 +2,7 @@ from __future__ import annotations import json import math -import os import sys -import threading import time from datetime import UTC, datetime from pathlib import Path @@ -13,320 +11,104 @@ from typing import Any, Iterator PROJECT_ROOT = Path(__file__).resolve().parents[2] WORKSPACE_ROOT = PROJECT_ROOT.parent -JPEG_FRAME_DIR = WORKSPACE_ROOT / "RobotDataShow" / "jpeg-frames" -# GPS 数据 JSON 文件路径 GEOSTREAM_JSON_PATH = WORKSPACE_ROOT / "GeoStream" / "gps_latest.json" GEOSTREAM_STALE_SECONDS = 15 -OMNISOCKET_CONFIG_PATH = PROJECT_ROOT / "config" / "omnisocket_demo.yaml" -VIDEO_SOURCE_MODE = os.getenv("VIDEO_SOURCE_MODE", "auto").strip().lower() -OMNISOCKET_FRAME_FRESH_SECONDS = 2.0 +DAEMON_FRAME_URI = "omni-daemon://latest-frame" def utc_iso_now() -> str: return datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z") -class OmniSocketVideoReceiver: - def __init__(self) -> None: - self._lock = threading.Lock() - self._thread: threading.Thread | None = None - self._started = False - self._session = None - self._session_cls = None - self._binary_msg_type = None - self._video_defaults: dict[str, Any] = {} - self._latest_frame: bytes | None = None - self._latest_received_at = 0.0 - self._latest_sequence: int | None = None - self._frames_received = 0 - self._last_error = "" - self._load_backend() +def _load_daemon_client_api(): + try: + from omnisocket_a_side.client import OmniDaemonClient, OmniDaemonError + except ImportError: + python_dir = WORKSPACE_ROOT / "OmniSocketGo" / "python" + if python_dir.exists(): + sys.path.insert(0, str(python_dir)) + from omnisocket_a_side.client import OmniDaemonClient, OmniDaemonError + return OmniDaemonClient, OmniDaemonError - def _load_backend(self) -> None: - # 服务启动时先尝试导入一次 Python/C 扩展。 - try: - self._import_backend() - except Exception as error: # pragma: no cover - 可选的运行时依赖 - self._last_error = f"omnisocket import failed: {error}" - def _import_backend(self) -> None: - # 优先使用已经安装到当前 Python 环境里的 omnisocket。 - # 如果导入失败,再尝试样例目录下的本地 python 路径。 - try: - from omnisocket import MSG_TYPE_BINARY, Session, VIDEO_DEFAULTS # type: ignore - except ImportError: - python_dir = WORKSPACE_ROOT / "OmniSocketGo" / "python" - if python_dir.exists(): - sys.path.insert(0, str(python_dir)) - from omnisocket import MSG_TYPE_BINARY, Session, VIDEO_DEFAULTS # type: ignore +_OmniDaemonClient, OmniDaemonError = _load_daemon_client_api() +_daemon_client = _OmniDaemonClient() - self._binary_msg_type = MSG_TYPE_BINARY - self._session_cls = Session - self._video_defaults = dict(VIDEO_DEFAULTS) - def ensure_started(self) -> None: - # 当第一次请求帧或状态时,再懒启动后台接收线程。 - if self._session_cls is None or self._binary_msg_type is None: - return +def get_daemon_client(): + return _daemon_client - with self._lock: - if self._started: - return - self._started = True - self._thread = threading.Thread( - target=self._run, - name="omnisocket-video-receiver", - daemon=True, - ) - self._thread.start() - def _load_config(self) -> dict[str, Any]: - # 这里保持和 SampleCData/omnisocket_video_receiver.py 一样的配置结构: - # transport + video_receiver。 - # 即使配置文件不存在,也允许回退到默认值继续运行。 - config: dict[str, Any] = {} - if OMNISOCKET_CONFIG_PATH.exists(): - try: - try: - import yaml # type: ignore - - with OMNISOCKET_CONFIG_PATH.open("r", encoding="utf-8") as file: - config = yaml.safe_load(file) or {} - except ImportError: - # 当前配置文件结构非常简单,缺少 PyYAML 时用简化解析器兜底。 - config = self._load_simple_yaml_config(OMNISOCKET_CONFIG_PATH) - except Exception as error: # pragma: no cover - 可选依赖 - self._last_error = f"config load failed: {error}" - - transport_cfg = dict(config.get("transport", {})) - video_cfg = dict(config.get("video_receiver", {})) - - transport_cfg["server_addr"] = os.getenv( - "OMNISOCKET_SERVER_ADDR", - str(transport_cfg.get("server_addr", "127.0.0.1:10909")), - ) - transport_cfg["relay_via"] = os.getenv( - "OMNISOCKET_RELAY_VIA", - str(transport_cfg.get("relay_via", "")), - ) - transport_cfg["bind_ip"] = os.getenv( - "OMNISOCKET_BIND_IP", - str(transport_cfg.get("bind_ip", "")), - ) - transport_cfg["bind_device"] = os.getenv( - "OMNISOCKET_BIND_DEVICE", - str(transport_cfg.get("bind_device", "")), - ) - video_cfg["peer_id"] = os.getenv( - "OMNISOCKET_VIDEO_PEER_ID", - str(video_cfg.get("peer_id", "peer-a-video")), - ) - video_cfg["buffer_bytes"] = int( - os.getenv( - "OMNISOCKET_BUFFER_BYTES", - str(video_cfg.get("buffer_bytes", 1024 * 1024)), - ) - ) - - return { - "transport": transport_cfg, - "video_receiver": video_cfg, - } - - def _load_simple_yaml_config(self, path: Path) -> dict[str, Any]: - parsed: dict[str, Any] = {} - current_section: str | None = None - - with path.open("r", encoding="utf-8") as file: - for raw_line in file: - line = raw_line.split("#", 1)[0].rstrip() - if not line.strip(): - continue - - if not line.startswith(" "): - if not line.endswith(":"): - raise ValueError(f"invalid top-level yaml line: {raw_line.strip()}") - current_section = line[:-1].strip() - parsed[current_section] = {} - continue - - if current_section is None: - raise ValueError(f"yaml key outside section: {raw_line.strip()}") - - stripped = line.strip() - if ":" not in stripped: - raise ValueError(f"invalid yaml key line: {raw_line.strip()}") - - key, value = stripped.split(":", 1) - parsed[current_section][key.strip()] = self._parse_simple_yaml_scalar(value.strip()) - - return parsed - - def _parse_simple_yaml_scalar(self, value: str) -> Any: - if value in {'""', "''"}: - return "" - if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}: - return value[1:-1] - if value.lower() == "true": - return True - if value.lower() == "false": - return False - if value and value.lstrip("-").isdigit(): - return int(value) - return value - - def _connect_session(self): - # 这里和样例接收器一致:创建 Session(),然后使用 transport/video 配置建立连接。 - assert self._session_cls is not None - - config = self._load_config() - transport_cfg = config.get("transport", {}) - video_cfg = config.get("video_receiver", {}) - - session = self._session_cls() - session.connect( - server_addr=str(transport_cfg.get("server_addr", "127.0.0.1:10909")), - peer_id=str(video_cfg.get("peer_id", "peer-a-video")), - relay_via=str(transport_cfg.get("relay_via", "")), - bind_ip=str(transport_cfg.get("bind_ip", "")), - bind_device=str(transport_cfg.get("bind_device", "")), - **self._video_defaults, - ) - return session, int(video_cfg.get("buffer_bytes", 1024 * 1024)) - - def _extract_jpeg_frame(self, frame: bytes) -> bytes | None: - # 同时兼容两种帧格式: - # 1. 纯 JPEG 二进制 - # 2. 前 8 字节是序号,后面才是真正的 JPEG 数据 - if frame.startswith(b"\xff\xd8"): - return frame - if len(frame) > 8 and frame[8:10] == b"\xff\xd8": - return frame[8:] - return None - - def _extract_sequence(self, frame: bytes) -> int | None: - if len(frame) >= 8 and not frame.startswith(b"\xff\xd8"): - return int.from_bytes(frame[:8], "big") - return None - - def _run(self) -> None: - # 后台持续接收循环: - # connect -> recv_into(buffer) -> 按 body_len 截出有效内容 -> 把最新 JPEG 帧缓存在内存里 - while True: - try: - session, buffer_bytes = self._connect_session() - self._session = session - self._last_error = "" - buffer = bytearray(buffer_bytes) - - while True: - # 从 OmniSocket / C 侧收一帧原始二进制数据到缓冲区 - meta = session.recv_into(buffer, timeout_ms=1000) - if meta is None: - continue - if meta.get("msg_type") != self._binary_msg_type: - continue - - # 真正收到的有效部分切出来,形成当前这一帧 - frame = bytes(buffer[: meta["body_len"]]) - jpeg_frame = self._extract_jpeg_frame(frame) - if jpeg_frame is None: - self._last_error = "received non-JPEG binary frame" - continue - - with self._lock: - # 缓存:这里只保留最新的一张 JPEG 帧,供 Web 接口直接返回给前端。 - self._latest_frame = jpeg_frame - self._latest_received_at = time.time() - self._latest_sequence = self._extract_sequence(frame) - self._frames_received += 1 - except Exception as error: # pragma: no cover - 运行时集成路径 - self._last_error = str(error) - time.sleep(2) - finally: - if self._session is not None: - try: - self._session.close() - except Exception: - pass - self._session = None - - def get_latest_frame(self) -> bytes | None: - # 把内存里最新的一张真实 JPEG 帧暴露给 Django 视图层。 - # 如果这张帧已经过旧,就返回 None,让上层回退到本地模拟帧。 - self.ensure_started() - with self._lock: - if self._latest_frame is None: - return None - if time.time() - self._latest_received_at > OMNISOCKET_FRAME_FRESH_SECONDS: - return None - return self._latest_frame - - def get_status(self) -> dict[str, Any]: - self.ensure_started() - config = self._load_config() - transport_cfg = config.get("transport", {}) - video_cfg = config.get("video_receiver", {}) - with self._lock: - has_recent_frame = self._latest_frame is not None and ( - time.time() - self._latest_received_at <= OMNISOCKET_FRAME_FRESH_SECONDS - ) - return { - "backend_ready": self._session_cls is not None, - "mode": VIDEO_SOURCE_MODE, - "connected": self._session is not None, - "has_recent_frame": has_recent_frame, - "frames_received": self._frames_received, - "latest_sequence": self._latest_sequence, - "last_error": self._last_error, - "config_path": str(OMNISOCKET_CONFIG_PATH), - "server_addr": str(transport_cfg.get("server_addr", "")), - "relay_via": str(transport_cfg.get("relay_via", "")), - "peer_id": str(video_cfg.get("peer_id", "")), - "buffer_bytes": int(video_cfg.get("buffer_bytes", 0)), - } +def _default_receiver(error_message: str) -> dict[str, Any]: + return { + "backend_ready": False, + "mode": "daemon", + "connected": False, + "has_recent_frame": False, + "frames_received": 0, + "latest_sequence": None, + "last_error": error_message, + "config_path": "", + "server_addr": "", + "relay_via": "", + "peer_id": "", + "buffer_bytes": 0, + } class VideoFrameService: - def __init__(self) -> None: - self._receiver = OmniSocketVideoReceiver() - def get_status(self) -> dict[str, Any]: - receiver_status = self._receiver.get_status() - receiver_frame = self._receiver.get_latest_frame() - - # 如果已经收到了真实视频帧,就把当前状态标记为实时模式,给前端显示。 - if receiver_frame is not None: + try: + state = get_daemon_client().get_state() + except OmniDaemonError as error: return { - "available": True, - "source_mode": "omnisocket-jpeg-live", - "frame_count": receiver_status["frames_received"], - "fps": 30, - "frame_dir": str(JPEG_FRAME_DIR), - "source_detail": f"peer stream active, frames={receiver_status['frames_received']}", - "receiver": receiver_status, + "available": False, + "source_mode": "daemon-unavailable", + "frame_count": 0, + "fps": 5, + "frame_dir": DAEMON_FRAME_URI, + "source_detail": str(error), + "receiver": _default_receiver(str(error)), } - wait_detail = receiver_status["last_error"] or ( - "未实时获取真实值,请检查 OmniSocket 服务、视频发送端和接收配置。" - ) + video = dict(state.get("video") or {}) + receiver = dict(video.get("receiver") or {}) + profile = dict((state.get("policy") or {}).get("recommended_video_profile") or {}) + return { - "available": False, - "source_mode": "omnisocket-waiting", - "frame_count": receiver_status["frames_received"], - "fps": 30, - "frame_dir": str(JPEG_FRAME_DIR), - "source_detail": wait_detail, - "receiver": receiver_status, + "available": bool(video.get("available", False)), + "source_mode": str(video.get("source_mode") or "omnisocket-waiting"), + "frame_count": int( + video.get("frame_count", receiver.get("frames_received", 0)) or 0 + ), + "fps": int(video.get("fps", profile.get("fps", 5)) or 5), + "frame_dir": str(video.get("frame_dir") or DAEMON_FRAME_URI), + "source_detail": str( + video.get("source_detail") + or receiver.get("last_error") + or "waiting for latest JPEG frame from daemon" + ), + "receiver": { + "backend_ready": bool(receiver.get("backend_ready", True)), + "mode": str(receiver.get("mode") or "daemon"), + "connected": bool(receiver.get("connected", False)), + "has_recent_frame": bool(receiver.get("has_recent_frame", False)), + "frames_received": int(receiver.get("frames_received", 0) or 0), + "latest_sequence": receiver.get("latest_sequence"), + "last_error": str(receiver.get("last_error") or ""), + "config_path": str(receiver.get("config_path") or ""), + "server_addr": str(receiver.get("server_addr") or ""), + "relay_via": str(receiver.get("relay_via") or ""), + "peer_id": str(receiver.get("peer_id") or ""), + "buffer_bytes": int(receiver.get("buffer_bytes", 0) or 0), + }, } def get_next_frame(self) -> bytes: - # 优先返回从 Python/C 视频接收器拿到的最新真实 JPEG 帧。 - receiver_frame = self._receiver.get_latest_frame() - if receiver_frame is not None: - return receiver_frame - - raise RuntimeError("未实时获取真实值,当前没有可用的真实 JPEG 帧。") + try: + return get_daemon_client().get_video_frame() + except OmniDaemonError as error: + raise RuntimeError(str(error)) from error def iter_mjpeg(self, fps: float = 6.0) -> Iterator[bytes]: frame_interval = 1.0 / max(1.0, min(fps, 30.0)) @@ -344,24 +126,17 @@ class VideoFrameService: class GpsDataService: def get_latest(self) -> dict[str, Any]: - # 优先使用由 GeoStream C 解析器生成的最新数据包。 payload = self._read_geostream_payload() if payload is not None: payload["source_mode"] = "geostream-json" payload["updated_at"] = utc_iso_now() return payload - - # 当没有最新的 GPS 文件可用时,退回到使用一个移动的演示点位。 return self._build_simulated_payload() def _read_geostream_payload(self) -> dict[str, Any] | None: - # Django 后端目前还不直接解析串口数据流。 - # 它只读取由 GeoStream/parse_gps.c 生成的 JSON 文件。 if not GEOSTREAM_JSON_PATH.exists(): return None - # 忽略过期的文件,以便 UI 可以退回到使用模拟数据, - # 而不是把旧位置当作实时位置来显示。 age_seconds = time.time() - GEOSTREAM_JSON_PATH.stat().st_mtime if age_seconds > GEOSTREAM_STALE_SECONDS: return None @@ -373,7 +148,6 @@ class GpsDataService: return None def _build_simulated_payload(self) -> dict[str, Any]: - # 为本地 UI 开发构建一个平滑的伪造轨迹。 tick = time.time() / 12.0 latitude = 31.2304 + math.sin(tick) * 0.0014 longitude = 121.4737 + math.cos(tick) * 0.0018 @@ -395,26 +169,41 @@ class GpsDataService: class NetworkTelemetryService: def get_latest(self) -> dict[str, Any]: - tick = time.time() - - latency_ms = 28 + math.sin(tick / 4.0) * 6 - jitter_ms = 3 + math.cos(tick / 5.0) * 1.4 - tx_kbps = 780 + math.sin(tick / 6.0) * 160 - rx_kbps = 720 + math.cos(tick / 7.0) * 140 - packet_loss_pct = max(0.0, 0.35 + math.sin(tick / 9.0) * 0.25) - signal_dbm = -53 - abs(math.sin(tick / 8.0) * 7) + try: + state = get_daemon_client().get_state() + except OmniDaemonError as error: + return { + "peer_status": "offline", + "latency_ms": 0.0, + "jitter_ms": 0.0, + "retrans_pct": 0.0, + "packet_loss_pct": 0.0, + "tx_kbps": 0, + "rx_kbps": 0, + "signal_dbm": None, + "transport": "OmniSocket / daemon", + "source_mode": "daemon-unavailable", + "updated_at": utc_iso_now(), + "error": str(error), + } + network = dict(state.get("network") or {}) return { - "peer_status": "online", - "latency_ms": round(latency_ms, 1), - "jitter_ms": round(jitter_ms, 1), - "packet_loss_pct": round(packet_loss_pct, 2), - "tx_kbps": int(tx_kbps), - "rx_kbps": int(rx_kbps), - "signal_dbm": round(signal_dbm, 1), - "transport": "OmniSocket / simulated", - "source_mode": "simulated", - "updated_at": utc_iso_now(), + "peer_status": str(network.get("peer_status") or "offline"), + "latency_ms": float(network.get("latency_ms", 0.0) or 0.0), + "jitter_ms": float(network.get("jitter_ms", 0.0) or 0.0), + "retrans_pct": float( + network.get("retrans_pct", network.get("packet_loss_pct", 0.0)) or 0.0 + ), + "packet_loss_pct": float( + network.get("packet_loss_pct", network.get("retrans_pct", 0.0)) or 0.0 + ), + "tx_kbps": int(network.get("tx_kbps", 0) or 0), + "rx_kbps": int(network.get("rx_kbps", 0) or 0), + "signal_dbm": network.get("signal_dbm"), + "transport": str(network.get("transport") or "OmniSocket / daemon"), + "source_mode": str(network.get("source_mode") or "daemon-live"), + "updated_at": str(network.get("updated_at") or utc_iso_now()), } diff --git a/config/omnisocket_demo.yaml b/config/omnisocket_demo.yaml index 531faf8..5ac214d 100644 --- a/config/omnisocket_demo.yaml +++ b/config/omnisocket_demo.yaml @@ -1,6 +1,6 @@ transport: - server_addr: "127.0.0.1:10909" - relay_via: "" + server_addr: "" + relay_via: 106.55.173.235:10909 bind_ip: "" bind_device: "" diff --git a/frontend/src/components/NetworkPanel.vue b/frontend/src/components/NetworkPanel.vue index dc98dc2..6b9e6f4 100644 --- a/frontend/src/components/NetworkPanel.vue +++ b/frontend/src/components/NetworkPanel.vue @@ -35,8 +35,8 @@ const updatedAt = computed(() => { {{ network?.jitter_ms ?? '--' }} ms
- 丢包率 - {{ network?.packet_loss_pct ?? '--' }} % + Retrans + {{ network?.retrans_pct ?? network?.packet_loss_pct ?? '--' }} %
信号强度 @@ -148,4 +148,3 @@ h2 { } } - diff --git a/frontend/src/composables/useMonitoringData.ts b/frontend/src/composables/useMonitoringData.ts index 6c372e3..f55749d 100644 --- a/frontend/src/composables/useMonitoringData.ts +++ b/frontend/src/composables/useMonitoringData.ts @@ -13,7 +13,7 @@ export function useMonitoringData(options: UseMonitoringDataOptions = {}) { const video = ref(null) const loading = ref(true) const errorMessage = ref('') - const refreshIntervalMs = Math.max(200, options.refreshIntervalMs ?? 2000) + const refreshIntervalMs = Math.max(200, options.refreshIntervalMs ?? 500) let refreshTimer: number | null = null diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 987ca97..90f517d 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -16,10 +16,11 @@ export interface NetworkTelemetry { peer_status: string latency_ms: number jitter_ms: number - packet_loss_pct: number + retrans_pct: number + packet_loss_pct?: number tx_kbps: number rx_kbps: number - signal_dbm: number + signal_dbm: number | null transport: string source_mode: string updated_at: string