162 lines
5.9 KiB
Python
162 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import threading
|
|
import time
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from .common import GEOSTREAM_JSON_PATH, GEOSTREAM_STALE_SECONDS, utc_iso_now
|
|
from .control import ControlArbiter, NativeUdpControlIngress, OmniSocketControlSender
|
|
from .video import OmniSocketVideoReceiver
|
|
|
|
|
|
class GpsDataService:
|
|
def get_latest(self) -> dict[str, Any]:
|
|
payload = self._read_geostream_payload()
|
|
if payload is not None:
|
|
payload["source_mode"] = "geostream-json"
|
|
payload["updated_at"] = utc_iso_now()
|
|
return payload
|
|
|
|
return self._build_simulated_payload()
|
|
|
|
def _read_geostream_payload(self) -> dict[str, Any] | None:
|
|
if not GEOSTREAM_JSON_PATH.exists():
|
|
return None
|
|
|
|
age_seconds = time.time() - GEOSTREAM_JSON_PATH.stat().st_mtime
|
|
if age_seconds > GEOSTREAM_STALE_SECONDS:
|
|
return None
|
|
|
|
try:
|
|
with GEOSTREAM_JSON_PATH.open("r", encoding="utf-8") as file:
|
|
return json.load(file)
|
|
except (OSError, json.JSONDecodeError):
|
|
return None
|
|
|
|
def _build_simulated_payload(self) -> dict[str, Any]:
|
|
tick = time.time() / 12.0
|
|
latitude = 31.2304 + math.sin(tick) * 0.0014
|
|
longitude = 121.4737 + math.cos(tick) * 0.0018
|
|
|
|
return {
|
|
"has_fix": True,
|
|
"utc_time": datetime.now(UTC).strftime("%H:%M:%S"),
|
|
"latitude": round(latitude, 6),
|
|
"longitude": round(longitude, 6),
|
|
"satellites": 14 + int((math.sin(tick * 0.7) + 1.0) * 2),
|
|
"altitude_m": round(6.5 + math.cos(tick * 0.5) * 1.2, 2),
|
|
"coordinate_system": "WGS84",
|
|
"source_sentence": "SIMULATED",
|
|
"raw_coordinate_format": "decimal degrees",
|
|
"source_mode": "simulated",
|
|
"updated_at": utc_iso_now(),
|
|
}
|
|
|
|
|
|
class NetworkTelemetryService:
|
|
def __init__(
|
|
self,
|
|
video_receiver: OmniSocketVideoReceiver,
|
|
control_sender: OmniSocketControlSender,
|
|
control_arbiter: ControlArbiter,
|
|
native_ingress: NativeUdpControlIngress,
|
|
) -> None:
|
|
self._video_receiver = video_receiver
|
|
self._control_sender = control_sender
|
|
self._control_arbiter = control_arbiter
|
|
self._native_ingress = native_ingress
|
|
self._rate_lock = threading.Lock()
|
|
self._last_rate_sample: tuple[float, int, int] | None = None
|
|
|
|
def _compute_rates(self, send_bytes: int, recv_bytes: int) -> tuple[float, float]:
|
|
now = time.monotonic()
|
|
with self._rate_lock:
|
|
previous = self._last_rate_sample
|
|
self._last_rate_sample = (now, send_bytes, recv_bytes)
|
|
|
|
if previous is None:
|
|
return 0.0, 0.0
|
|
|
|
prev_time, prev_send, prev_recv = previous
|
|
elapsed = now - prev_time
|
|
if elapsed <= 0.0:
|
|
return 0.0, 0.0
|
|
|
|
tx_kbps = max(0.0, ((send_bytes - prev_send) * 8.0) / elapsed / 1000.0)
|
|
rx_kbps = max(0.0, ((recv_bytes - prev_recv) * 8.0) / elapsed / 1000.0)
|
|
return tx_kbps, rx_kbps
|
|
|
|
def get_latest(self) -> dict[str, Any]:
|
|
self._video_receiver.ensure_started()
|
|
self._control_arbiter.ensure_started()
|
|
self._native_ingress.ensure_started()
|
|
|
|
video_app = self._video_receiver.session_stats()
|
|
control_app = self._control_sender.session_stats()
|
|
video_kcp = self._video_receiver.session_kcp_stats()
|
|
control_kcp = self._control_sender.session_kcp_stats()
|
|
arbiter_status = self._control_arbiter.get_status()
|
|
ingress_status = self._native_ingress.get_status()
|
|
sender_status = self._control_sender.get_status()
|
|
|
|
total_send_bytes = int(video_app.get("send_bytes", 0)) + int(control_app.get("send_bytes", 0))
|
|
total_recv_bytes = int(video_app.get("recv_bytes", 0)) + int(control_app.get("recv_bytes", 0))
|
|
tx_kbps, rx_kbps = self._compute_rates(total_send_bytes, total_recv_bytes)
|
|
|
|
video_connected = int(video_app.get("connected", 0))
|
|
control_connected = int(control_app.get("connected", 0))
|
|
connected_sessions = video_connected + control_connected
|
|
|
|
primary_kcp = control_kcp if control_connected else video_kcp
|
|
latency_ms = primary_kcp.get("srtt_ms")
|
|
jitter_ms = primary_kcp.get("srttvar_ms")
|
|
|
|
if connected_sessions > 0:
|
|
peer_status = "online"
|
|
elif sender_status.get("backend_ready"):
|
|
peer_status = "idle"
|
|
else:
|
|
peer_status = "backend-unavailable"
|
|
|
|
return {
|
|
"peer_status": peer_status,
|
|
"latency_ms": latency_ms,
|
|
"jitter_ms": jitter_ms,
|
|
"packet_loss_pct": None,
|
|
"tx_kbps": round(tx_kbps, 3),
|
|
"rx_kbps": round(rx_kbps, 3),
|
|
"transport": "OmniSocket / kcp",
|
|
"source_mode": "omnisocket-live" if connected_sessions > 0 else "omnisocket-idle",
|
|
"updated_at": utc_iso_now(),
|
|
"active_control_source": arbiter_status["active_source"],
|
|
"control_lease_remaining_ms": arbiter_status["control_lease_remaining_ms"],
|
|
"combined": {
|
|
"connected_sessions": connected_sessions,
|
|
"send_bytes": total_send_bytes,
|
|
"recv_bytes": total_recv_bytes,
|
|
"tx_kbps": round(tx_kbps, 3),
|
|
"rx_kbps": round(rx_kbps, 3),
|
|
},
|
|
"sessions": {
|
|
"video": {
|
|
"app": video_app,
|
|
"kcp": video_kcp,
|
|
},
|
|
"control": {
|
|
"app": control_app,
|
|
"kcp": control_kcp,
|
|
},
|
|
},
|
|
"ingress": {
|
|
"native_udp": ingress_status,
|
|
},
|
|
"control": {
|
|
"arbiter": arbiter_status,
|
|
"sender": sender_status,
|
|
},
|
|
}
|
|
|