mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
144 lines
6.0 KiB
Python
144 lines
6.0 KiB
Python
import json
|
|
import math
|
|
import threading
|
|
import time
|
|
|
|
FIELDS = {"latitude", "longitude", "altitude", "speed", "bearing", "horizontalAccuracy", "verticalAccuracy",
|
|
"speedAccuracy", "bearingAccuracy", "timestamp", "ageMs"}
|
|
MAX_AGE_MS = 2000
|
|
|
|
|
|
def _number(payload, key, required=False):
|
|
value = payload.get(key)
|
|
if value is None and not required:
|
|
return math.nan
|
|
if type(value) not in (int, float) or not math.isfinite(value):
|
|
raise ValueError(f"Invalid {key}")
|
|
return float(value)
|
|
|
|
|
|
def _optional(payload, key, low, high):
|
|
# Phones report an unknown value as null or a negative number.
|
|
value = _number(payload, key)
|
|
return value if low <= value < high else math.nan
|
|
|
|
|
|
def phone_gps(payload, now_ns):
|
|
if not isinstance(payload, dict) or set(payload) - FIELDS:
|
|
raise ValueError("Unexpected location fields")
|
|
latitude, longitude = _number(payload, "latitude", True), _number(payload, "longitude", True)
|
|
accuracy, age_ms = _number(payload, "horizontalAccuracy", True), _number(payload, "ageMs", True)
|
|
if not (-90 <= latitude <= 90 and -180 <= longitude <= 180) or (latitude == 0 and longitude == 0):
|
|
raise ValueError("Invalid position")
|
|
if not 0 < accuracy <= 10_000:
|
|
raise ValueError("Invalid accuracy")
|
|
if not 0 <= age_ms <= MAX_AGE_MS:
|
|
raise ValueError("Location is too old")
|
|
timestamp = payload.get("timestamp", 0)
|
|
if type(timestamp) is not int or not 0 <= timestamp < 2 ** 53:
|
|
raise ValueError("Invalid timestamp")
|
|
bearing_accuracy = _optional(payload, "bearingAccuracy", 0, 180)
|
|
return {
|
|
"sourceMonoTime": now_ns - int(age_ms * 1e6),
|
|
"latitude": latitude,
|
|
"longitude": longitude,
|
|
"altitude": _optional(payload, "altitude", -1000, 10_000),
|
|
"speed": _optional(payload, "speed", 0, 100),
|
|
"bearingDeg": _optional(payload, "bearing", 0, 360) if math.isfinite(bearing_accuracy) else math.nan,
|
|
"horizontalAccuracy": accuracy,
|
|
"verticalAccuracy": _optional(payload, "verticalAccuracy", 0, 10_000),
|
|
"bearingAccuracyDeg": bearing_accuracy,
|
|
"speedAccuracy": _optional(payload, "speedAccuracy", 0, 100),
|
|
"unixTimestampMillis": timestamp,
|
|
"hasFix": True,
|
|
}
|
|
|
|
|
|
def handoff_path():
|
|
import tempfile
|
|
from pathlib import Path
|
|
shm = Path("/dev/shm")
|
|
return (shm if shm.is_dir() else Path(tempfile.gettempdir())) / "starpilot_phone_gps.json"
|
|
|
|
|
|
class PhoneGpsPublisher:
|
|
"""Hands the newest validated phone fix to navigationd, which publishes it inside starpilotNavigation."""
|
|
|
|
def __init__(self, path=None, mono_clock=time.monotonic_ns):
|
|
self._path, self._mono_clock = path, mono_clock
|
|
self._lock = threading.Lock()
|
|
|
|
def publish(self, payload):
|
|
from openpilot.starpilot.gps.source import PHONE_MAX_AGE_NS, MAX_HORIZONTAL_ACCURACY
|
|
path = self._path or handoff_path()
|
|
with self._lock:
|
|
now = self._mono_clock()
|
|
try:
|
|
gps = phone_gps(payload, now)
|
|
except ValueError as error:
|
|
self._status(path, now, False, str(error))
|
|
raise
|
|
# Keep the last usable measurement through a brief accuracy dropout, without
|
|
# changing its timestamp. It will still expire at the original deadline.
|
|
if gps["horizontalAccuracy"] > MAX_HORIZONTAL_ACCURACY:
|
|
return self._status(path, now, False, "poorAccuracy")
|
|
previous = read_phone_gps(now, path)
|
|
if previous is not None:
|
|
stamp, old = previous
|
|
timestamp, old_timestamp = gps["unixTimestampMillis"], old.unixTimestampMillis
|
|
if gps["sourceMonoTime"] <= stamp or (timestamp and old_timestamp and timestamp <= old_timestamp):
|
|
return self._status(path, now, False, "outOfOrder")
|
|
# Within a live stream, differences between phone timestamps reveal added
|
|
# queue delay without assuming the phone's wall clock matches comma's.
|
|
if timestamp and old_timestamp:
|
|
elapsed = (timestamp - old_timestamp) * 1_000_000
|
|
if elapsed <= PHONE_MAX_AGE_NS:
|
|
gps["sourceMonoTime"] = min(gps["sourceMonoTime"], stamp + elapsed)
|
|
if now - gps["sourceMonoTime"] > MAX_AGE_MS * 1_000_000:
|
|
return self._status(path, now, False, "delayed")
|
|
temporary = path.with_name(path.name + ".tmp")
|
|
temporary.write_text(json.dumps(gps))
|
|
temporary.replace(path)
|
|
self._status(path, now, True, "accepted")
|
|
return {"ok": True}
|
|
|
|
@staticmethod
|
|
def _status(path, now, accepted, reason):
|
|
status = path.with_name(path.name + ".status")
|
|
temporary = status.with_name(status.name + ".tmp")
|
|
temporary.write_text(json.dumps({"receivedMonoTime": now, "accepted": accepted, "reason": reason}))
|
|
temporary.replace(status)
|
|
return {"ok": True, "accepted": accepted, "reason": reason}
|
|
|
|
|
|
def phone_gps_status(now_ns=None, path=None):
|
|
"""Coordinate-free diagnostics; a successful upload need not be a usable fix."""
|
|
now_ns = time.monotonic_ns() if now_ns is None else now_ns
|
|
path = path or handoff_path()
|
|
try:
|
|
value = json.loads(path.with_name(path.name + ".status").read_text())
|
|
received = value["receivedMonoTime"]
|
|
if type(received) is not int or not 0 < received <= now_ns:
|
|
return None
|
|
fix = read_phone_gps(now_ns, path)
|
|
return {"reason": value["reason"], "accepted": value["accepted"], "usable": fix is not None,
|
|
"receivedAgeMs": (now_ns - received) / 1e6,
|
|
"fixAgeMs": (now_ns - fix[0]) / 1e6 if fix is not None else None}
|
|
except (OSError, ValueError, KeyError, TypeError, AttributeError, OverflowError):
|
|
return None
|
|
|
|
|
|
def read_phone_gps(now_ns, path=None):
|
|
"""The handed-off fix as a Gps-like object, or None when absent, malformed or older than the phone hold."""
|
|
from types import SimpleNamespace
|
|
from openpilot.starpilot.gps.source import PHONE_MAX_AGE_NS, valid_fix
|
|
try:
|
|
with (path or handoff_path()).open("rb") as source:
|
|
gps = json.loads(source.read(1 << 16))
|
|
stamp = gps["sourceMonoTime"]
|
|
if type(stamp) is not int:
|
|
return None
|
|
return valid_fix(SimpleNamespace(**gps), stamp, stamp, now_ns, PHONE_MAX_AGE_NS)
|
|
except (OSError, ValueError, KeyError, TypeError, AttributeError, OverflowError):
|
|
return None
|