Files
StarPilot/openpilot/starpilot/ui/navigation_state.py
T
2026-10-19 21:23:03 -05:00

86 lines
3.7 KiB
Python

from dataclasses import dataclass
import math
@dataclass(frozen=True)
class NavigationDisplay:
key: tuple[str, int]
text: str
maneuver_type: str
modifier: str
distance_m: float
remaining_distance_m: float
remaining_seconds: float
arrived: bool = False
def navigation_display(message, *, now_ns: int, drive_id: int) -> NavigationDisplay | None:
if message is None or drive_id <= 0:
return None
try:
if (not message.enabled or message.status not in ('guiding', 'arrived') or
message.startedMonoTime != drive_id or not message.sessionId or
not drive_id <= message.frameMonoTime <= now_ns <= message.frameMonoTime + 3_000_000_000 or
not drive_id <= message.locationMonoTime <= now_ns <= message.locationMonoTime + 3_000_000_000):
return None
instruction = message.instruction
distances = (instruction.distanceMeters, instruction.remainingDistanceMeters, instruction.remainingDurationSeconds)
if not all(math.isfinite(value) and value >= 0 for value in distances):
return None
text = 'You have arrived' if message.status == 'arrived' else ' '.join(instruction.text.split())[:240]
if not text:
return None
return NavigationDisplay((message.sessionId, drive_id), text, instruction.maneuverType,
instruction.maneuverModifier, *distances, message.status == 'arrived')
except (AttributeError, TypeError, ValueError, OverflowError):
return None
def distance_text(meters: float, metric: bool) -> str:
if metric:
if meters < 1000:
step = 25 if meters < 500 else 50
return f'{round(meters / step) * step} m'
kilometers = meters / 1000
return f'{kilometers:.0f} km' if kilometers >= 10 else f'{kilometers:.1f} km'
feet = meters * 3.28084
if feet < 1000:
step = 10 if feet <= 100 else 50
return f'{round(feet / step) * step} ft'
miles = feet / 5280
return f'{miles:.0f} mi' if miles >= 10 else f'{miles:.1f} mi'
def official_navigation_display(instruction, route, *, destination, drive_id: int) -> NavigationDisplay | None:
"""Read-only presentation of comma nav alpha; never rewrites StarPilot navigation or map/SLC sources.
The caller supplies transport-valid, fresh, post-transition messages. This
adapter only uses a route with a destination and tags its display identity.
"""
if instruction is None or route is None or destination is None or drive_id <= 0:
return None
try:
latitude, longitude = destination['latitude'], destination['longitude']
if not (-90 <= latitude <= 90 and -180 <= longitude <= 180):
return None
coordinates = route.coordinates
if len(coordinates) < 2:
return None
endpoints = tuple((point.longitude, point.latitude) for point in (coordinates[0], coordinates[-1]))
if not all(-180 <= lon <= 180 and -90 <= lat <= 90 for lon, lat in endpoints):
return None
distances = (instruction.maneuverDistance, instruction.distanceRemaining, instruction.timeRemaining)
if not all(math.isfinite(value) for value in distances):
return None
text = ' '.join(instruction.maneuverPrimaryText.split())[:240]
if not text:
return None
# Destination and route endpoints keep card collapse stable across periodic
# route resends, but reset it for a new trip or changed route.
identity = f'comma:{longitude},{latitude}:{len(coordinates)}:{endpoints}'
return NavigationDisplay((identity, drive_id), text, instruction.maneuverType,
instruction.maneuverModifier, max(0., distances[0]),
max(0., distances[1]), max(0., distances[2]))
except (AttributeError, KeyError, TypeError, ValueError, OverflowError):
return None