Files
full-stack-fastapi/backend/app/mqtt/repository.py
魏风 e81aadcd99
All checks were successful
Deploy to Production / deploy (push) Successful in 1m10s
feat: add realtime GNSS monitor with telemetry websocket
2026-03-20 11:48:04 +08:00

238 lines
7.9 KiB
Python

from __future__ import annotations
import hashlib
from datetime import datetime
from typing import Any
from sqlmodel import Session, col, select
from app.models import (
TerminalDeviceState,
TerminalMessageRaw,
get_datetime_utc,
)
def insert_raw_message(
*,
session: Session,
device_id: str,
topic: str,
payload: bytes,
qos: int,
retain: bool,
received_at: datetime | None = None,
) -> TerminalMessageRaw:
ts = received_at or get_datetime_utc()
row = TerminalMessageRaw(
device_id=device_id,
topic=topic,
received_at=ts,
payload_bytes=payload,
payload_size=len(payload),
payload_sha256=hashlib.sha256(payload).hexdigest(),
qos=qos,
retain=retain,
created_at=ts,
)
session.add(row)
session.commit()
session.refresh(row)
return row
def fetch_pending_rows_for_update(
*, session: Session, batch_size: int
) -> list[TerminalMessageRaw]:
statement = (
select(TerminalMessageRaw)
.where(col(TerminalMessageRaw.parsed_at).is_(None))
.where(col(TerminalMessageRaw.parse_error).is_(None))
.order_by(col(TerminalMessageRaw.received_at))
.limit(batch_size)
)
bind = session.get_bind()
if bind is not None and bind.dialect.name != "sqlite":
statement = statement.with_for_update(skip_locked=True)
rows = session.exec(statement).all()
return list(rows)
def mark_succeeded(*, session: Session, row: TerminalMessageRaw) -> None:
row.parsed_at = get_datetime_utc()
row.parse_error = None
session.add(row)
def mark_failed(
*,
session: Session,
row: TerminalMessageRaw,
parse_error: str,
) -> None:
row.parsed_at = get_datetime_utc()
row.parse_error = parse_error[:1024]
session.add(row)
def _build_satellite_signal_summary(
*,
gsv_satellites: list[dict[str, Any]],
satellites_in_view: int | None,
used_satellite_ids: set[str],
) -> dict[str, Any]:
deduped: dict[tuple[str, str], dict[str, Any]] = {}
for satellite in gsv_satellites:
constellation = str(satellite.get("constellation", "Unknown"))
satellite_id = str(satellite.get("satellite_id", "")).strip()
if not satellite_id:
continue
key = (constellation, satellite_id)
existing = deduped.get(key)
if existing is None:
deduped[key] = satellite
continue
incoming_snr = satellite.get("snr_dbhz")
existing_snr = existing.get("snr_dbhz")
if isinstance(incoming_snr, int) and (
not isinstance(existing_snr, int) or incoming_snr > existing_snr
):
deduped[key] = satellite
satellites = list(deduped.values())
satellites.sort(
key=lambda sat: (str(sat["constellation"]), str(sat["satellite_id"]))
)
snr_values: list[int] = []
cn0_values: list[int] = []
constellation_summary: dict[str, dict[str, int]] = {}
for satellite in gsv_satellites:
cn0_value = satellite.get("snr_dbhz")
if isinstance(cn0_value, int):
cn0_values.append(cn0_value)
for satellite in satellites:
constellation = str(satellite["constellation"])
sat_id = str(satellite["satellite_id"])
used_in_navigation = sat_id in used_satellite_ids
satellite["used_in_navigation"] = used_in_navigation
constellation_item = constellation_summary.setdefault(
constellation,
{"count": 0, "with_signal": 0, "used_in_navigation": 0},
)
constellation_item["count"] += 1
if used_in_navigation:
constellation_item["used_in_navigation"] += 1
snr_dbhz = satellite.get("snr_dbhz")
if isinstance(snr_dbhz, int):
snr_values.append(snr_dbhz)
constellation_item["with_signal"] += 1
return {
"satellites_in_view": satellites_in_view or len(satellites),
"snr_values": snr_values,
"cn0_values": cn0_values,
"used_satellite_ids": sorted(used_satellite_ids),
"constellations": constellation_summary,
"satellites": satellites,
}
def update_device_state_from_events(
*,
session: Session,
row: TerminalMessageRaw,
events: list[dict[str, Any]],
) -> None:
if not events:
state = session.get(TerminalDeviceState, row.device_id)
if state is None:
state = TerminalDeviceState(device_id=row.device_id)
state.last_event_time = row.received_at
state.updated_at = get_datetime_utc()
session.add(state)
return
state = session.get(TerminalDeviceState, row.device_id)
if state is None:
state = TerminalDeviceState(device_id=row.device_id)
gsv_satellites: list[dict[str, Any]] = []
satellites_in_view: int | None = None
used_satellite_ids: set[str] = set()
for event in events:
payload = event.get("payload", {})
event_type = str(event.get("event_type", "unknown"))
if event_type == "nmea_gga":
state.lat = payload.get("lat")
state.lon = payload.get("lon")
state.alt_m = payload.get("altitude_m")
state.satellites_used = payload.get("satellites_used")
fix_quality = payload.get("fix_quality")
if fix_quality is not None:
state.fix_status = str(fix_quality)
elif event_type == "nmea_rmc":
if payload.get("lat") is not None:
state.lat = payload.get("lat")
if payload.get("lon") is not None:
state.lon = payload.get("lon")
status = payload.get("status")
if status is not None and state.fix_status is None:
state.fix_status = str(status)
elif event_type == "nmea_gsv":
payload_satellites = payload.get("satellites", [])
if isinstance(payload_satellites, list):
for sat in payload_satellites:
if not isinstance(sat, dict):
continue
gsv_satellites.append(
{
"satellite_id": str(sat.get("satellite_id", "")),
"elevation_deg": sat.get("elevation_deg"),
"azimuth_deg": sat.get("azimuth_deg"),
"snr_dbhz": sat.get("snr_dbhz"),
"constellation": payload.get("constellation", "Unknown"),
}
)
satellites_count = payload.get("satellites_in_view")
if isinstance(satellites_count, int):
satellites_in_view = max(satellites_in_view or 0, satellites_count)
elif event_type == "nmea_gsa":
for satellite_id in payload.get("used_satellite_ids", []):
if satellite_id is None:
continue
used_satellite_ids.add(str(satellite_id))
elif event_type == "rtcm_summary":
state.rtcm_type_summary = payload.get("rtcm_types", {})
if gsv_satellites:
state.satellite_signal_summary = _build_satellite_signal_summary(
gsv_satellites=gsv_satellites,
satellites_in_view=satellites_in_view,
used_satellite_ids=used_satellite_ids,
)
elif used_satellite_ids and state.satellite_signal_summary:
current_summary = dict(state.satellite_signal_summary)
current_summary["used_satellite_ids"] = sorted(used_satellite_ids)
satellites = current_summary.get("satellites", [])
if isinstance(satellites, list):
for satellite in satellites:
if not isinstance(satellite, dict):
continue
sat_id = str(satellite.get("satellite_id", ""))
satellite["used_in_navigation"] = sat_id in used_satellite_ids
state.satellite_signal_summary = current_summary
state.last_event_time = row.received_at
state.updated_at = get_datetime_utc()
session.add(state)