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)