238 lines
7.9 KiB
Python
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)
|