mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-21 21:12:06 +08:00
2775 lines
119 KiB
Python
Executable File
2775 lines
119 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
import atexit
|
|
import ipaddress
|
|
import logging
|
|
import math
|
|
import multiprocessing
|
|
import os
|
|
import pty
|
|
import queue
|
|
import re
|
|
import signal
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import termios
|
|
import time
|
|
from dataclasses import dataclass
|
|
import fcntl
|
|
|
|
import numpy as np
|
|
import pyray as rl
|
|
|
|
import cereal.messaging as messaging
|
|
from msgq.visionipc import VisionStreamType
|
|
from openpilot.common.basedir import BASEDIR
|
|
from openpilot.common.transformations.camera import DEVICE_CAMERAS, view_frame_from_device_frame
|
|
from openpilot.common.transformations.orientation import rot_from_euler
|
|
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
from openpilot.tools.replay.lib.ui_helpers import Calibration, plot_model
|
|
from opendbc.can import CANParser
|
|
from opendbc.car import structs
|
|
from opendbc.car.hyundai.radar_interface import HYUNDAI_RADAR_TRACK_SPECS, RadarInterface
|
|
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP
|
|
|
|
os.environ["BASEDIR"] = BASEDIR
|
|
|
|
WINDOW_WIDTH = 1600
|
|
WINDOW_HEIGHT = 900
|
|
CAMERA_WIDTH = 960
|
|
CONTENT_HEIGHT = 720
|
|
CAMERA_BUFFER_WIDTH = 640
|
|
CAMERA_BUFFER_HEIGHT = 480
|
|
MAX_FORWARD_DISTANCE = 160.0
|
|
MAX_LATERAL_DISTANCE = 30.0
|
|
FUSED_CAMERA_ZOOM = 1.7
|
|
FUSED_STREAM_GRACE_SECONDS = 1.0
|
|
REMOTE_BRIDGE_TIMEOUT_SECONDS = 1.5
|
|
REPLAY_EXECUTABLE = os.path.join(BASEDIR, "tools/replay/replay")
|
|
MESSAGING_BRIDGE_EXECUTABLE = os.path.join(BASEDIR, "cereal/messaging/bridge")
|
|
CAMERA_RELAY_EXECUTABLE = os.path.join(BASEDIR, "tools/camerastream/compressed_vipc.py")
|
|
CAMERA_RELAY_BOOTSTRAP = "".join((
|
|
"import multiprocessing, runpy, sys; ",
|
|
"multiprocessing.set_start_method('fork', force=True); ",
|
|
"path = sys.argv.pop(1); ",
|
|
"runpy.run_path(path, run_name='__main__')",
|
|
))
|
|
REMOTE_SERVICES = (
|
|
"can", "carParams", "carState", "liveCalibration", "liveTracks", "modelV2", "radarState",
|
|
"roadCameraState", "wideRoadCameraState", "roadEncodeData", "wideRoadEncodeData",
|
|
)
|
|
ACTIVE_REMOTE_RELAYS: list[subprocess.Popen] = []
|
|
|
|
BACKGROUND = rl.Color(11, 16, 24, 255)
|
|
PANEL = rl.Color(17, 25, 36, 255)
|
|
GRID = rl.Color(69, 82, 99, 110)
|
|
TEXT = rl.Color(236, 241, 247, 255)
|
|
MUTED = rl.Color(154, 168, 184, 255)
|
|
CYAN = rl.Color(72, 220, 255, 255)
|
|
GREEN = rl.Color(0, 255, 126, 255)
|
|
ORANGE = rl.Color(255, 164, 48, 255)
|
|
RED = rl.Color(255, 76, 89, 255)
|
|
WHITE = rl.Color(255, 255, 255, 255)
|
|
PURPLE = rl.Color(190, 125, 255, 255)
|
|
|
|
SPEC_BY_RANGE = {
|
|
(spec.start_addr, end_addr): spec
|
|
for spec in HYUNDAI_RADAR_TRACK_SPECS
|
|
for end_addr in {spec.required_end_addr, spec.end_addr}
|
|
}
|
|
SPEC_BY_NAME = {spec.name: spec for spec in HYUNDAI_RADAR_TRACK_SPECS}
|
|
PREFERRED_RADAR_SOURCE = "RADAR_3A5_3C4"
|
|
DEFAULT_SOURCE_FILTERS = (False, False, False) # hide moving, stationary, unknown
|
|
RadarSourceKey = tuple[int, int, int]
|
|
RADAR_DETAIL_SIGNALS = {
|
|
"RADAR_210_21F": {
|
|
f"{prefix}{signal}"
|
|
for prefix in ("1_", "2_")
|
|
for signal in (
|
|
"MOTION_STATE", "TRACK_QUALITY", "AGE", "COAST_AGE", "STATE", "STATE_ALT", "RCS",
|
|
"REL_LAT_SPEED", "NEW_SIGNAL_4", "NEW_SIGNAL_18", "OBJECT_ID",
|
|
)
|
|
},
|
|
"RADAR_3A5_3C4": {
|
|
"MOTION_STATE", "TRACK_QUALITY", "AGE", "COAST_AGE", "STATE", "STATE_ALT", "RCS",
|
|
"REL_LAT_SPEED", "ABS_SPEED", "WIDTH", "LENGTH", "ORIENTATION_ANGLE", "TRACK_COUNTER",
|
|
"NEW_SIGNAL_4", "NEW_SIGNAL_5", "NEW_SIGNAL_12", "NEW_SIGNAL_13", "NEW_SIGNAL_14",
|
|
"NEW_SIGNAL_15", "NEW_SIGNAL_16", "NEW_SIGNAL_17", "NEW_SIGNAL_18",
|
|
},
|
|
}
|
|
TABLE_MODES = ("motion", "kinematics", "object", "signals")
|
|
PLAYBACK_SPEEDS = (0.2, 0.5, 1.0, 2.0, 4.0, 8.0)
|
|
TRACK_COUNT_FIELD_WIDTH = 3
|
|
SOURCE_CIRCLE_RADIUS_SCALE = 0.8
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ResearchSourceSpec:
|
|
name: str
|
|
start_address: int
|
|
end_address: int
|
|
message_sizes: tuple[int, ...]
|
|
details: str
|
|
|
|
|
|
RESEARCH_SOURCE_SPECS = (
|
|
ResearchSourceSpec("CCNC_FUSED_OBJECTS", 0x162, 0x162, (32,), "fused cluster objects, not raw radar 32 B"),
|
|
ResearchSourceSpec("RADAR_500_STATUS_CORE", 0x4E0, 0x4E1, (8,) * 2, "front-radar status 20 Hz ESR/shared 4E0"),
|
|
ResearchSourceSpec("RADAR_500_BUILD", 0x4E2, 0x4E2, (8,), "optional build metadata 20 Hz"),
|
|
ResearchSourceSpec("RADAR_500_PATHS", 0x4E3, 0x4E3, (8,), "front-radar path selectors/status 20 Hz"),
|
|
ResearchSourceSpec("RADAR_500_HEALTH", 0x4E4, 0x4E5, (8,) * 2, "optional ADC/alignment status 20 Hz"),
|
|
ResearchSourceSpec("RADAR_602_ALT", 0x612, 0x617, (8,) * 6, "alternate legacy front objects 8 B"),
|
|
ResearchSourceSpec("CORNER_A_OBJECTS", 0x240, 0x24F, (16, *([24] * 15)), "right-front corner objects (inferred) 20 Hz"),
|
|
ResearchSourceSpec("CORNER_A_DETECTIONS", 0x270, 0x277, (32,) * 8, "right-front compact scan bins (inferred) 20 Hz"),
|
|
ResearchSourceSpec("CORNER_B_OBJECTS", 0x278, 0x287, (8, *([24] * 15)), "left-front corner objects (inferred) 20 Hz"),
|
|
ResearchSourceSpec("CORNER_B_DETECTIONS", 0x288, 0x28F, (32,) * 8, "left-front compact scan bins (inferred) 20 Hz"),
|
|
ResearchSourceSpec("RADAR_RAW_DETECTIONS", 0x3D0, 0x3D4, (32,) * 5, "front-radar auxiliary scan records 20 Hz 32 B"),
|
|
ResearchSourceSpec("CAMERA_LANE_PATH", 0x360, 0x366, (32,) * 7, "forward-camera lane/path geometry 32 B"),
|
|
)
|
|
RESEARCH_SPEC_BY_RANGE = {(spec.start_address, spec.end_address): spec for spec in RESEARCH_SOURCE_SPECS}
|
|
|
|
|
|
@dataclass
|
|
class ManagedReplay:
|
|
process: subprocess.Popen
|
|
master_fd: int
|
|
paused: bool = False
|
|
playback_speed: float = 1.0
|
|
progress_current: int = 0
|
|
progress_total: int = 0
|
|
car_fingerprint: str = ""
|
|
terminal_buffer: str = ""
|
|
|
|
def poll(self):
|
|
return self.process.poll()
|
|
|
|
def send(self, keys: str) -> None:
|
|
if self.master_fd >= 0 and self.poll() is None:
|
|
try:
|
|
os.write(self.master_fd, keys.encode())
|
|
except OSError:
|
|
pass
|
|
if keys == " ":
|
|
self.paused = not self.paused
|
|
elif keys == "+":
|
|
self.playback_speed = next(
|
|
(speed for speed in PLAYBACK_SPEEDS if speed > self.playback_speed),
|
|
self.playback_speed,
|
|
)
|
|
elif keys == "-":
|
|
self.playback_speed = next(
|
|
(speed for speed in reversed(PLAYBACK_SPEEDS) if speed < self.playback_speed),
|
|
self.playback_speed,
|
|
)
|
|
|
|
def drain_output(self) -> None:
|
|
if self.master_fd < 0:
|
|
return
|
|
output = bytearray()
|
|
while True:
|
|
try:
|
|
chunk = os.read(self.master_fd, 65536)
|
|
if not chunk:
|
|
break
|
|
output.extend(chunk)
|
|
except BlockingIOError:
|
|
break
|
|
except OSError:
|
|
break
|
|
if output:
|
|
self.terminal_buffer = (self.terminal_buffer + output.decode(errors="ignore"))[-65536:]
|
|
progress_matches = list(re.finditer(r"PROGRESS:(\d+):(\d+)", self.terminal_buffer))
|
|
if progress_matches:
|
|
self.progress_current = int(progress_matches[-1].group(1))
|
|
self.progress_total = int(progress_matches[-1].group(2))
|
|
fingerprint_matches = list(re.finditer(r"Car Fingerprint:\s*([^\r\n\x1b]+)", self.terminal_buffer))
|
|
if fingerprint_matches:
|
|
self.car_fingerprint = fingerprint_matches[-1].group(1).strip()
|
|
|
|
def close_terminal(self) -> None:
|
|
if self.master_fd >= 0:
|
|
os.close(self.master_fd)
|
|
self.master_fd = -1
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ReplayTimelineEntry:
|
|
start: float
|
|
end: float
|
|
kind: str
|
|
title: str = ""
|
|
detail: str = ""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ReplayTimeline:
|
|
start: float = 0.0
|
|
end: float = 0.0
|
|
entries: tuple[ReplayTimelineEntry, ...] = ()
|
|
car_fingerprint: str = ""
|
|
|
|
|
|
@dataclass
|
|
class ReplayScrubState:
|
|
active: bool = False
|
|
preview_seconds: float | None = None
|
|
last_seek_at: float = 0.0
|
|
last_seek_second: int | None = None
|
|
pending_seconds: float | None = None
|
|
|
|
|
|
def start_route_replay(route: str) -> ManagedReplay:
|
|
master_fd, slave_fd = pty.openpty()
|
|
try:
|
|
fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, struct.pack("HHHH", 40, 140, 0, 0))
|
|
os.set_blocking(master_fd, False)
|
|
env = os.environ.copy()
|
|
env["TERM"] = "xterm-256color"
|
|
process = subprocess.Popen(
|
|
[REPLAY_EXECUTABLE, "--ecam", route],
|
|
cwd=BASEDIR,
|
|
env=env,
|
|
stdin=slave_fd,
|
|
stdout=slave_fd,
|
|
stderr=slave_fd,
|
|
close_fds=True,
|
|
)
|
|
except Exception:
|
|
os.close(master_fd)
|
|
raise
|
|
finally:
|
|
os.close(slave_fd)
|
|
return ManagedReplay(process, master_fd)
|
|
|
|
|
|
def stop_route_replay(replay: ManagedReplay | None) -> None:
|
|
if replay is None:
|
|
return
|
|
if replay.poll() is None:
|
|
replay.process.terminate()
|
|
try:
|
|
replay.process.wait(timeout=2.0)
|
|
except subprocess.TimeoutExpired:
|
|
replay.process.kill()
|
|
replay.process.wait(timeout=1.0)
|
|
replay.close_terminal()
|
|
|
|
|
|
def stop_remote_relays(processes: list[subprocess.Popen]) -> None:
|
|
stopped_processes = list(processes)
|
|
for process in reversed(stopped_processes):
|
|
if process.poll() is not None:
|
|
continue
|
|
process.send_signal(signal.SIGINT)
|
|
try:
|
|
process.wait(timeout=2.0)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait(timeout=1.0)
|
|
ACTIVE_REMOTE_RELAYS[:] = [
|
|
process for process in ACTIVE_REMOTE_RELAYS
|
|
if all(process is not stopped_process for stopped_process in stopped_processes)
|
|
]
|
|
if processes is not ACTIVE_REMOTE_RELAYS:
|
|
processes.clear()
|
|
|
|
|
|
def start_remote_relays(address: str) -> list[subprocess.Popen]:
|
|
processes: list[subprocess.Popen] = []
|
|
try:
|
|
processes.append(subprocess.Popen([
|
|
MESSAGING_BRIDGE_EXECUTABLE,
|
|
address,
|
|
",".join(REMOTE_SERVICES),
|
|
], cwd=BASEDIR))
|
|
processes.append(subprocess.Popen([
|
|
sys.executable,
|
|
"-c",
|
|
CAMERA_RELAY_BOOTSTRAP,
|
|
CAMERA_RELAY_EXECUTABLE,
|
|
"127.0.0.1",
|
|
"--cams", "0,2",
|
|
"--silent",
|
|
], cwd=BASEDIR))
|
|
except BaseException:
|
|
stop_remote_relays(processes)
|
|
raise
|
|
ACTIVE_REMOTE_RELAYS.extend(processes)
|
|
return processes
|
|
|
|
|
|
atexit.register(stop_remote_relays, ACTIVE_REMOTE_RELAYS)
|
|
|
|
|
|
def replace_route_replay(process: ManagedReplay | None, route: str) -> tuple[ManagedReplay | None, str]:
|
|
route = route.strip()
|
|
if not route:
|
|
return process, "Enter a route or segment ID."
|
|
stop_route_replay(process)
|
|
try:
|
|
replay = start_route_replay(route)
|
|
print(f"Radar debugger opened route: {route}", flush=True)
|
|
return replay, ""
|
|
except OSError as exc:
|
|
return None, f"Could not start replay: {exc}"
|
|
|
|
|
|
def route_start_seconds(route: str) -> float:
|
|
match = re.search(r"(?:--|/)(\d+)(?::[^/]*)?(?:/[qra])?$", route)
|
|
return int(match.group(1)) * 60.0 if match is not None else 0.0
|
|
|
|
|
|
def parse_live_ip(value: str) -> str | None:
|
|
try:
|
|
return str(ipaddress.ip_address(value.strip()))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def replay_timeline_worker(route: str, output_queue) -> None:
|
|
from openpilot.tools.lib.logreader import LogReader, ReadMode
|
|
|
|
entries: list[list[float | str]] = []
|
|
engagement_index = None
|
|
alert_index = None
|
|
alert_signature = None
|
|
first_mono_time = None
|
|
route_offset = route_start_seconds(route)
|
|
end_time = route_offset
|
|
car_fingerprint = ""
|
|
try:
|
|
for message in LogReader(route, default_mode=ReadMode.QLOG, only_union_types=True):
|
|
mono_time = int(message.logMonoTime)
|
|
if first_mono_time is None:
|
|
first_mono_time = mono_time
|
|
seconds = route_offset + (mono_time - first_mono_time) / 1e9
|
|
end_time = max(end_time, seconds)
|
|
message_type = message.which()
|
|
if message_type == "carParams":
|
|
car_fingerprint = str(message.carParams.carFingerprint)
|
|
elif message_type == "userBookmark":
|
|
entries.append([seconds, seconds, "bookmark", "USER TAG", ""])
|
|
elif message_type == "selfdriveState":
|
|
state = message.selfdriveState
|
|
if engagement_index is not None:
|
|
entries[engagement_index][1] = seconds
|
|
if state.enabled:
|
|
if engagement_index is None:
|
|
engagement_index = len(entries)
|
|
entries.append([seconds, seconds, "engaged", "", ""])
|
|
else:
|
|
engagement_index = None
|
|
|
|
if alert_index is not None:
|
|
entries[alert_index][1] = seconds
|
|
if int(state.alertSize.raw) != 0:
|
|
alert_kind = ("info", "warning", "critical")[int(state.alertStatus.raw)]
|
|
next_signature = (alert_kind, str(state.alertText1), str(state.alertText2))
|
|
if alert_signature != next_signature:
|
|
alert_index = len(entries)
|
|
alert_signature = next_signature
|
|
entries.append([seconds, seconds, alert_kind, str(state.alertText1), str(state.alertText2)])
|
|
else:
|
|
alert_index = None
|
|
alert_signature = None
|
|
timeline_entries = tuple(
|
|
ReplayTimelineEntry(float(start), float(end), str(kind), str(title), str(detail))
|
|
for start, end, kind, title, detail in entries
|
|
)
|
|
output_queue.put(ReplayTimeline(route_offset, end_time, timeline_entries, car_fingerprint))
|
|
except Exception:
|
|
output_queue.put(ReplayTimeline(route_offset, end_time, (), car_fingerprint))
|
|
|
|
|
|
def stop_timeline_process(process) -> None:
|
|
if process is not None and process.is_alive():
|
|
process.terminate()
|
|
process.join(timeout=1.0)
|
|
|
|
|
|
def start_timeline_process(process_context, process, output_queue, route: str):
|
|
stop_timeline_process(process)
|
|
while True:
|
|
try:
|
|
output_queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
process = process_context.Process(target=replay_timeline_worker, args=(route, output_queue), daemon=True)
|
|
process.start()
|
|
return process
|
|
|
|
|
|
def format_download_progress(current: int, total: int) -> str:
|
|
if total <= 0:
|
|
return "--"
|
|
percent = int(np.clip(current / total, 0.0, 1.0) * 100)
|
|
return f"{current / (1024 * 1024):.1f}/{total / (1024 * 1024):.1f} MB ({percent}%)"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DisplayTrack:
|
|
trackId: int
|
|
dRel: float
|
|
yRel: float
|
|
vRel: float
|
|
aRel: float
|
|
canAddress: int = -1
|
|
canBus: int = -1
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DisplaySource:
|
|
startAddress: int
|
|
endAddress: int
|
|
bus: int
|
|
trackCount: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DisplayTrackSignals:
|
|
trackId: int
|
|
relLatSpeed: float
|
|
absSpeed: float
|
|
width: float
|
|
length: float
|
|
orientationAngle: float
|
|
age: int
|
|
coastAge: int
|
|
state: int
|
|
stateAlt: int
|
|
trackCounter: int
|
|
signalSummary: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DisplayRawSignal:
|
|
startAddress: int
|
|
endAddress: int
|
|
address: int
|
|
bus: int
|
|
label: str
|
|
signalSummary: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RadarSnapshot:
|
|
points: tuple[DisplayTrack, ...] = ()
|
|
trackSources: tuple[DisplaySource, ...] = ()
|
|
trackSignals: tuple[DisplayTrackSignals, ...] = ()
|
|
rawSignals: tuple[DisplayRawSignal, ...] = ()
|
|
radarTracksAvailable: bool = False
|
|
|
|
|
|
class ResearchCanDecoder:
|
|
"""Inventory auxiliary families without publishing them as control-facing radar tracks."""
|
|
|
|
def __init__(self):
|
|
self.payloads: dict[tuple[str, int], dict[int, bytes]] = {}
|
|
|
|
def update(self, can_messages) -> None:
|
|
for address, dat, bus in can_messages:
|
|
for spec in RESEARCH_SOURCE_SPECS:
|
|
if not spec.start_address <= address <= spec.end_address:
|
|
continue
|
|
expected_size = spec.message_sizes[address - spec.start_address]
|
|
if len(dat) == expected_size:
|
|
self.payloads.setdefault((spec.name, int(bus)), {})[int(address)] = bytes(dat)
|
|
|
|
@staticmethod
|
|
def _packed_detection_records(payload: bytes) -> tuple[int, ...]:
|
|
return tuple(
|
|
int.from_bytes(payload[offset:offset + 4], "little")
|
|
for offset in (3, 8, 12, 16, 20, 24, 28)
|
|
)
|
|
|
|
@staticmethod
|
|
def _signed(raw: int, size: int) -> int:
|
|
return raw - (1 << size) if raw & (1 << (size - 1)) else raw
|
|
|
|
@classmethod
|
|
def _little(cls, raw: int, start: int, size: int, *, signed: bool = False) -> int:
|
|
value = (raw >> start) & ((1 << size) - 1)
|
|
return cls._signed(value, size) if signed else value
|
|
|
|
@classmethod
|
|
def _big(cls, raw: int, start: int, size: int, *, signed: bool = False) -> int:
|
|
value = 0
|
|
bit = start
|
|
for _ in range(size):
|
|
value = (value << 1) | ((raw >> bit) & 1)
|
|
bit = bit + 15 if bit % 8 == 0 else bit - 1
|
|
return cls._signed(value, size) if signed else value
|
|
|
|
@classmethod
|
|
def _corner_object(cls, payload: bytes) -> tuple[float, float, float] | None:
|
|
raw = int.from_bytes(payload, "little")
|
|
raw_dist = (raw >> 63) & 0x1FFF
|
|
if raw_dist == 0xFFE:
|
|
return None
|
|
|
|
d_rel = raw_dist * 0.05
|
|
# Exclude the high-range default/sentinel region and points outside the debugger plot.
|
|
if not 0 < d_rel <= MAX_FORWARD_DISTANCE:
|
|
return None
|
|
|
|
y_rel = cls._signed((raw >> 76) & 0x7FF, 11) * 0.05
|
|
v_rel = cls._signed((raw >> 88) & 0x3FF, 10) * 0.2
|
|
return d_rel, y_rel, v_rel
|
|
|
|
@classmethod
|
|
def _radar_500_status_summary(cls, address: int, raw: int) -> str:
|
|
if address == 0x4E0:
|
|
return " ".join((
|
|
f"CTR:{cls._little(raw, 6, 2)}",
|
|
f"TS:{cls._big(raw, 5, 7) * 2}ms",
|
|
f"SCAN:{cls._big(raw, 31, 16)}",
|
|
f"SPD:{cls._big(raw, 50, 11) * 0.0625:.2f}m/s",
|
|
f"YAW:{cls._big(raw, 47, 12, signed=True) * 0.0625:.2f}deg/s",
|
|
f"CURV:{cls._big(raw, 13, 14, signed=True)}m",
|
|
f"COMM:{cls._big(raw, 14, 1)}",
|
|
))
|
|
if address == 0x4E1:
|
|
return " ".join((
|
|
f"CTR:{cls._big(raw, 1, 2)}",
|
|
f"MAX:{cls._big(raw, 7, 6) + 1}",
|
|
f"STEER:{cls._big(raw, 10, 11)}",
|
|
f"RAW:{cls._big(raw, 11, 1)}",
|
|
f"OP:{cls._big(raw, 12, 1)}",
|
|
f"ERR:{cls._big(raw, 13, 1)}/{cls._big(raw, 14, 1)}/{cls._big(raw, 15, 1)}",
|
|
f"TEMP:{cls._big(raw, 31, 8, signed=True)}C",
|
|
f"GROUP:{cls._big(raw, 33, 2)}",
|
|
f"VER:{cls._big(raw, 55, 16):04X}",
|
|
))
|
|
if address == 0x4E2:
|
|
values = tuple(cls._little(raw, byte * 8, 8) for byte in range(8))
|
|
return " ".join((
|
|
f"BUILD:{values[0]:02X}-{values[1]:02X}-{values[2]:02X}",
|
|
f"{values[3]:02X}:{values[4]:02X}",
|
|
f"U5-7:{values[5]:02X}/{values[6]:02X}/{values[7]:02X}",
|
|
))
|
|
if address == 0x4E3:
|
|
return " ".join((
|
|
f"CTR:{cls._big(raw, 1, 2)}",
|
|
f"MODE:{cls._big(raw, 3, 2)}",
|
|
f"BLOCK:{cls._big(raw, 4, 1)}/{cls._big(raw, 5, 1)}/{cls._big(raw, 6, 1)}",
|
|
f"TRUCK:{cls._big(raw, 7, 1)}",
|
|
f"PATHS:{'/'.join(str(cls._big(raw, start, 8)) for start in (15, 23, 31, 39, 47, 63))}",
|
|
f"ALIGN:{cls._big(raw, 55, 8, signed=True) * 0.0625:.2f}deg",
|
|
))
|
|
if address == 0x4E4:
|
|
names = ("BAT", "IGN", "T1", "T2", "5VA", "5VDX", "3V3", "10V")
|
|
return " ".join(f"{name}:{cls._little(raw, byte * 8, 8):02X}" for byte, name in enumerate(names))
|
|
if address == 0x4E5:
|
|
return " ".join((
|
|
f"1V8:{cls._little(raw, 0, 8):02X}",
|
|
f"N5V:{cls._little(raw, 8, 8):02X}",
|
|
f"WAVE:{cls._little(raw, 16, 8):02X}",
|
|
f"PWR:{cls._little(raw, 24, 3)}",
|
|
f"VUP:{cls._little(raw, 27, 1)}",
|
|
f"FACT:{cls._little(raw, 32, 3)}/{cls._little(raw, 35, 3)}",
|
|
f"FLAGS:{cls._little(raw, 38, 1)}/{cls._little(raw, 39, 1)}",
|
|
f"MIS:{cls._little(raw, 40, 8, signed=True)}/{cls._little(raw, 56, 8, signed=True)}",
|
|
f"UPDATES:{cls._little(raw, 48, 8)}",
|
|
))
|
|
return f"RAW:{raw:016X}"
|
|
|
|
def snapshot(
|
|
self,
|
|
) -> tuple[
|
|
tuple[DisplaySource, ...],
|
|
tuple[DisplayTrack, ...],
|
|
tuple[DisplayTrackSignals, ...],
|
|
tuple[DisplayRawSignal, ...],
|
|
dict[int, tuple[int, int]],
|
|
]:
|
|
sources = []
|
|
tracks = []
|
|
track_signals = []
|
|
raw_signals = []
|
|
track_locations = {}
|
|
for spec in RESEARCH_SOURCE_SPECS:
|
|
for (name, bus), payloads in self.payloads.items():
|
|
if name != spec.name or any(address not in payloads for address in range(spec.start_address, spec.end_address + 1)):
|
|
continue
|
|
|
|
track_count = 0
|
|
if spec.name == "RADAR_602_ALT":
|
|
for address in range(spec.start_address, spec.end_address + 1):
|
|
raw = int.from_bytes(payloads[address], "little")
|
|
d_rel = self._little(raw, 0, 10) * 0.25
|
|
if not 0 < d_rel < 255.75:
|
|
continue
|
|
y_rel = self._little(raw, 10, 11) * 0.03 - 30.705
|
|
v_rel = self._little(raw, 21, 10) * 0.25 - 128
|
|
object_id = self._little(raw, 31, 9)
|
|
track_id = 800_000 + bus * 0x10000 + address
|
|
tracks.append(DisplayTrack(
|
|
trackId=track_id,
|
|
dRel=d_rel,
|
|
yRel=y_rel,
|
|
vRel=v_rel,
|
|
aRel=float("nan"),
|
|
canAddress=address,
|
|
canBus=bus,
|
|
))
|
|
track_signals.append(DisplayTrackSignals(
|
|
trackId=track_id,
|
|
relLatSpeed=float("nan"),
|
|
absSpeed=float("nan"),
|
|
width=float("nan"),
|
|
length=float("nan"),
|
|
orientationAngle=float("nan"),
|
|
age=-1,
|
|
coastAge=-1,
|
|
state=-1,
|
|
stateAlt=-1,
|
|
trackCounter=-1,
|
|
signalSummary=" ".join((
|
|
f"ID:{object_id}",
|
|
f"S1:{self._little(raw, 42, 8) - 128}",
|
|
f"S2:{self._little(raw, 50, 6) - 32}",
|
|
f"CTR:{self._little(raw, 56, 4)}",
|
|
f"CRC:{self._little(raw, 60, 4):X}",
|
|
)),
|
|
))
|
|
track_locations[track_id] = (address, bus)
|
|
track_count += 1
|
|
elif spec.name in ("CORNER_A_OBJECTS", "CORNER_B_OBJECTS"):
|
|
object_start = 0x241 if spec.name == "CORNER_A_OBJECTS" else 0x279
|
|
for address in range(object_start, object_start + 15):
|
|
decoded = self._corner_object(payloads[address])
|
|
if decoded is None:
|
|
continue
|
|
track_id = 900_000 + bus * 0x10000 + address
|
|
d_rel, y_rel, v_rel = decoded
|
|
tracks.append(DisplayTrack(
|
|
trackId=track_id,
|
|
dRel=d_rel,
|
|
yRel=y_rel,
|
|
vRel=v_rel,
|
|
aRel=float("nan"),
|
|
canAddress=address,
|
|
canBus=bus,
|
|
))
|
|
raw = int.from_bytes(payloads[address], "little")
|
|
category = (raw >> 24) & 0x3
|
|
rcs = self._signed((raw >> 56) & 0x7F, 7)
|
|
unknown_87 = (raw >> 87) & 0x1
|
|
unknown_108 = (raw >> 108) & 0x3FF
|
|
unknown_118 = (raw >> 118) & 0x3FF
|
|
unknown_128 = (raw >> 128) & 0xFFFFFFFFFFFFFFFF
|
|
track_signals.append(DisplayTrackSignals(
|
|
trackId=track_id,
|
|
relLatSpeed=self._signed((raw >> 98) & 0x3FF, 10) * 0.05,
|
|
absSpeed=float("nan"),
|
|
width=float("nan"),
|
|
length=float("nan"),
|
|
orientationAngle=float("nan"),
|
|
age=-1,
|
|
coastAge=-1,
|
|
state=-1,
|
|
stateAlt=-1,
|
|
trackCounter=-1,
|
|
signalSummary=" ".join((
|
|
f"CAT:{category}", f"RCS:{rcs}", f"U87:{unknown_87}",
|
|
f"U108:{unknown_108:03X}", f"U118:{unknown_118:03X}", f"U128:{unknown_128:016X}",
|
|
)),
|
|
))
|
|
track_locations[track_id] = (address, bus)
|
|
track_count += 1
|
|
elif spec.name == "CCNC_FUSED_OBJECTS":
|
|
raw = int.from_bytes(payloads[0x162], "little")
|
|
fused_slots = (
|
|
("FRONT", self._little(raw, 64, 5), self._little(raw, 69, 11) * 0.1, self._little(raw, 80, 7) * 0.1),
|
|
("FRONT_ALT", self._little(raw, 88, 5), self._little(raw, 93, 11) * 0.1, self._little(raw, 104, 7) * 0.1),
|
|
("LEFT", self._little(raw, 112, 5), self._little(raw, 117, 11) * 0.1, self._little(raw, 128, 7) * 0.1),
|
|
("RIGHT", self._little(raw, 136, 5), self._little(raw, 141, 11) * 0.1, self._little(raw, 152, 7) * 0.1),
|
|
("LEFT_REAR", self._big(raw, 167, 5), self._big(raw, 175, 8) * 0.1, self._big(raw, 182, 7) * 0.1),
|
|
("RIGHT_REAR", self._big(raw, 196, 5), self._little(raw, 197, 8) * 0.1, self._little(raw, 205, 7) * 0.1),
|
|
)
|
|
for label, status, distance, lateral in fused_slots:
|
|
raw_signals.append(DisplayRawSignal(
|
|
spec.start_address, spec.end_address, 0x162, bus, label,
|
|
f"STATUS:{status} DIST:{distance:.1f}m LAT:{lateral:.1f}m",
|
|
))
|
|
track_count += status != 0
|
|
raw_signals.append(DisplayRawSignal(
|
|
spec.start_address, spec.end_address, 0x162, bus, "FAULTS",
|
|
" ".join((
|
|
f"FSS:{self._little(raw, 213, 3)}", f"FCA:{self._little(raw, 216, 3)}",
|
|
f"LSS:{self._little(raw, 219, 3)}", f"SLA:{self._little(raw, 222, 3)}",
|
|
f"DAW:{self._little(raw, 225, 3)}", f"HBA:{self._little(raw, 228, 3)}",
|
|
f"SCC:{self._little(raw, 231, 3)}", f"LFA:{self._little(raw, 234, 3)}",
|
|
)),
|
|
))
|
|
elif spec.name in ("CORNER_A_DETECTIONS", "CORNER_B_DETECTIONS", "RADAR_RAW_DETECTIONS"):
|
|
for address in range(spec.start_address, spec.end_address + 1):
|
|
for record, raw in enumerate(self._packed_detection_records(payloads[address])):
|
|
if raw == 0:
|
|
continue
|
|
if spec.name in ("CORNER_A_DETECTIONS", "CORNER_B_DETECTIONS"):
|
|
if (raw & 0x1FFF) == 8000:
|
|
continue
|
|
raw_signals.append(DisplayRawSignal(
|
|
spec.start_address, spec.end_address, address, bus, f"REC{record}",
|
|
" ".join((
|
|
f"DIST?:{(raw & 0x1FFF) * 0.05:.2f}",
|
|
f"RSV:{(raw >> 13) & 0x7}",
|
|
f"PROP:{(raw >> 16) & 0x7F}",
|
|
f"AUX:{(raw >> 23) & 0x1FF}",
|
|
f"RAW:{raw:08X}",
|
|
)),
|
|
))
|
|
track_count += 1
|
|
continue
|
|
if raw == 0xC8782EE0:
|
|
continue
|
|
raw_signals.append(DisplayRawSignal(
|
|
spec.start_address, spec.end_address, address, bus, f"REC{record}",
|
|
" ".join((
|
|
f"DIST?:{(raw & 0xFFF) * 0.05:.2f}",
|
|
f"FLAGS:{(raw >> 12) & 0xF}",
|
|
f"U16:{(raw >> 16) & 0xFF:02X}",
|
|
f"U24:{(raw >> 24) & 0xFF:02X}",
|
|
f"RAW:{raw:08X}",
|
|
)),
|
|
))
|
|
track_count += 1
|
|
elif spec.name.startswith("RADAR_500_"):
|
|
for address in range(spec.start_address, spec.end_address + 1):
|
|
raw = int.from_bytes(payloads[address], "little")
|
|
raw_signals.append(DisplayRawSignal(
|
|
spec.start_address, spec.end_address, address, bus, f"{address:X}",
|
|
self._radar_500_status_summary(address, raw),
|
|
))
|
|
elif spec.name == "CAMERA_LANE_PATH":
|
|
for address in range(spec.start_address, spec.end_address + 1):
|
|
payload = payloads[address]
|
|
raw_signals.append(DisplayRawSignal(
|
|
spec.start_address, spec.end_address, address, bus, f"{address:X}",
|
|
f"CTR:{payload[2]} DATA:{payload[3:].hex().upper()}",
|
|
))
|
|
sources.append(DisplaySource(spec.start_address, spec.end_address, bus, track_count))
|
|
return tuple(sources), tuple(tracks), tuple(track_signals), tuple(raw_signals), track_locations
|
|
|
|
|
|
def make_radar_snapshot(radar_data, track_signals: tuple[DisplayTrackSignals, ...] = (),
|
|
raw_signals: tuple[DisplayRawSignal, ...] = (),
|
|
track_locations: dict[int, tuple[int, int]] | None = None) -> RadarSnapshot:
|
|
track_locations = track_locations or {}
|
|
return RadarSnapshot(
|
|
points=tuple(DisplayTrack(
|
|
trackId=int(point.trackId),
|
|
dRel=float(point.dRel),
|
|
yRel=float(point.yRel),
|
|
vRel=float(point.vRel),
|
|
aRel=float(point.aRel),
|
|
canAddress=track_locations.get(int(point.trackId), (-1, -1))[0],
|
|
canBus=track_locations.get(int(point.trackId), (-1, -1))[1],
|
|
) for point in radar_data.points),
|
|
trackSources=tuple(DisplaySource(
|
|
startAddress=int(source.startAddress),
|
|
endAddress=int(source.endAddress),
|
|
bus=int(source.bus),
|
|
trackCount=int(source.trackCount),
|
|
) for source in radar_data.trackSources),
|
|
trackSignals=track_signals,
|
|
rawSignals=raw_signals,
|
|
radarTracksAvailable=bool(radar_data.radarTracksAvailable),
|
|
)
|
|
|
|
|
|
def source_name(start_address: int, end_address: int) -> str:
|
|
spec = SPEC_BY_RANGE.get((start_address, end_address))
|
|
if spec is not None:
|
|
return "CAMERA_OBJECTS_235_248" if spec.source_kind == "camera" else spec.name
|
|
research_spec = RESEARCH_SPEC_BY_RANGE.get((start_address, end_address))
|
|
return research_spec.name if research_spec is not None else f"RADAR_{start_address:X}_{end_address:X}"
|
|
|
|
|
|
def source_details(start_address: int, end_address: int) -> str:
|
|
spec = SPEC_BY_RANGE.get((start_address, end_address))
|
|
if spec is not None:
|
|
prefix = "forward-camera objects " if spec.source_kind == "camera" else ""
|
|
return f"{prefix}{spec.frequency} Hz {spec.message_size} B"
|
|
research_spec = RESEARCH_SPEC_BY_RANGE.get((start_address, end_address))
|
|
return research_spec.details if research_spec is not None else "unknown format"
|
|
|
|
|
|
def radar_source_sort_key(source) -> tuple[bool, int, int, int]:
|
|
spec = SPEC_BY_RANGE.get((source.startAddress, source.endAddress))
|
|
return (
|
|
spec is None or spec.name != PREFERRED_RADAR_SOURCE,
|
|
source.startAddress,
|
|
source.endAddress,
|
|
source.bus,
|
|
)
|
|
|
|
|
|
def radar_source_key(source) -> RadarSourceKey:
|
|
return int(source.startAddress), int(source.endAddress), int(source.bus)
|
|
|
|
|
|
def source_filter_state(source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]],
|
|
source_key: RadarSourceKey) -> tuple[bool, bool, bool]:
|
|
if source_key[:2] == (0x3D0, 0x3D4):
|
|
return source_filters.get(source_key, (False, True, False))
|
|
return source_filters.get(source_key, DEFAULT_SOURCE_FILTERS)
|
|
|
|
|
|
def toggle_source_filter(source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]],
|
|
source_key: RadarSourceKey, filter_index: int) -> None:
|
|
filters = list(source_filter_state(source_filters, source_key))
|
|
filters[filter_index] = not filters[filter_index]
|
|
source_filters[source_key] = (filters[0], filters[1], filters[2])
|
|
|
|
|
|
def dbc_motion_class(motion_state: int | None) -> str:
|
|
if motion_state is None:
|
|
return "unknown"
|
|
return {0: "unknown (raw 0)", 1: "stationary", 2: "moving", 3: "stopped", 4: "oncoming"}.get(
|
|
motion_state, f"unknown (raw {motion_state})",
|
|
)
|
|
|
|
|
|
def dbc_motion_color(motion_state: int | None) -> rl.Color:
|
|
return {0: MUTED, 1: WHITE, 2: PURPLE}.get(motion_state, MUTED)
|
|
|
|
|
|
def dbc_unknown_raw_label(motion_state: int | None) -> str | None:
|
|
if motion_state in (1, 2):
|
|
return None
|
|
return "DBC unknown" if motion_state is None else f"DBC raw={motion_state}"
|
|
|
|
|
|
def display_track_color(track, motion_states: dict[int, int]) -> rl.Color:
|
|
return dbc_motion_color(motion_states.get(int(track.trackId)))
|
|
|
|
|
|
def filter_tracks(tracks, motion_states: dict[int, int], track_locations: dict[int, tuple[int, int]], sources,
|
|
source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]]):
|
|
filtered_tracks = []
|
|
for track in tracks:
|
|
address, bus = track_locations.get(int(track.trackId), (-1, -1))
|
|
source = next((
|
|
source for source in sources
|
|
if source.startAddress <= address <= source.endAddress and source.bus == bus
|
|
), None)
|
|
filters = source_filter_state(source_filters, radar_source_key(source)) if source is not None else DEFAULT_SOURCE_FILTERS
|
|
motion_state = motion_states.get(int(track.trackId))
|
|
if ((filters[0] and motion_state == 2)
|
|
or (filters[1] and motion_state == 1)
|
|
or (filters[2] and motion_state not in (1, 2))):
|
|
continue
|
|
filtered_tracks.append(track)
|
|
return filtered_tracks
|
|
|
|
|
|
def enable_dbc_detail_signals(radar_interface: RadarInterface, enhanced_parsers: set[tuple[str, int]]) -> None:
|
|
"""Add visualization-only signals to the branch parser without maintaining a second CAN decoder."""
|
|
for radar_parser in radar_interface.radar_parsers:
|
|
parser_key = (radar_parser.spec.name, radar_parser.bus)
|
|
detail_signals = RADAR_DETAIL_SIGNALS.get(radar_parser.spec.name)
|
|
if parser_key in enhanced_parsers or detail_signals is None:
|
|
continue
|
|
|
|
# This UI can intentionally run slower than replay and uses conflated CAN, so don't apply parser liveness checks here.
|
|
messages = [(f"RADAR_TRACK_{addr:x}", 0) for addr in radar_parser.spec.address_range]
|
|
parser = CANParser(
|
|
radar_parser.spec.dbc_name,
|
|
messages,
|
|
radar_parser.bus,
|
|
signals={*radar_parser.spec.signals, *detail_signals},
|
|
)
|
|
for message_state in parser.message_states.values():
|
|
message_state.ignore_alive = True
|
|
radar_parser.parser = parser
|
|
enhanced_parsers.add(parser_key)
|
|
|
|
|
|
def dbc_signal_value(message, prefix: str, name: str, default=0):
|
|
return message.get(f"{prefix}{name}", default)
|
|
|
|
|
|
def get_dbc_track_details(
|
|
radar_interface: RadarInterface,
|
|
) -> tuple[dict[int, int], tuple[DisplayTrackSignals, ...], dict[int, tuple[int, int]]]:
|
|
states = {}
|
|
details = []
|
|
locations = {}
|
|
for track_key, point in radar_interface.pts.items():
|
|
if not isinstance(track_key, tuple):
|
|
continue
|
|
|
|
spec = SPEC_BY_NAME.get(track_key[0])
|
|
if spec is None:
|
|
continue
|
|
stored_address = track_key[1]
|
|
can_address = stored_address // 2 if len(spec.track_prefixes) > 1 else stored_address
|
|
active_bus = radar_interface.active_radar_buses.get(track_key[0])
|
|
locations[int(point.trackId)] = (can_address, active_bus if active_bus is not None else -1)
|
|
|
|
if track_key[0] not in RADAR_DETAIL_SIGNALS:
|
|
continue
|
|
|
|
radar_parser = next((parser for parser in radar_interface.radar_parsers
|
|
if parser.spec.name == track_key[0] and parser.bus == active_bus), None)
|
|
if radar_parser is None:
|
|
continue
|
|
|
|
prefix = f"{stored_address % 2 + 1}_" if spec.name == "RADAR_210_21F" else ""
|
|
message = radar_parser.parser.vl[f"RADAR_TRACK_{can_address:x}"]
|
|
motion_signal = f"{prefix}MOTION_STATE"
|
|
if motion_signal in message:
|
|
states[int(point.trackId)] = int(message[motion_signal])
|
|
|
|
if spec.name == "RADAR_210_21F":
|
|
signal_summary = " ".join((
|
|
f"Q:{int(dbc_signal_value(message, prefix, 'TRACK_QUALITY'))}",
|
|
f"RCS:{int(dbc_signal_value(message, prefix, 'RCS'))}",
|
|
f"U4:{int(dbc_signal_value(message, prefix, 'NEW_SIGNAL_4'))}",
|
|
f"U18:{int(dbc_signal_value(message, prefix, 'NEW_SIGNAL_18'))}",
|
|
f"ID:{int(dbc_signal_value(message, prefix, 'OBJECT_ID'))}",
|
|
))
|
|
else:
|
|
geometry = "/".join(
|
|
str(int(dbc_signal_value(message, prefix, f"NEW_SIGNAL_{index}"))) for index in range(12, 18)
|
|
)
|
|
signal_summary = " ".join((
|
|
f"Q:{int(dbc_signal_value(message, prefix, 'TRACK_QUALITY'))}",
|
|
f"RCS:{int(dbc_signal_value(message, prefix, 'RCS'))}",
|
|
f"U4:{int(dbc_signal_value(message, prefix, 'NEW_SIGNAL_4'))}",
|
|
f"U5:{int(dbc_signal_value(message, prefix, 'NEW_SIGNAL_5'))}",
|
|
f"U18:{int(dbc_signal_value(message, prefix, 'NEW_SIGNAL_18'))}",
|
|
f"G12-17:{geometry}",
|
|
))
|
|
details.append(DisplayTrackSignals(
|
|
trackId=int(point.trackId),
|
|
relLatSpeed=float(dbc_signal_value(message, prefix, "REL_LAT_SPEED", float("nan"))),
|
|
absSpeed=float(dbc_signal_value(message, prefix, "ABS_SPEED", float("nan"))),
|
|
width=float(dbc_signal_value(message, prefix, "WIDTH", float("nan"))),
|
|
length=float(dbc_signal_value(message, prefix, "LENGTH", float("nan"))),
|
|
orientationAngle=float(dbc_signal_value(message, prefix, "ORIENTATION_ANGLE", float("nan"))),
|
|
age=int(dbc_signal_value(message, prefix, "AGE", -1)),
|
|
coastAge=int(dbc_signal_value(message, prefix, "COAST_AGE", -1)),
|
|
state=int(dbc_signal_value(message, prefix, "STATE", -1)),
|
|
stateAlt=int(dbc_signal_value(message, prefix, "STATE_ALT", -1)),
|
|
trackCounter=int(dbc_signal_value(message, prefix, "TRACK_COUNTER", -1)),
|
|
signalSummary=signal_summary,
|
|
))
|
|
return states, tuple(details), locations
|
|
|
|
|
|
def match_decoded_track_values(tracks, decoded_tracks, decoded_values: dict):
|
|
"""Match liveTracks points to the independent CAN decoder for side-by-side comparison."""
|
|
matched = {}
|
|
for track in tracks:
|
|
best_track = None
|
|
best_score = math.inf
|
|
for decoded in decoded_tracks:
|
|
score = abs(track.dRel - decoded.dRel) + 2.0 * abs(track.yRel - decoded.yRel) + 0.25 * abs(track.vRel - decoded.vRel)
|
|
if score < best_score:
|
|
best_track, best_score = decoded, score
|
|
if best_track is not None and best_score < 2.0 and int(best_track.trackId) in decoded_values:
|
|
matched[int(track.trackId)] = decoded_values[int(best_track.trackId)]
|
|
return matched
|
|
|
|
|
|
def replace_queued_value(output_queue, value) -> None:
|
|
try:
|
|
output_queue.put_nowait(value)
|
|
except queue.Full:
|
|
try:
|
|
output_queue.get_nowait()
|
|
except queue.Empty:
|
|
return
|
|
try:
|
|
output_queue.put_nowait(value)
|
|
except queue.Full:
|
|
# The queue feeder can still own the slot briefly; the next publish will replace it.
|
|
pass
|
|
|
|
|
|
def radar_decoder_worker(addr: str, output_queue) -> None:
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
logging.getLogger("carlog").setLevel(logging.ERROR)
|
|
messaging.reset_context()
|
|
sm = messaging.SubMaster(["can"], addr=addr)
|
|
radar_cp = structs.CarParams.new_message()
|
|
radar_cp.carFingerprint = "RADAR_UI"
|
|
radar_cp.flags = 0
|
|
radar_cp_sp = structs.CarParamsSP(flags=HyundaiFlagsSP.RADAR_FULL_RADAR.value)
|
|
radar_interface = RadarInterface(radar_cp, radar_cp_sp)
|
|
research_decoder = ResearchCanDecoder()
|
|
enhanced_parsers: set[tuple[str, int]] = set()
|
|
radar_snapshot = RadarSnapshot()
|
|
snapshot = radar_snapshot
|
|
motion_states: dict[int, int] = {}
|
|
last_publish_time = 0.0
|
|
last_can_message_time = 0.0
|
|
|
|
while True:
|
|
sm.update(100)
|
|
radar_data = None
|
|
if sm.updated["can"]:
|
|
last_can_message_time = time.monotonic()
|
|
can_messages = [(message.address, bytes(message.dat), message.src) for message in sm["can"]]
|
|
research_decoder.update(can_messages)
|
|
radar_data = radar_interface.update([(sm.logMonoTime["can"], can_messages)])
|
|
enable_dbc_detail_signals(radar_interface, enhanced_parsers)
|
|
if radar_data is not None:
|
|
motion_states, track_signals, track_locations = get_dbc_track_details(radar_interface)
|
|
radar_snapshot = make_radar_snapshot(radar_data, track_signals, track_locations=track_locations)
|
|
research_sources, research_tracks, research_signals, raw_signals, _ = research_decoder.snapshot()
|
|
snapshot = RadarSnapshot(
|
|
points=(*radar_snapshot.points, *research_tracks),
|
|
trackSources=(*radar_snapshot.trackSources, *research_sources),
|
|
trackSignals=(*radar_snapshot.trackSignals, *research_signals),
|
|
rawSignals=raw_signals,
|
|
radarTracksAvailable=radar_snapshot.radarTracksAvailable,
|
|
)
|
|
|
|
now = time.monotonic()
|
|
if radar_data is not None or now - last_publish_time >= 0.1:
|
|
replace_queued_value(
|
|
output_queue, (snapshot, motion_states, sm.alive["can"], sm.seen["can"], last_can_message_time),
|
|
)
|
|
last_publish_time = now
|
|
|
|
|
|
def draw_text(font, text: str, x: float, y: float, size: float, color: rl.Color = TEXT) -> None:
|
|
rl.draw_text_ex(font, text, rl.Vector2(round(x), round(y)), size, 0, color)
|
|
|
|
|
|
def measure_standalone_text(font, text: str, size: float) -> rl.Vector2:
|
|
return rl.measure_text_ex(font, text, size, 0) # noqa: TID251 - standalone raylib tool
|
|
|
|
|
|
def fit_overlay_text(font, text: str, size: float, max_width: float) -> str:
|
|
text = " ".join(text.split())
|
|
if measure_standalone_text(font, text, size).x <= max_width:
|
|
return text
|
|
suffix = "..."
|
|
while text and measure_standalone_text(font, text + suffix, size).x > max_width:
|
|
text = text[:-1]
|
|
return text.rstrip() + suffix if text else suffix
|
|
|
|
|
|
def current_replay_events(timeline: ReplayTimeline, route_seconds: float) -> list[ReplayTimelineEntry]:
|
|
events = []
|
|
for entry in timeline.entries:
|
|
if entry.kind == "bookmark" and 0.0 <= route_seconds - entry.start <= 2.0:
|
|
events.append(entry)
|
|
elif entry.kind in ("info", "warning", "critical"):
|
|
visible_end = max(entry.end + 0.25, entry.start + 1.0)
|
|
if entry.start - 0.1 <= route_seconds <= visible_end:
|
|
events.append(entry)
|
|
priority = {"critical": 3, "warning": 2, "info": 1, "bookmark": 0}
|
|
return sorted(events, key=lambda entry: (priority[entry.kind], entry.start), reverse=True)[:2]
|
|
|
|
|
|
def draw_replay_event_overlay(font, bounds: rl.Rectangle, top: float,
|
|
timeline: ReplayTimeline, route_seconds: float) -> None:
|
|
events = current_replay_events(timeline, route_seconds)
|
|
if not events:
|
|
return
|
|
|
|
event_colors = {
|
|
"bookmark": CYAN,
|
|
"info": GREEN,
|
|
"warning": ORANGE,
|
|
"critical": RED,
|
|
}
|
|
event_labels = {
|
|
"bookmark": "USER TAG",
|
|
"info": "INFO ALERT",
|
|
"warning": "WARNING",
|
|
"critical": "CRITICAL ALERT",
|
|
}
|
|
width = min(max(340.0, bounds.width * 0.62), bounds.width - 24.0)
|
|
y = max(bounds.y + 12.0, top)
|
|
for entry in events:
|
|
body_lines = [line for line in (entry.title, entry.detail) if line and line != event_labels[entry.kind]]
|
|
height = 42.0 + len(body_lines) * 22.0
|
|
if y + height > bounds.y + bounds.height - 12.0:
|
|
break
|
|
rect = rl.Rectangle(bounds.x + (bounds.width - width) / 2, y, width, height)
|
|
color = event_colors[entry.kind]
|
|
rl.draw_rectangle_rounded(rect, 0.12, 6, rl.Color(4, 8, 14, 225))
|
|
rl.draw_rectangle_rounded_lines_ex(rect, 0.12, 6, 1.0, color)
|
|
rl.draw_rectangle_rec(rl.Rectangle(rect.x, rect.y, 5, rect.height), color)
|
|
draw_text(font, event_labels[entry.kind], rect.x + 16, rect.y + 10, 16, color)
|
|
for index, line in enumerate(body_lines):
|
|
fitted = fit_overlay_text(font, line, 18 if index == 0 else 15, rect.width - 32)
|
|
draw_text(font, fitted, rect.x + 16, rect.y + 34 + index * 22, 18 if index == 0 else 15, TEXT)
|
|
y += height + 8
|
|
|
|
|
|
def edit_key_pressed(key) -> bool:
|
|
return rl.is_key_pressed(key) or rl.is_key_pressed_repeat(key)
|
|
|
|
|
|
def replay_keyboard_command() -> str | None:
|
|
shift_down = (
|
|
rl.is_key_down(rl.KeyboardKey.KEY_LEFT_SHIFT)
|
|
or rl.is_key_down(rl.KeyboardKey.KEY_RIGHT_SHIFT)
|
|
)
|
|
for key, command in (
|
|
(rl.KeyboardKey.KEY_S, "S" if shift_down else "s"),
|
|
(rl.KeyboardKey.KEY_M, "M" if shift_down else "m"),
|
|
(rl.KeyboardKey.KEY_SPACE, " "),
|
|
(rl.KeyboardKey.KEY_E, "e"),
|
|
(rl.KeyboardKey.KEY_D, "d"),
|
|
(rl.KeyboardKey.KEY_T, "t"),
|
|
(rl.KeyboardKey.KEY_I, "i"),
|
|
(rl.KeyboardKey.KEY_W, "w"),
|
|
(rl.KeyboardKey.KEY_C, "c"),
|
|
(rl.KeyboardKey.KEY_EQUAL, "+"),
|
|
(rl.KeyboardKey.KEY_KP_ADD, "+"),
|
|
(rl.KeyboardKey.KEY_MINUS, "-"),
|
|
(rl.KeyboardKey.KEY_KP_SUBTRACT, "-"),
|
|
(rl.KeyboardKey.KEY_Q, "q"),
|
|
):
|
|
if rl.is_key_pressed(key):
|
|
return command
|
|
return None
|
|
|
|
|
|
def camera_content_rect(camera_view: CameraView, rect: rl.Rectangle) -> rl.Rectangle:
|
|
if camera_view.frame is None:
|
|
return rect
|
|
|
|
widget_aspect = rect.width / rect.height
|
|
frame_aspect = camera_view.frame.width / camera_view.frame.height
|
|
width = rect.width * min(frame_aspect / widget_aspect, 1.0)
|
|
height = rect.height * min(widget_aspect / frame_aspect, 1.0)
|
|
return rl.Rectangle(
|
|
rect.x + (rect.width - width) / 2,
|
|
rect.y + (rect.height - height) / 2,
|
|
width,
|
|
height,
|
|
)
|
|
|
|
|
|
def build_camera_calibration(camera, rpy_calib: np.ndarray, wide_from_device_euler: np.ndarray | None = None):
|
|
calibration_scale = camera.width / CAMERA_BUFFER_WIDTH
|
|
calibration = Calibration(1, rpy_calib, camera.intrinsics, calibration_scale)
|
|
if wide_from_device_euler is not None:
|
|
calibration.extrinsics_matrix = (
|
|
view_frame_from_device_frame @ rot_from_euler(wide_from_device_euler) @ rot_from_euler(rpy_calib)
|
|
)
|
|
projection_height = float(np.clip(camera.height / calibration_scale, 1, CAMERA_BUFFER_HEIGHT))
|
|
return calibration, projection_height
|
|
|
|
|
|
def aligned_road_camera_rect(device_camera, wide_content_rect: rl.Rectangle,
|
|
wide_from_device_euler: np.ndarray) -> rl.Rectangle:
|
|
road_camera = device_camera.fcam
|
|
wide_camera = device_camera.ecam
|
|
wide_from_road_view = (
|
|
view_frame_from_device_frame @ rot_from_euler(wide_from_device_euler) @ view_frame_from_device_frame.T
|
|
)
|
|
homography = wide_camera.intrinsics @ wide_from_road_view @ np.linalg.inv(road_camera.intrinsics)
|
|
road_corners = np.asarray([
|
|
[0.0, 0.0, 1.0],
|
|
[road_camera.width, 0.0, 1.0],
|
|
[road_camera.width, road_camera.height, 1.0],
|
|
[0.0, road_camera.height, 1.0],
|
|
]).T
|
|
mapped = homography @ road_corners
|
|
mapped = (mapped[:2] / mapped[2]).T
|
|
x = wide_content_rect.x + mapped[:, 0] / wide_camera.width * wide_content_rect.width
|
|
y = wide_content_rect.y + mapped[:, 1] / wide_camera.height * wide_content_rect.height
|
|
left = float(np.clip(np.min(x), wide_content_rect.x, wide_content_rect.x + wide_content_rect.width))
|
|
right = float(np.clip(np.max(x), wide_content_rect.x, wide_content_rect.x + wide_content_rect.width))
|
|
top = float(np.clip(np.min(y), wide_content_rect.y, wide_content_rect.y + wide_content_rect.height))
|
|
bottom = float(np.clip(np.max(y), wide_content_rect.y, wide_content_rect.y + wide_content_rect.height))
|
|
return rl.Rectangle(left, top, max(1.0, right - left), max(1.0, bottom - top))
|
|
|
|
|
|
def camera_track_geometry(calibration: Calibration | None, track, camera_rect: rl.Rectangle,
|
|
projection_height: float) -> tuple[float, float, float] | None:
|
|
if calibration is None or not (math.isfinite(track.dRel) and math.isfinite(track.yRel) and math.isfinite(track.vRel)):
|
|
return None
|
|
projected = calibration.car_space_to_bb(
|
|
np.asarray([track.dRel]),
|
|
np.asarray([-track.yRel]),
|
|
np.asarray([1.2]),
|
|
)[0]
|
|
if not np.all(np.isfinite(projected)):
|
|
return None
|
|
|
|
x = camera_rect.x + float(projected[0]) * camera_rect.width / CAMERA_BUFFER_WIDTH
|
|
y = camera_rect.y + float(projected[1]) * camera_rect.height / projection_height
|
|
if not rl.check_collision_point_rec(rl.Vector2(x, y), camera_rect):
|
|
return None
|
|
return x, y, float(np.clip(18.0 - track.dRel / 18.0, 7.0, 16.0))
|
|
|
|
|
|
def top_down_track_geometry(rect: rl.Rectangle, track) -> tuple[float, float, float] | None:
|
|
if not (math.isfinite(track.dRel) and math.isfinite(track.yRel) and math.isfinite(track.vRel)):
|
|
return None
|
|
if not (0 <= track.dRel <= MAX_FORWARD_DISTANCE and abs(track.yRel) <= MAX_LATERAL_DISTANCE):
|
|
return None
|
|
plot = rl.Rectangle(rect.x + 48, rect.y + 44, rect.width - 72, rect.height - 72)
|
|
center_x = plot.x + plot.width / 2
|
|
car_y = plot.y + plot.height - 18
|
|
return (
|
|
center_x - track.yRel * plot.width / (MAX_LATERAL_DISTANCE * 2),
|
|
car_y - track.dRel * (plot.height - 18) / MAX_FORWARD_DISTANCE,
|
|
9.0,
|
|
)
|
|
|
|
|
|
def hovered_track_id(tracks, geometry, mouse: rl.Vector2) -> int | None:
|
|
best_id = None
|
|
best_distance = math.inf
|
|
for track in tracks:
|
|
point = geometry(track)
|
|
if point is None:
|
|
continue
|
|
x, y, radius = point
|
|
distance = math.hypot(mouse.x - x, mouse.y - y)
|
|
if distance <= radius + 6.0 and distance < best_distance:
|
|
best_id = int(track.trackId)
|
|
best_distance = distance
|
|
return best_id
|
|
|
|
|
|
def retain_selected_track_id(selected_id: int | None, hovered_id: int | None, tracks) -> int | None:
|
|
if hovered_id is not None:
|
|
return hovered_id
|
|
if selected_id is not None and any(int(track.trackId) == selected_id for track in tracks):
|
|
return selected_id
|
|
return None
|
|
|
|
|
|
def track_source_index(track, track_locations: dict[int, tuple[int, int]], sources) -> int:
|
|
address, bus = track_locations.get(int(track.trackId), (-1, -1))
|
|
return next((
|
|
index for index, source in enumerate(sorted(sources, key=radar_source_sort_key))
|
|
if source.startAddress <= address <= source.endAddress and source.bus == bus
|
|
), -1)
|
|
|
|
|
|
def draw_track_source_marker(center: rl.Vector2, radius: float, color: rl.Color, source_index: int) -> None:
|
|
if source_index <= 0:
|
|
rl.draw_circle_v(center, radius * SOURCE_CIRCLE_RADIUS_SCALE, color)
|
|
return
|
|
sides = (4, 3, 5, 6)[(source_index - 1) % 4]
|
|
rotation = 45.0 if sides == 4 else -90.0
|
|
rl.draw_poly(center, sides, radius, rotation, color)
|
|
|
|
|
|
def draw_track_popup(font, track, x: float, y: float, radius: float, bounds: rl.Rectangle, color: rl.Color,
|
|
motion_state: int | None) -> None:
|
|
label = f"#{track.trackId} {track.dRel:.1f} m {track.vRel:+.1f} m/s"
|
|
if (raw_label := dbc_unknown_raw_label(motion_state)) is not None:
|
|
label += f" {raw_label}"
|
|
label_size = measure_text_cached(font, label, 18)
|
|
label_x = float(np.clip(x - label_size.x / 2, bounds.x + 5, bounds.x + bounds.width - label_size.x - 5))
|
|
label_y = max(bounds.y + 5, y - radius - 29)
|
|
rl.draw_rectangle_rounded(
|
|
rl.Rectangle(label_x - 5, label_y - 2, label_size.x + 10, 24), 0.25, 4, rl.Color(0, 0, 0, 220),
|
|
)
|
|
draw_text(font, label, label_x, label_y, 18, color)
|
|
|
|
|
|
def draw_camera_tracks(font, calibration: Calibration | None, tracks, camera_rect: rl.Rectangle,
|
|
show_labels: bool, motion_states: dict[int, int],
|
|
track_locations: dict[int, tuple[int, int]], sources,
|
|
projection_height: float, hovered_id: int | None, selected_id: int | None,
|
|
preview_id: int | None = None) -> None:
|
|
if calibration is None:
|
|
return
|
|
|
|
for track in tracks:
|
|
geometry = camera_track_geometry(calibration, track, camera_rect, projection_height)
|
|
if geometry is None:
|
|
continue
|
|
x, y, radius = geometry
|
|
dot_radius = max(4.5, radius * 0.68)
|
|
|
|
color = display_track_color(track, motion_states)
|
|
is_hovered = int(track.trackId) == hovered_id
|
|
is_selected = int(track.trackId) == selected_id
|
|
is_previewed = int(track.trackId) == preview_id and not is_selected
|
|
center = rl.Vector2(x, y)
|
|
source_index = track_source_index(track, track_locations, sources)
|
|
draw_track_source_marker(center, dot_radius + 3.0, rl.Color(0, 0, 0, 180), source_index)
|
|
if is_selected:
|
|
rl.draw_circle_lines_v(center, dot_radius + 4.0, CYAN)
|
|
elif is_previewed:
|
|
rl.draw_circle_lines_v(center, dot_radius + 4.0, ORANGE)
|
|
draw_track_source_marker(center, dot_radius, color, source_index)
|
|
rl.draw_circle_v(center, max(1.5, dot_radius * 0.32), BACKGROUND)
|
|
|
|
if show_labels or is_hovered:
|
|
draw_track_popup(font, track, x, y, dot_radius, camera_rect, color, motion_states.get(int(track.trackId)))
|
|
|
|
|
|
def draw_fused_camera_mode(font, road_camera_view: CameraView, wide_camera_view: CameraView, device_camera,
|
|
rpy_calib: np.ndarray, wide_from_device_euler: np.ndarray, full_rect: rl.Rectangle,
|
|
tracks, show_labels: bool, motion_states: dict[int, int],
|
|
track_locations: dict[int, tuple[int, int]], sources,
|
|
selected_id: int | None) -> int | None:
|
|
wide_render_rect = rl.Rectangle(
|
|
full_rect.x - full_rect.width * (FUSED_CAMERA_ZOOM - 1.0) / 2,
|
|
full_rect.y - full_rect.height * (FUSED_CAMERA_ZOOM - 1.0) / 2,
|
|
full_rect.width * FUSED_CAMERA_ZOOM,
|
|
full_rect.height * FUSED_CAMERA_ZOOM,
|
|
)
|
|
wide_camera_view.render(wide_render_rect)
|
|
wide_content_rect = camera_content_rect(wide_camera_view, wide_render_rect)
|
|
if device_camera is None:
|
|
return retain_selected_track_id(selected_id, None, tracks)
|
|
|
|
road_rect = aligned_road_camera_rect(device_camera, wide_content_rect, wide_from_device_euler)
|
|
road_camera_view.render(road_rect)
|
|
road_content_rect = camera_content_rect(road_camera_view, road_rect)
|
|
road_calibration, road_projection_height = build_camera_calibration(device_camera.fcam, rpy_calib)
|
|
wide_calibration, wide_projection_height = build_camera_calibration(
|
|
device_camera.ecam, rpy_calib, wide_from_device_euler,
|
|
)
|
|
|
|
road_track_ids = {
|
|
int(track.trackId) for track in tracks
|
|
if camera_track_geometry(road_calibration, track, road_content_rect, road_projection_height) is not None
|
|
}
|
|
road_tracks = [track for track in tracks if int(track.trackId) in road_track_ids]
|
|
wide_tracks = [track for track in tracks if int(track.trackId) not in road_track_ids]
|
|
|
|
mouse_position = rl.get_mouse_position()
|
|
road_hovered_id = hovered_track_id(
|
|
road_tracks,
|
|
lambda track: camera_track_geometry(road_calibration, track, road_content_rect, road_projection_height),
|
|
mouse_position,
|
|
) if rl.check_collision_point_rec(mouse_position, road_content_rect) else None
|
|
wide_hovered_id = hovered_track_id(
|
|
wide_tracks,
|
|
lambda track: camera_track_geometry(wide_calibration, track, wide_content_rect, wide_projection_height),
|
|
mouse_position,
|
|
) if road_hovered_id is None and rl.check_collision_point_rec(mouse_position, wide_content_rect) else None
|
|
current_hovered_id = road_hovered_id if road_hovered_id is not None else wide_hovered_id
|
|
selected_id = retain_selected_track_id(selected_id, current_hovered_id, tracks)
|
|
|
|
draw_camera_tracks(font, wide_calibration, wide_tracks, wide_content_rect, show_labels, motion_states,
|
|
track_locations, sources, wide_projection_height, wide_hovered_id, selected_id)
|
|
draw_camera_tracks(font, road_calibration, road_tracks, road_content_rect, show_labels, motion_states,
|
|
track_locations, sources, road_projection_height, road_hovered_id, selected_id)
|
|
return selected_id
|
|
|
|
|
|
def draw_model_line(points_x, points_y, center_x: float, car_y: float, longitudinal_scale: float,
|
|
lateral_scale: float, color: rl.Color, width: float) -> None:
|
|
previous = None
|
|
for forward, lateral in zip(points_x, points_y, strict=True):
|
|
if not (0.0 <= forward <= MAX_FORWARD_DISTANCE and abs(lateral) <= MAX_LATERAL_DISTANCE):
|
|
previous = None
|
|
continue
|
|
current = rl.Vector2(center_x + lateral * lateral_scale, car_y - forward * longitudinal_scale)
|
|
if previous is not None:
|
|
edge_alpha = min(80, color.a)
|
|
rl.draw_line_ex(previous, current, width + 2.0, rl.Color(color.r, color.g, color.b, edge_alpha))
|
|
rl.draw_line_ex(previous, current, width, color)
|
|
previous = current
|
|
|
|
|
|
def draw_top_down(font, rect: rl.Rectangle, tracks, show_labels: bool, model,
|
|
motion_states: dict[int, int], hide_moving: bool, hide_stationary: bool,
|
|
hide_unknown: bool, track_locations: dict[int, tuple[int, int]], sources,
|
|
hovered_id: int | None, selected_id: int | None,
|
|
preview_id: int | None = None) -> None:
|
|
rl.draw_rectangle_rec(rect, PANEL)
|
|
plot = rl.Rectangle(rect.x + 48, rect.y + 44, rect.width - 72, rect.height - 72)
|
|
center_x = plot.x + plot.width / 2
|
|
car_y = plot.y + plot.height - 18
|
|
longitudinal_scale = (plot.height - 18) / MAX_FORWARD_DISTANCE
|
|
lateral_scale = plot.width / (MAX_LATERAL_DISTANCE * 2)
|
|
|
|
scale_x = plot.x - 5
|
|
scale_top = car_y - MAX_FORWARD_DISTANCE * longitudinal_scale
|
|
rl.draw_line_ex(rl.Vector2(scale_x, scale_top), rl.Vector2(scale_x, car_y), 1.0, rl.Color(MUTED.r, MUTED.g, MUTED.b, 110))
|
|
for distance in range(0, int(MAX_FORWARD_DISTANCE) + 1, 20):
|
|
y = car_y - distance * longitudinal_scale
|
|
rl.draw_line_ex(rl.Vector2(scale_x - 7, y), rl.Vector2(scale_x, y), 1.0, MUTED)
|
|
label = f"{distance}m"
|
|
label_size = measure_text_cached(font, label, 14)
|
|
draw_text(font, label, scale_x - label_size.x - 10, y - 7, 14, MUTED)
|
|
|
|
legend = (
|
|
(PURPLE, "moving", hide_moving),
|
|
(WHITE, "stationary", hide_stationary),
|
|
(MUTED, "unknown", hide_unknown),
|
|
)
|
|
legend_x = rect.x + rect.width - 150
|
|
legend_y = rect.y + 50
|
|
for index, (color, label, disabled) in enumerate(legend):
|
|
y = legend_y + index * 23
|
|
rl.draw_circle(int(legend_x), int(y + 7), 5, color)
|
|
draw_text(font, label, legend_x + 12, y, 15, TEXT)
|
|
if disabled:
|
|
label_width = measure_standalone_text(font, label, 15).x
|
|
rl.draw_line_ex(
|
|
rl.Vector2(legend_x + 10, y + 8),
|
|
rl.Vector2(legend_x + 14 + label_width, y + 8),
|
|
1.5,
|
|
MUTED,
|
|
)
|
|
|
|
if model is not None:
|
|
for lane_line, probability in zip(model.laneLines, model.laneLineProbs, strict=True):
|
|
alpha = int(np.clip(probability, 0.15, 1.0) * 210)
|
|
draw_model_line(lane_line.x, lane_line.y, center_x, car_y, longitudinal_scale, lateral_scale,
|
|
rl.Color(0, 255, 126, alpha), 2.0)
|
|
for road_edge, std in zip(model.roadEdges, model.roadEdgeStds, strict=True):
|
|
alpha = int(np.clip(1.0 - std, 0.1, 1.0) * 150)
|
|
draw_model_line(road_edge.x, road_edge.y, center_x, car_y, longitudinal_scale, lateral_scale,
|
|
rl.Color(255, 76, 89, alpha), 1.0)
|
|
draw_model_line(model.position.x, model.position.y, center_x, car_y, longitudinal_scale, lateral_scale,
|
|
rl.Color(72, 220, 255, 45), 0.75)
|
|
|
|
ego_car_rect = rl.Rectangle(center_x - 13, car_y - 25, 26, 48)
|
|
rl.draw_rectangle_rounded(ego_car_rect, 0.3, 6, rl.Color(82, 94, 108, 210))
|
|
rl.draw_rectangle_rounded_lines_ex(
|
|
ego_car_rect, 0.3, 6, 1.0, rl.Color(MUTED.r, MUTED.g, MUTED.b, 150),
|
|
)
|
|
|
|
for track in tracks:
|
|
geometry = top_down_track_geometry(rect, track)
|
|
if geometry is None:
|
|
continue
|
|
x, y, radius = geometry
|
|
color = display_track_color(track, motion_states)
|
|
is_hovered = int(track.trackId) == hovered_id
|
|
is_selected = int(track.trackId) == selected_id
|
|
is_previewed = int(track.trackId) == preview_id and not is_selected
|
|
center = rl.Vector2(x, y)
|
|
source_index = track_source_index(track, track_locations, sources)
|
|
draw_track_source_marker(center, radius, rl.Color(0, 0, 0, 180), source_index)
|
|
if is_selected:
|
|
rl.draw_circle_lines_v(rl.Vector2(x, y), radius + 3.0, CYAN)
|
|
elif is_previewed:
|
|
rl.draw_circle_lines_v(rl.Vector2(x, y), radius + 3.0, ORANGE)
|
|
draw_track_source_marker(center, 6.0, color, source_index)
|
|
if show_labels:
|
|
raw_label = dbc_unknown_raw_label(motion_states.get(int(track.trackId)))
|
|
label = f"{track.trackId} {raw_label}" if raw_label is not None else f"{track.trackId}"
|
|
draw_text(font, label, x + 9, y - 9, 16, color)
|
|
elif is_hovered:
|
|
draw_track_popup(font, track, x, y, radius, rect, color, motion_states.get(int(track.trackId)))
|
|
|
|
|
|
def table_capacity(rect: rl.Rectangle) -> int:
|
|
return max(1, int((rect.height - 62) // 24))
|
|
|
|
|
|
def table_hovered_track_id(tracks, rect: rl.Rectangle, scroll: int, mouse: rl.Vector2) -> int | None:
|
|
if not rl.check_collision_point_rec(mouse, rect):
|
|
return None
|
|
row_top = rect.y + 56
|
|
if mouse.y < row_top:
|
|
return None
|
|
row = int((mouse.y - row_top) // 24)
|
|
sorted_tracks = sorted(tracks, key=lambda track: (track.dRel, track.trackId))
|
|
visible = sorted_tracks[scroll:scroll + table_capacity(rect)]
|
|
return int(visible[row].trackId) if 0 <= row < len(visible) else None
|
|
|
|
|
|
def dbc_track_state(state: int) -> str:
|
|
return {0: "empty", 1: "tent 1", 2: "tent 2", 3: "measured", 4: "coasted", 7: "unresolved"}.get(state, str(state))
|
|
|
|
|
|
def draw_track_table(font, rect: rl.Rectangle, tracks, motion_states: dict[int, int], scroll: int,
|
|
selected_id: int | None, hovered_id: int | None,
|
|
track_signals: dict[int, DisplayTrackSignals], track_locations: dict[int, tuple[int, int]],
|
|
table_mode: str, raw_signals: tuple[DisplayRawSignal, ...] = ()) -> None:
|
|
rl.draw_rectangle_rec(rect, BACKGROUND)
|
|
rl.draw_line_ex(rl.Vector2(rect.x, rect.y), rl.Vector2(rect.x + rect.width, rect.y), 2.0, GRID)
|
|
sorted_tracks = sorted(tracks, key=lambda track: (track.dRel, track.trackId))
|
|
capacity = table_capacity(rect)
|
|
|
|
if table_mode == "signals":
|
|
rows = [
|
|
(
|
|
"{:X}/B{}".format(*track_locations.get(int(track.trackId), (-1, -1))),
|
|
f"TRACK {track.trackId}",
|
|
track_signals[int(track.trackId)].signalSummary,
|
|
int(track.trackId),
|
|
)
|
|
for track in sorted_tracks
|
|
if int(track.trackId) in track_signals
|
|
]
|
|
rows.extend(
|
|
(
|
|
f"{signal.address:X}/B{signal.bus}",
|
|
f"{source_name(signal.startAddress, signal.endAddress).removeprefix('RADAR_')} {signal.label}",
|
|
signal.signalSummary,
|
|
None,
|
|
)
|
|
for signal in sorted(raw_signals, key=lambda item: (
|
|
item.startAddress, item.bus, item.address, item.label,
|
|
))
|
|
)
|
|
visible_rows = rows[scroll:scroll + capacity]
|
|
draw_text(font, "SIGNALS", rect.x + 12, rect.y + 8, 21, TEXT)
|
|
if rows:
|
|
draw_text(font, f"{scroll + 1}-{scroll + len(visible_rows)} / {len(rows)}",
|
|
rect.x + rect.width - 112, rect.y + 11, 16, MUTED)
|
|
columns = (("CAN", 0.02), ("SOURCE / ITEM", 0.12), ("DECODED / RAW SIGNALS", 0.36))
|
|
header_y = rect.y + 36
|
|
for title, offset in columns:
|
|
draw_text(font, title, rect.x + rect.width * offset, header_y, 15, MUTED)
|
|
for row, (can_location, label, summary, track_id) in enumerate(visible_rows):
|
|
y = header_y + 23 + row * 24
|
|
if track_id == selected_id:
|
|
rl.draw_rectangle_rec(rl.Rectangle(rect.x, y - 3, rect.width, 24), rl.Color(CYAN.r, CYAN.g, CYAN.b, 42))
|
|
elif row % 2:
|
|
rl.draw_rectangle_rec(rl.Rectangle(rect.x, y - 3, rect.width, 24), rl.Color(255, 255, 255, 8))
|
|
draw_text(font, can_location, rect.x + rect.width * 0.02, y, 15, TEXT)
|
|
draw_text(font, label, rect.x + rect.width * 0.12, y, 15, CYAN if track_id is None else TEXT)
|
|
draw_text(font, summary, rect.x + rect.width * 0.36, y, 15, TEXT)
|
|
return
|
|
|
|
visible = sorted_tracks[scroll:scroll + capacity]
|
|
|
|
draw_text(font, f"TRACK {table_mode.upper()}", rect.x + 12, rect.y + 8, 21, TEXT)
|
|
if sorted_tracks:
|
|
draw_text(font, f"{scroll + 1}-{scroll + len(visible)} / {len(sorted_tracks)}", rect.x + rect.width - 112, rect.y + 11, 16, MUTED)
|
|
|
|
if table_mode == "kinematics":
|
|
columns = (
|
|
("ID", 0.02), ("CAN", 0.08), ("DIST", 0.19), ("LAT", 0.29), ("REL V", 0.39), ("LAT V", 0.50),
|
|
("REL A", 0.61), ("ABS V", 0.73), ("AGE", 0.88),
|
|
)
|
|
elif table_mode == "object":
|
|
columns = (
|
|
("ID", 0.02), ("CAN", 0.09), ("WIDTH", 0.21), ("LENGTH", 0.33), ("ANGLE", 0.45), ("STATE", 0.57),
|
|
("COAST", 0.74), ("UPD", 0.87),
|
|
)
|
|
else:
|
|
columns = (
|
|
("ID", 0.02), ("CAN", 0.10), ("DIST", 0.24), ("LAT", 0.38), ("REL V", 0.52),
|
|
("DBC MOTION", 0.70),
|
|
)
|
|
header_y = rect.y + 36
|
|
for title, offset in columns:
|
|
draw_text(font, title, rect.x + rect.width * offset, header_y, 15, MUTED)
|
|
|
|
for row, track in enumerate(visible):
|
|
y = header_y + 23 + row * 24
|
|
if int(track.trackId) == selected_id:
|
|
rl.draw_rectangle_rec(rl.Rectangle(rect.x, y - 3, rect.width, 24), rl.Color(CYAN.r, CYAN.g, CYAN.b, 42))
|
|
rl.draw_rectangle_lines_ex(rl.Rectangle(rect.x, y - 3, rect.width, 24), 1.0, CYAN)
|
|
elif int(track.trackId) == hovered_id:
|
|
rl.draw_rectangle_rec(rl.Rectangle(rect.x, y - 3, rect.width, 24), rl.Color(ORANGE.r, ORANGE.g, ORANGE.b, 32))
|
|
rl.draw_rectangle_lines_ex(rl.Rectangle(rect.x, y - 3, rect.width, 24), 1.0, ORANGE)
|
|
elif row % 2:
|
|
rl.draw_rectangle_rec(rl.Rectangle(rect.x, y - 3, rect.width, 24), rl.Color(255, 255, 255, 8))
|
|
motion_state = motion_states.get(int(track.trackId))
|
|
detail = track_signals.get(int(track.trackId))
|
|
can_address, can_bus = track_locations.get(int(track.trackId), (-1, -1))
|
|
can_location = f"{can_address:X}/B{can_bus}" if can_address >= 0 and can_bus >= 0 else "n/a"
|
|
if table_mode == "kinematics":
|
|
values = (
|
|
(str(track.trackId), 0.02, TEXT),
|
|
(can_location, 0.08, TEXT if can_address >= 0 else MUTED),
|
|
(f"{track.dRel:.1f}", 0.19, TEXT),
|
|
(f"{track.yRel:+.1f}", 0.29, TEXT),
|
|
(f"{track.vRel:+.1f}", 0.39, TEXT),
|
|
(f"{detail.relLatSpeed:+.1f}" if detail and math.isfinite(detail.relLatSpeed) else "n/a",
|
|
0.50, TEXT if detail and math.isfinite(detail.relLatSpeed) else MUTED),
|
|
(f"{track.aRel:+.1f}" if math.isfinite(track.aRel) else "n/a", 0.61, TEXT),
|
|
(f"{detail.absSpeed:.1f}" if detail and math.isfinite(detail.absSpeed) else "n/a",
|
|
0.73, TEXT if detail and math.isfinite(detail.absSpeed) else MUTED),
|
|
(str(detail.age) if detail and detail.age >= 0 else "n/a", 0.88, TEXT if detail and detail.age >= 0 else MUTED),
|
|
)
|
|
elif table_mode == "object":
|
|
values = (
|
|
(str(track.trackId), 0.02, TEXT),
|
|
(can_location, 0.09, TEXT if can_address >= 0 else MUTED),
|
|
(f"{detail.width:.1f}" if detail and math.isfinite(detail.width) else "n/a",
|
|
0.21, TEXT if detail and math.isfinite(detail.width) else MUTED),
|
|
(f"{detail.length:.1f}" if detail and math.isfinite(detail.length) else "n/a",
|
|
0.33, TEXT if detail and math.isfinite(detail.length) else MUTED),
|
|
(f"{detail.orientationAngle:+.0f}" if detail and math.isfinite(detail.orientationAngle) else "n/a",
|
|
0.45, TEXT if detail and math.isfinite(detail.orientationAngle) else MUTED),
|
|
(dbc_track_state(detail.state) if detail and detail.state >= 0 else "n/a",
|
|
0.57, TEXT if detail and detail.state >= 0 else MUTED),
|
|
(str(detail.coastAge) if detail and detail.coastAge >= 0 else "n/a",
|
|
0.74, TEXT if detail and detail.coastAge >= 0 else MUTED),
|
|
(str(detail.trackCounter) if detail and detail.trackCounter >= 0 else "n/a",
|
|
0.87, TEXT if detail and detail.trackCounter >= 0 else MUTED),
|
|
)
|
|
else:
|
|
values = (
|
|
(str(track.trackId), 0.02, TEXT),
|
|
(can_location, 0.10, TEXT if can_address >= 0 else MUTED),
|
|
(f"{track.dRel:6.1f}", 0.24, TEXT),
|
|
(f"{track.yRel:+6.1f}", 0.38, TEXT),
|
|
(f"{track.vRel:+6.1f}", 0.52, TEXT),
|
|
(dbc_motion_class(motion_state), 0.70, dbc_motion_color(motion_state)),
|
|
)
|
|
for value, offset, color in values:
|
|
draw_text(font, value, rect.x + rect.width * offset, y, 15, color)
|
|
|
|
|
|
def draw_status_chip(font, label: str, x: float, y: float, color: rl.Color,
|
|
mouse_position: rl.Vector2, selected: bool | None = None,
|
|
selection_color: "rl.Color | None" = None) -> tuple[float, rl.Rectangle, bool]:
|
|
size = measure_standalone_text(font, label, 15)
|
|
width = size.x + 16
|
|
chip_rect = rl.Rectangle(x, y, width, 25)
|
|
hovered = rl.check_collision_point_rec(mouse_position, chip_rect)
|
|
alpha = 72 if hovered else (52 if selected else 18) if selected is not None else 32
|
|
rl.draw_rectangle_rounded(chip_rect, 0.3, 4, rl.Color(color.r, color.g, color.b, alpha))
|
|
if hovered:
|
|
rl.draw_rectangle_rounded_lines_ex(chip_rect, 0.3, 4, 1.0, color)
|
|
elif selected:
|
|
rl.draw_rectangle_rounded_lines_ex(chip_rect, 0.3, 4, 1.0, selection_color or color)
|
|
draw_text(font, label, x + 8, y + 4, 15, color)
|
|
return x + width + 7, chip_rect, hovered
|
|
|
|
|
|
def draw_status_tooltip(font, text: str, chip_rect: rl.Rectangle, bounds: rl.Rectangle) -> None:
|
|
size = measure_text_cached(font, text, 15)
|
|
width = size.x + 18
|
|
x = float(np.clip(chip_rect.x, bounds.x + 5, bounds.x + bounds.width - width - 5))
|
|
y = chip_rect.y + chip_rect.height + 6
|
|
tooltip_rect = rl.Rectangle(x, y, width, 27)
|
|
rl.draw_rectangle_rounded(tooltip_rect, 0.25, 4, rl.Color(0, 0, 0, 235))
|
|
rl.draw_rectangle_rounded_lines_ex(tooltip_rect, 0.25, 4, 1.0, MUTED)
|
|
draw_text(font, text, x + 9, y + 5, 15, TEXT)
|
|
|
|
|
|
def draw_status_parts(font, parts: tuple[tuple[str, str], ...], x: float, y: float, size: float, color: rl.Color,
|
|
mouse_position: rl.Vector2, separator: str) -> tuple[str, rl.Rectangle] | None:
|
|
hovered_tooltip = None
|
|
for index, (text, tooltip) in enumerate(parts):
|
|
if index:
|
|
draw_text(font, separator, x, y, size, color)
|
|
x += measure_standalone_text(font, separator, size).x
|
|
text_size = measure_standalone_text(font, text, size)
|
|
text_rect = rl.Rectangle(x, y, text_size.x, max(size + 4, text_size.y))
|
|
draw_text(font, text, x, y, size, color)
|
|
if rl.check_collision_point_rec(mouse_position, text_rect):
|
|
hovered_tooltip = (tooltip, text_rect)
|
|
x += text_size.x
|
|
return hovered_tooltip
|
|
|
|
|
|
def draw_route_dialog(font, bounds: rl.Rectangle, route: str, cursor_index: int, error: str,
|
|
select_all: bool) -> str | None:
|
|
rl.draw_rectangle_rec(bounds, rl.Color(0, 0, 0, 190))
|
|
width = min(760.0, bounds.width - 40.0)
|
|
dialog = rl.Rectangle(bounds.x + (bounds.width - width) / 2, bounds.y + (bounds.height - 190.0) / 2, width, 190.0)
|
|
rl.draw_rectangle_rounded(dialog, 0.08, 8, PANEL)
|
|
rl.draw_rectangle_rounded_lines_ex(dialog, 0.08, 8, 1.0, MUTED)
|
|
draw_text(font, "Open route or live car", dialog.x + 20, dialog.y + 17, 22, TEXT)
|
|
draw_text(font, "Paste a route, segment ID, or IP address, then press Enter.",
|
|
dialog.x + 20, dialog.y + 48, 15, MUTED)
|
|
|
|
input_rect = rl.Rectangle(dialog.x + 20, dialog.y + 76, dialog.width - 40, 38)
|
|
rl.draw_rectangle_rounded(input_rect, 0.15, 4, BACKGROUND)
|
|
rl.draw_rectangle_rounded_lines_ex(input_rect, 0.15, 4, 1.0, CYAN)
|
|
cursor_index = int(np.clip(cursor_index, 0, len(route)))
|
|
view_start = 0
|
|
view_end = len(route)
|
|
while view_start < cursor_index and measure_standalone_text(font, route[view_start:cursor_index], 18).x > input_rect.width - 24:
|
|
view_start += 1
|
|
while view_end > cursor_index and measure_standalone_text(font, route[view_start:view_end], 18).x > input_rect.width - 24:
|
|
view_end -= 1
|
|
display_route = route[view_start:view_end]
|
|
input_text = display_route
|
|
if route and select_all:
|
|
selection_width = min(measure_standalone_text(font, display_route, 18).x + 4, input_rect.width - 20)
|
|
rl.draw_rectangle_rec(
|
|
rl.Rectangle(input_rect.x + 8, input_rect.y + 7, selection_width, 24),
|
|
rl.Color(CYAN.r, CYAN.g, CYAN.b, 70),
|
|
)
|
|
draw_text(font, input_text, input_rect.x + 10, input_rect.y + 9, 18, TEXT)
|
|
if not select_all and int(time.monotonic() * 2) % 2 == 0:
|
|
text_before_cursor = route[view_start:cursor_index]
|
|
cursor_x = input_rect.x + 10 + measure_standalone_text(font, text_before_cursor, 18).x
|
|
rl.draw_line_ex(rl.Vector2(cursor_x + 1, input_rect.y + 8), rl.Vector2(cursor_x + 1, input_rect.y + 30), 1.0, TEXT)
|
|
|
|
if error:
|
|
draw_text(font, error, dialog.x + 20, dialog.y + 124, 14, RED)
|
|
|
|
mouse_position = rl.get_mouse_position()
|
|
cancel_size = measure_standalone_text(font, "Cancel", 16)
|
|
open_size = measure_standalone_text(font, "Open", 16)
|
|
open_rect = rl.Rectangle(dialog.x + dialog.width - open_size.x - 44, dialog.y + 137, open_size.x + 24, 34)
|
|
cancel_rect = rl.Rectangle(open_rect.x - cancel_size.x - 34, dialog.y + 137, cancel_size.x + 24, 34)
|
|
cancel_hovered = rl.check_collision_point_rec(mouse_position, cancel_rect)
|
|
open_hovered = rl.check_collision_point_rec(mouse_position, open_rect)
|
|
rl.draw_rectangle_rounded(cancel_rect, 0.2, 4, rl.Color(MUTED.r, MUTED.g, MUTED.b, 55 if cancel_hovered else 28))
|
|
rl.draw_rectangle_rounded(open_rect, 0.2, 4, rl.Color(CYAN.r, CYAN.g, CYAN.b, 75 if open_hovered else 42))
|
|
draw_text(font, "Cancel", cancel_rect.x + (cancel_rect.width - cancel_size.x) / 2, cancel_rect.y + 8, 16, TEXT)
|
|
draw_text(font, "Open", open_rect.x + (open_rect.width - open_size.x) / 2, open_rect.y + 8, 16, CYAN)
|
|
rl.set_mouse_cursor(
|
|
rl.MouseCursor.MOUSE_CURSOR_POINTING_HAND if cancel_hovered or open_hovered else rl.MouseCursor.MOUSE_CURSOR_IBEAM,
|
|
)
|
|
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): # noqa: TID251 - standalone raylib tool
|
|
if cancel_hovered:
|
|
return "cancel"
|
|
if open_hovered:
|
|
return "open"
|
|
return None
|
|
|
|
|
|
def draw_route_loading(font, bounds: rl.Rectangle, route: str, elapsed: float) -> None:
|
|
rl.draw_rectangle_rec(bounds, rl.Color(0, 0, 0, 175))
|
|
width = min(580.0, bounds.width - 40.0)
|
|
card = rl.Rectangle(bounds.x + (bounds.width - width) / 2, bounds.y + (bounds.height - 154.0) / 2, width, 154.0)
|
|
rl.draw_rectangle_rounded(card, 0.1, 8, PANEL)
|
|
rl.draw_rectangle_rounded_lines_ex(card, 0.1, 8, 1.0, MUTED)
|
|
draw_text(font, "Loading route", card.x + 22, card.y + 19, 22, TEXT)
|
|
draw_text(font, "Waiting for replay data...", card.x + 22, card.y + 51, 15, MUTED)
|
|
|
|
route_text = route
|
|
while route_text and measure_standalone_text(font, route_text, 15).x > card.width - 44:
|
|
route_text = route_text[1:]
|
|
draw_text(font, route_text, card.x + 22, card.y + 77, 15, CYAN)
|
|
|
|
bar = rl.Rectangle(card.x + 22, card.y + 112, card.width - 44, 12)
|
|
rl.draw_rectangle_rounded(bar, 0.5, 6, rl.Color(MUTED.r, MUTED.g, MUTED.b, 35))
|
|
segment_width = min(140.0, bar.width * 0.3)
|
|
segment_left = (elapsed * 190.0) % (bar.width + segment_width) - segment_width
|
|
visible_left = max(0.0, segment_left)
|
|
visible_right = min(bar.width, segment_left + segment_width)
|
|
if visible_right > visible_left:
|
|
progress = rl.Rectangle(bar.x + visible_left, bar.y, visible_right - visible_left, bar.height)
|
|
rl.draw_rectangle_rounded(progress, 0.5, 6, CYAN)
|
|
|
|
|
|
def draw_replay_timeline(font, bounds: rl.Rectangle, timeline: ReplayTimeline,
|
|
route_seconds: float, scrub: ReplayScrubState,
|
|
download_current: int, download_total: int) -> tuple[str | None, bool]:
|
|
timeline_rect = rl.Rectangle(bounds.x + 12, bounds.y + 68, bounds.width - 24, 16)
|
|
rl.draw_rectangle_rounded(timeline_rect, 0.25, 4, rl.Color(25, 65, 135, 150))
|
|
duration = timeline.end - timeline.start
|
|
if duration <= 0.0:
|
|
scrub.active = False
|
|
scrub.preview_seconds = None
|
|
return None, False
|
|
|
|
mouse_position = rl.get_mouse_position()
|
|
hovered = rl.check_collision_point_rec(mouse_position, timeline_rect)
|
|
download_ratio = float(np.clip(download_current / download_total, 0.0, 1.0)) if download_total > 0 else 1.0
|
|
downloaded_end = timeline.start + duration * download_ratio
|
|
pressed = hovered and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) # noqa: TID251
|
|
if pressed:
|
|
scrub.active = True
|
|
|
|
action = None
|
|
if scrub.active:
|
|
scrub_ratio = float(np.clip((mouse_position.x - timeline_rect.x) / timeline_rect.width, 0.0, 1.0))
|
|
scrub.preview_seconds = timeline.start + scrub_ratio * duration
|
|
seek_second = round(scrub.preview_seconds)
|
|
now = time.monotonic()
|
|
released = rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) # noqa: TID251
|
|
if (pressed or released
|
|
or (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
and seek_second != scrub.last_seek_second and now - scrub.last_seek_at >= 0.15)):
|
|
action = f"seek-time:{seek_second}"
|
|
scrub.last_seek_at = now
|
|
scrub.last_seek_second = seek_second
|
|
scrub.pending_seconds = scrub.preview_seconds if scrub.preview_seconds > downloaded_end else None
|
|
if released:
|
|
scrub.active = False
|
|
|
|
for entry in timeline.entries:
|
|
start_ratio = float(np.clip((entry.start - timeline.start) / duration, 0.0, 1.0))
|
|
end_ratio = float(np.clip((entry.end - timeline.start) / duration, start_ratio, 1.0))
|
|
start_x = timeline_rect.x + start_ratio * timeline_rect.width
|
|
end_x = timeline_rect.x + end_ratio * timeline_rect.width
|
|
width = max(2.0, end_x - start_x)
|
|
if entry.kind == "engaged":
|
|
rl.draw_rectangle_rec(rl.Rectangle(start_x, timeline_rect.y, width, 10), GREEN)
|
|
elif entry.kind == "bookmark":
|
|
rl.draw_rectangle_rec(rl.Rectangle(start_x, timeline_rect.y + 10, 2, 6), CYAN)
|
|
else:
|
|
alert_color = {"info": GREEN, "warning": ORANGE, "critical": RED}[entry.kind]
|
|
rl.draw_rectangle_rec(rl.Rectangle(start_x, timeline_rect.y + 11, width, 5), alert_color)
|
|
|
|
if download_ratio < 1.0:
|
|
downloaded_x = timeline_rect.x + download_ratio * timeline_rect.width
|
|
rl.draw_rectangle_rec(
|
|
rl.Rectangle(downloaded_x, timeline_rect.y, timeline_rect.x + timeline_rect.width - downloaded_x,
|
|
timeline_rect.height),
|
|
rl.Color(5, 9, 15, 105),
|
|
)
|
|
rl.draw_line_ex(
|
|
rl.Vector2(downloaded_x, timeline_rect.y),
|
|
rl.Vector2(downloaded_x, timeline_rect.y + timeline_rect.height),
|
|
1.0,
|
|
MUTED,
|
|
)
|
|
|
|
if scrub.pending_seconds is not None:
|
|
if abs(route_seconds - scrub.pending_seconds) <= 1.0:
|
|
scrub.pending_seconds = None
|
|
else:
|
|
pending_ratio = float(np.clip((scrub.pending_seconds - timeline.start) / duration, 0.0, 1.0))
|
|
pending_x = timeline_rect.x + pending_ratio * timeline_rect.width
|
|
for dash_y in np.arange(timeline_rect.y - 3, timeline_rect.y + timeline_rect.height + 3, 5):
|
|
rl.draw_line_ex(
|
|
rl.Vector2(pending_x, float(dash_y)),
|
|
rl.Vector2(pending_x, float(min(dash_y + 3, timeline_rect.y + timeline_rect.height + 3))),
|
|
2.0,
|
|
ORANGE,
|
|
)
|
|
rl.draw_triangle(
|
|
rl.Vector2(pending_x, timeline_rect.y - 4),
|
|
rl.Vector2(pending_x - 5, timeline_rect.y - 10),
|
|
rl.Vector2(pending_x + 5, timeline_rect.y - 10),
|
|
ORANGE,
|
|
)
|
|
|
|
display_seconds = scrub.preview_seconds if scrub.active and scrub.preview_seconds is not None else route_seconds
|
|
current_ratio = float(np.clip((display_seconds - timeline.start) / duration, 0.0, 1.0))
|
|
current_x = timeline_rect.x + current_ratio * timeline_rect.width
|
|
rl.draw_line_ex(
|
|
rl.Vector2(current_x, timeline_rect.y - 2),
|
|
rl.Vector2(current_x, timeline_rect.y + timeline_rect.height + 2),
|
|
2.0,
|
|
WHITE,
|
|
)
|
|
if scrub.active and scrub.preview_seconds is not None:
|
|
preview = f"{scrub.preview_seconds:.1f}s"
|
|
preview_size = measure_text_cached(font, preview, 14)
|
|
preview_x = float(np.clip(current_x - preview_size.x / 2, timeline_rect.x, timeline_rect.x + timeline_rect.width - preview_size.x - 10))
|
|
preview_rect = rl.Rectangle(preview_x, timeline_rect.y - 27, preview_size.x + 10, 22)
|
|
rl.draw_rectangle_rounded(preview_rect, 0.25, 4, rl.Color(0, 0, 0, 220))
|
|
draw_text(font, preview, preview_rect.x + 5, preview_rect.y + 3, 14, TEXT)
|
|
if not scrub.active:
|
|
scrub.preview_seconds = None
|
|
return action, hovered or scrub.active
|
|
|
|
|
|
def draw_replay_control_bar(font, bounds: rl.Rectangle, replay: ManagedReplay, route_seconds: float,
|
|
route_fingerprint: str, timeline: ReplayTimeline, v_ego: float, loading: bool, seek_input: str,
|
|
seek_active: bool, scrub: ReplayScrubState) -> str | None:
|
|
rl.draw_rectangle_rec(bounds, rl.Color(8, 12, 18, 248))
|
|
rl.draw_line_ex(rl.Vector2(bounds.x, bounds.y), rl.Vector2(bounds.x + bounds.width, bounds.y), 1.0, MUTED)
|
|
controls = (
|
|
("-60s", "M", CYAN, "Shift+M: back 60 seconds"),
|
|
("-10s", "S", CYAN, "Shift+S: back 10 seconds"),
|
|
("PAUSE", " ", GREEN, "Space: pause / resume"),
|
|
("+10s", "s", CYAN, "S: forward 10 seconds"),
|
|
("+60s", "m", CYAN, "M: forward 60 seconds"),
|
|
("SPD-", "-", MUTED, "-: slower playback"),
|
|
("SPD+", "+", MUTED, "+: faster playback"),
|
|
("ENG", "e", GREEN, "E: next engagement"),
|
|
("DIS", "d", GREEN, "D: next disengagement"),
|
|
("TAG", "t", ORANGE, "T: next user tag"),
|
|
("INFO", "i", ORANGE, "I: next info alert"),
|
|
("WARN", "w", ORANGE, "W: next warning alert"),
|
|
("CRIT", "c", RED, "C: next critical alert"),
|
|
("EXIT", "q", RED, "Q: exit replay"),
|
|
)
|
|
mouse_position = rl.get_mouse_position()
|
|
chip_x = bounds.x + 10
|
|
chip_y = bounds.y + 9
|
|
clicked_action = None
|
|
hovered_tooltip = None
|
|
for label, action, color, tooltip in controls:
|
|
chip_x, chip_rect, hovered = draw_status_chip(font, label, chip_x, chip_y, color, mouse_position)
|
|
if hovered:
|
|
hovered_tooltip = (tooltip, chip_rect)
|
|
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): # noqa: TID251
|
|
clicked_action = action
|
|
|
|
field_x = chip_x + 2
|
|
field = rl.Rectangle(field_x, bounds.y + 8, measure_standalone_text(font, "SEEK", 15).x + 14, 27)
|
|
field_hovered = rl.check_collision_point_rec(mouse_position, field)
|
|
rl.draw_rectangle_rounded(field, 0.18, 4, BACKGROUND)
|
|
rl.draw_rectangle_rounded_lines_ex(field, 0.18, 4, 1.0, CYAN if seek_active else MUTED)
|
|
draw_text(font, seek_input or "SEEK", field.x + 7, field.y + 5, 15, TEXT if seek_input else MUTED)
|
|
if seek_active and int(time.monotonic() * 2) % 2 == 0:
|
|
cursor_x = field.x + 7 + measure_standalone_text(font, seek_input, 15).x
|
|
rl.draw_line_ex(rl.Vector2(cursor_x + 1, field.y + 4), rl.Vector2(cursor_x + 1, field.y + 22), 1.0, TEXT)
|
|
if field_hovered and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): # noqa: TID251
|
|
clicked_action = "seek-focus"
|
|
elif seek_active and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): # noqa: TID251
|
|
clicked_action = clicked_action or "seek-blur"
|
|
|
|
if hovered_tooltip is not None:
|
|
tooltip, chip_rect = hovered_tooltip
|
|
size = measure_text_cached(font, tooltip, 15)
|
|
tooltip_x = float(np.clip(chip_rect.x, bounds.x + 5, bounds.x + bounds.width - size.x - 23))
|
|
tooltip_rect = rl.Rectangle(tooltip_x, bounds.y - 33, size.x + 18, 27)
|
|
rl.draw_rectangle_rounded(tooltip_rect, 0.25, 4, rl.Color(0, 0, 0, 235))
|
|
rl.draw_rectangle_rounded_lines_ex(tooltip_rect, 0.25, 4, 1.0, MUTED)
|
|
draw_text(font, tooltip, tooltip_rect.x + 9, tooltip_rect.y + 5, 15, TEXT)
|
|
|
|
status_text = "loading" if loading else ("paused" if replay.paused else "playing")
|
|
info = "".join((
|
|
f"STATUS {status_text} ROUTE {route_seconds:.1f}s FINGERPRINT {route_fingerprint or '--'} ",
|
|
f"SPEED {v_ego:.1f} m/s PLAYBACK {replay.playback_speed:.1f}x ",
|
|
f"DOWNLOAD {format_download_progress(replay.progress_current, replay.progress_total)}",
|
|
))
|
|
draw_text(font, info, bounds.x + 12, bounds.y + 43, 14, TEXT)
|
|
timeline_action, timeline_hovered = draw_replay_timeline(
|
|
font, bounds, timeline, route_seconds, scrub, replay.progress_current, replay.progress_total,
|
|
)
|
|
if timeline_action is not None:
|
|
clicked_action = timeline_action
|
|
rl.set_mouse_cursor(
|
|
rl.MouseCursor.MOUSE_CURSOR_POINTING_HAND
|
|
if hovered_tooltip is not None or field_hovered or timeline_hovered
|
|
else rl.MouseCursor.MOUSE_CURSOR_DEFAULT,
|
|
)
|
|
return clicked_action
|
|
|
|
|
|
def apply_replay_control(replay: ManagedReplay, action: str | None, seek_input: str,
|
|
seek_active: bool) -> tuple[str, bool]:
|
|
if action is not None and action.startswith("seek-time:"):
|
|
replay.send(f"\n{action.removeprefix('seek-time:')}\n")
|
|
return "", False
|
|
if action == "seek-focus":
|
|
return seek_input, True
|
|
if action == "seek-blur":
|
|
return seek_input, False
|
|
if action == "seek":
|
|
if seek_input:
|
|
replay.send(f"\n{seek_input}\n")
|
|
return "", False
|
|
if action is not None:
|
|
replay.send(action)
|
|
return seek_input, False
|
|
return seek_input, seek_active
|
|
|
|
|
|
def both_camera_streams_available(available_streams) -> bool:
|
|
return (
|
|
VisionStreamType.VISION_STREAM_ROAD in available_streams
|
|
and VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams
|
|
)
|
|
|
|
|
|
def fused_stream_loss_state(available_streams, missing_since: float | None,
|
|
now: float) -> tuple[float | None, bool]:
|
|
if both_camera_streams_available(available_streams):
|
|
return None, False
|
|
missing_since = now if missing_since is None else missing_since
|
|
return missing_since, now - missing_since >= FUSED_STREAM_GRACE_SECONDS
|
|
|
|
|
|
def draw_source_status(font, rect: rl.Rectangle, live_tracks, valid: bool, alive: bool, source_active: bool,
|
|
show_labels: bool, data_source: str, camera_mode: str,
|
|
both_cameras_available: bool, remote_address: str | None, bridge_connected: bool,
|
|
visible_tracks, motion_states: dict[int, int], track_locations: dict[int, tuple[int, int]],
|
|
source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]],
|
|
table_mode: str, fps: int) -> str | tuple[str, RadarSourceKey] | None:
|
|
rl.draw_rectangle_rec(rect, rl.Color(11, 16, 24, 225))
|
|
tracks = live_tracks.points if valid else ()
|
|
sources = sorted(live_tracks.trackSources, key=radar_source_sort_key) if valid else []
|
|
source_title = "CAN" if data_source == "can" else "LIVE"
|
|
mouse_position = rl.get_mouse_position()
|
|
top_hovered_tooltip = None
|
|
source_clicked_action = None
|
|
source_hovered_tooltip = None
|
|
fps_text = f"{fps} fps"
|
|
fps_size = measure_standalone_text(font, fps_text, 16)
|
|
source_tooltip_anchor_y = rect.y + 49 + max(0, len(sources) - 1) * 30
|
|
|
|
if sources:
|
|
for row, source in enumerate(sources):
|
|
row_y = rect.y + 14 + row * 30
|
|
source_x = rect.x + 34
|
|
draw_track_source_marker(rl.Vector2(rect.x + 20, row_y + 8), 6.0, CYAN, row)
|
|
spec = SPEC_BY_RANGE.get((source.startAddress, source.endAddress))
|
|
compact_name = source_name(source.startAddress, source.endAddress).removeprefix("RADAR_")
|
|
source_parts = [
|
|
(compact_name, f"CAN address range 0x{source.startAddress:X}-0x{source.endAddress:X}"),
|
|
(f"B{source.bus}", f"CAN bus {source.bus} carrying this source"),
|
|
]
|
|
if spec is not None:
|
|
source_parts.extend((
|
|
(f"{spec.frequency} Hz", "Expected radar message frequency"),
|
|
(f"{spec.message_size} B", "Radar CAN message payload size"),
|
|
))
|
|
else:
|
|
details = source_details(source.startAddress, source.endAddress)
|
|
source_parts.append((details, details))
|
|
source_hovered_tooltip = draw_status_parts(
|
|
font, tuple(source_parts), source_x, row_y, 16, CYAN, mouse_position, " / ",
|
|
)
|
|
if source_hovered_tooltip is not None:
|
|
top_hovered_tooltip = source_hovered_tooltip
|
|
|
|
source_key = radar_source_key(source)
|
|
hide_moving, hide_stationary, hide_unknown = source_filter_state(source_filters, source_key)
|
|
source_text = " / ".join(text for text, _ in source_parts)
|
|
source_track_ids = [
|
|
int(track.trackId)
|
|
for track in tracks
|
|
if (
|
|
source.startAddress
|
|
<= track_locations.get(int(track.trackId), (-1, -1))[0]
|
|
<= source.endAddress
|
|
and track_locations.get(int(track.trackId), (-1, -1))[1] == source.bus
|
|
)
|
|
]
|
|
moving_count = sum(motion_states.get(track_id) == 2 for track_id in source_track_ids)
|
|
stationary_count = sum(motion_states.get(track_id) == 1 for track_id in source_track_ids)
|
|
unknown_count = max(0, source.trackCount - moving_count - stationary_count)
|
|
visible_source_tracks = sum(
|
|
source.startAddress <= address <= source.endAddress and bus == source.bus
|
|
for track in visible_tracks
|
|
for address, bus in (track_locations.get(int(track.trackId), (-1, -1)),)
|
|
)
|
|
count_text = f"{visible_source_tracks:>{TRACK_COUNT_FIELD_WIDTH}}/{source.trackCount:<{TRACK_COUNT_FIELD_WIDTH}}"
|
|
count_x = source_x + measure_standalone_text(font, source_text, 16).x + measure_standalone_text(font, " / ", 16).x
|
|
count_size = measure_standalone_text(font, count_text, 16)
|
|
count_rect = rl.Rectangle(count_x, row_y, count_size.x, count_size.y + 4)
|
|
draw_text(font, count_text, count_x, row_y, 16, TEXT)
|
|
if rl.check_collision_point_rec(mouse_position, count_rect):
|
|
top_hovered_tooltip = (
|
|
f"{visible_source_tracks} visible tracks out of {source.trackCount} decoded from this radar source",
|
|
count_rect,
|
|
)
|
|
filter_x = count_rect.x + count_rect.width + 9
|
|
filter_specs = (
|
|
(f"MOVING {moving_count:>{TRACK_COUNT_FIELD_WIDTH}}",
|
|
PURPLE, "moving", hide_moving, moving_count),
|
|
(f"STATIONARY {stationary_count:>{TRACK_COUNT_FIELD_WIDTH}}",
|
|
WHITE, "stationary", hide_stationary, stationary_count),
|
|
(f"UNKNOWN {unknown_count:>{TRACK_COUNT_FIELD_WIDTH}}",
|
|
MUTED, "unknown", hide_unknown, unknown_count),
|
|
)
|
|
for label, color, action, hidden, category_count in filter_specs:
|
|
filter_x, chip_rect, hovered = draw_status_chip(
|
|
font, label, filter_x, row_y - 4, color, mouse_position,
|
|
selected=not hidden, selection_color=MUTED,
|
|
)
|
|
if hovered:
|
|
tooltip_anchor = rl.Rectangle(chip_rect.x, source_tooltip_anchor_y, chip_rect.width, chip_rect.height)
|
|
source_hovered_tooltip = (
|
|
f"{'Show' if hidden else 'Hide'} {category_count} {action} tracks from {compact_name} on bus {source.bus}",
|
|
tooltip_anchor,
|
|
)
|
|
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): # noqa: TID251
|
|
source_clicked_action = (action, source_key)
|
|
if row == 0:
|
|
fps_x = rect.x + rect.width - fps_size.x - 14
|
|
fps_rect = rl.Rectangle(fps_x, row_y, fps_size.x, fps_size.y + 4)
|
|
draw_text(font, fps_text, fps_x, row_y, 16, TEXT)
|
|
if rl.check_collision_point_rec(mouse_position, fps_rect):
|
|
top_hovered_tooltip = ("Debugger rendering frame rate", fps_rect)
|
|
elif not source_active:
|
|
source_text = "no route loaded"
|
|
elif data_source == "liveTracks":
|
|
source_text = "no source metadata"
|
|
elif valid and live_tracks.radarTracksAvailable:
|
|
source_text = "radar detected / parsing"
|
|
else:
|
|
source_text = "searching for supported radar"
|
|
if not sources:
|
|
draw_text(font, source_text, rect.x + 14, rect.y + 14, 16, MUTED)
|
|
fallback_summary = f"{len(visible_tracks)}/{len(tracks)} {fps_text}"
|
|
summary_size = measure_standalone_text(font, fallback_summary, 16)
|
|
draw_text(font, fallback_summary, rect.x + rect.width - summary_size.x - 14, rect.y + 14, 16, TEXT)
|
|
|
|
chip_x = rect.x + 14
|
|
chip_y = source_tooltip_anchor_y
|
|
camera_label = {"fused": "FUSED", "wide 180": "WIDE"}.get(camera_mode, "ROAD")
|
|
table_label = {"motion": "MOTION", "kinematics": "KIN", "object": "OBJ"}[table_mode]
|
|
chip_specs = [
|
|
("OPEN", GREEN, "route", "Open a route or live-car IP, or paste one with Cmd/Ctrl+V"),
|
|
(source_title, CYAN, "source", "Switch CAN / liveTracks source"),
|
|
("LABELS ON" if show_labels else "LABELS", GREEN if show_labels else MUTED, "labels", "Show / hide all labels"),
|
|
]
|
|
if remote_address is not None:
|
|
bridge_label = f"BRIDGE {'OK' if bridge_connected else '--'} {remote_address}"
|
|
bridge_tooltip = (
|
|
f"Receiving live messages from {remote_address}"
|
|
if bridge_connected
|
|
else f"Waiting for remote bridge messages from {remote_address}"
|
|
)
|
|
chip_specs.insert(1, (bridge_label, GREEN if bridge_connected else ORANGE, None, bridge_tooltip))
|
|
if both_cameras_available:
|
|
chip_specs.append((camera_label, CYAN, "camera", "Cycle road / wide / fused camera"))
|
|
chip_specs.append((table_label, MUTED, "table", "Cycle motion / kinematics / object table"))
|
|
clicked_action = source_clicked_action
|
|
hovered_tooltip = source_hovered_tooltip or top_hovered_tooltip
|
|
for label, color, action, tooltip in chip_specs:
|
|
chip_x, chip_rect, hovered = draw_status_chip(font, label, chip_x, chip_y, color, mouse_position)
|
|
if hovered:
|
|
hovered_tooltip = (tooltip, chip_rect)
|
|
if action is not None and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): # noqa: TID251
|
|
clicked_action = action
|
|
rl.set_mouse_cursor(rl.MouseCursor.MOUSE_CURSOR_POINTING_HAND if hovered_tooltip is not None
|
|
else rl.MouseCursor.MOUSE_CURSOR_DEFAULT)
|
|
if hovered_tooltip is not None:
|
|
draw_status_tooltip(font, hovered_tooltip[0], hovered_tooltip[1], rect)
|
|
return clicked_action
|
|
|
|
|
|
def ui_thread(addr: str, start_wide: bool = False, start_fused: bool = False, start_route: str | None = None) -> None:
|
|
remote_relays = start_remote_relays(addr) if addr != "127.0.0.1" else []
|
|
remote_address = addr if remote_relays else None
|
|
remote_bridge_last_message = 0.0
|
|
bridge_connected = False
|
|
messaging_addr = "127.0.0.1"
|
|
rl.set_trace_log_level(rl.TraceLogLevel.LOG_ERROR)
|
|
rl.set_config_flags(rl.ConfigFlags.FLAG_VSYNC_HINT | rl.ConfigFlags.FLAG_WINDOW_RESIZABLE)
|
|
rl.init_window(WINDOW_WIDTH, WINDOW_HEIGHT, "Radar Tracks Debugger")
|
|
primary_position = rl.get_monitor_position(0)
|
|
primary_width = rl.get_monitor_width(0)
|
|
primary_height = rl.get_monitor_height(0)
|
|
rl.set_window_position(
|
|
int(primary_position.x + (primary_width - WINDOW_WIDTH) / 2),
|
|
int(primary_position.y + (primary_height - WINDOW_HEIGHT) / 2),
|
|
)
|
|
rl.maximize_window()
|
|
rl.set_window_focused()
|
|
rl.set_target_fps(max(60, rl.get_monitor_refresh_rate(0)))
|
|
|
|
font_path = os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf")
|
|
font = rl.load_font_ex(font_path, 64, None, 0)
|
|
rl.gen_texture_mipmaps(font.texture)
|
|
rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_TRILINEAR)
|
|
road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD)
|
|
wide_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD)
|
|
camera_view = wide_camera_view if start_wide else road_camera_view
|
|
|
|
overlay_pixels = np.zeros((CAMERA_BUFFER_HEIGHT, CAMERA_BUFFER_WIDTH, 4), dtype=np.uint8)
|
|
overlay_image = rl.gen_image_color(CAMERA_BUFFER_WIDTH, CAMERA_BUFFER_HEIGHT, rl.BLANK)
|
|
overlay_texture = rl.load_texture_from_image(overlay_image)
|
|
rl.set_texture_filter(overlay_texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
|
|
rl.unload_image(overlay_image)
|
|
|
|
sm = messaging.SubMaster([
|
|
"carParams",
|
|
"carState",
|
|
"liveCalibration",
|
|
"liveTracks",
|
|
"modelV2",
|
|
"radarState",
|
|
"roadCameraState",
|
|
"wideRoadCameraState",
|
|
], addr=messaging_addr)
|
|
|
|
process_context = multiprocessing.get_context("spawn")
|
|
decoder_output = process_context.Queue(maxsize=1)
|
|
decoder_process = process_context.Process(
|
|
target=radar_decoder_worker, args=(messaging_addr, decoder_output), daemon=True,
|
|
)
|
|
decoder_process.start()
|
|
timeline_output = process_context.Queue(maxsize=1)
|
|
timeline_process = None
|
|
replay_timeline = ReplayTimeline()
|
|
|
|
can_tracks = RadarSnapshot()
|
|
live_tracks = RadarSnapshot()
|
|
can_data_seen = False
|
|
live_data_seen = False
|
|
can_data_alive = False
|
|
decoded_motion_states: dict[int, int] = {}
|
|
|
|
calibration = None
|
|
calibration_key = None
|
|
camera_projection_height = float(CAMERA_BUFFER_HEIGHT)
|
|
show_labels = False
|
|
source_filters: dict[RadarSourceKey, tuple[bool, bool, bool]] = {}
|
|
use_can_source = True
|
|
table_mode_index = 0
|
|
table_scroll = 0
|
|
selected_track_id = None
|
|
composite_mode = start_fused
|
|
fused_streams_missing_since = None
|
|
model_pixels = np.zeros((CAMERA_BUFFER_HEIGHT, CAMERA_BUFFER_WIDTH, 3), dtype=np.uint8)
|
|
dummy_top_down = np.zeros((384, 960), dtype=np.uint8)
|
|
overlay_dirty = True
|
|
route_dialog_open = start_route is None and addr == "127.0.0.1"
|
|
route_input = start_route or ""
|
|
route_cursor = len(route_input)
|
|
route_error = ""
|
|
route_select_all = False
|
|
replay_process = None
|
|
route_loading = False
|
|
route_loading_started = 0.0
|
|
replay_seek_input = ""
|
|
replay_seek_active = False
|
|
replay_scrub = ReplayScrubState()
|
|
route_message_start_mono: int | None = None
|
|
route_fingerprint = ""
|
|
current_route_seconds = route_start_seconds(route_input)
|
|
|
|
def open_target(target: str) -> str:
|
|
nonlocal remote_relays, remote_address, remote_bridge_last_message, bridge_connected
|
|
nonlocal replay_process, timeline_process, replay_timeline, replay_scrub
|
|
nonlocal route_input, route_loading, route_loading_started, route_message_start_mono
|
|
nonlocal route_fingerprint, current_route_seconds, replay_seek_input, replay_seek_active
|
|
nonlocal can_tracks, live_tracks, can_data_seen, live_data_seen, can_data_alive, decoded_motion_states
|
|
nonlocal table_scroll, selected_track_id, calibration, calibration_key, overlay_dirty
|
|
nonlocal decoder_process
|
|
|
|
target = target.strip()
|
|
if not target:
|
|
return "Enter a route, segment ID, or IP address."
|
|
|
|
stop_route_replay(replay_process)
|
|
replay_process = None
|
|
stop_timeline_process(timeline_process)
|
|
timeline_process = None
|
|
stop_remote_relays(remote_relays)
|
|
remote_relays = []
|
|
remote_address = None
|
|
remote_bridge_last_message = 0.0
|
|
bridge_connected = False
|
|
decoder_process.terminate()
|
|
decoder_process.join(timeout=1.0)
|
|
|
|
for output_queue in (decoder_output, timeline_output):
|
|
while True:
|
|
try:
|
|
output_queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
|
|
can_tracks = RadarSnapshot()
|
|
live_tracks = RadarSnapshot()
|
|
can_data_seen = False
|
|
live_data_seen = False
|
|
can_data_alive = False
|
|
decoded_motion_states = {}
|
|
replay_timeline = ReplayTimeline()
|
|
replay_scrub = ReplayScrubState()
|
|
replay_seek_input = ""
|
|
replay_seek_active = False
|
|
route_loading = False
|
|
route_message_start_mono = None
|
|
route_fingerprint = ""
|
|
current_route_seconds = 0.0
|
|
table_scroll = 0
|
|
selected_track_id = None
|
|
calibration = None
|
|
calibration_key = None
|
|
overlay_dirty = True
|
|
|
|
if (live_ip := parse_live_ip(target)) is not None:
|
|
route_input = live_ip
|
|
remote_relays = start_remote_relays(live_ip)
|
|
remote_address = live_ip
|
|
decoder_process = process_context.Process(
|
|
target=radar_decoder_worker, args=(messaging_addr, decoder_output), daemon=True,
|
|
)
|
|
decoder_process.start()
|
|
print(f"Radar debugger opened live car: {live_ip}", flush=True)
|
|
return ""
|
|
|
|
route_input = target
|
|
replay_process, error = replace_route_replay(None, target)
|
|
route_loading = replay_process is not None and not error
|
|
route_loading_started = time.monotonic()
|
|
current_route_seconds = route_start_seconds(target)
|
|
if route_loading:
|
|
replay_timeline = ReplayTimeline(current_route_seconds, current_route_seconds)
|
|
timeline_process = start_timeline_process(process_context, timeline_process, timeline_output, target)
|
|
decoder_process = process_context.Process(
|
|
target=radar_decoder_worker, args=(messaging_addr, decoder_output), daemon=True,
|
|
)
|
|
decoder_process.start()
|
|
return error
|
|
|
|
if start_route:
|
|
route_error = open_target(start_route)
|
|
route_dialog_open = bool(route_error)
|
|
composite_mode = composite_mode and not route_dialog_open
|
|
startup_focus_deadline = time.monotonic() + 2.0
|
|
next_startup_focus_request = 0.0
|
|
|
|
while not rl.window_should_close() and (replay_process is None or replay_process.poll() is None):
|
|
now = time.monotonic()
|
|
if replay_process is not None:
|
|
replay_process.drain_output()
|
|
if replay_process.car_fingerprint:
|
|
route_fingerprint = replay_process.car_fingerprint
|
|
if now < startup_focus_deadline and now >= next_startup_focus_request:
|
|
rl.set_window_focused()
|
|
next_startup_focus_request = now + 0.1
|
|
open_entered_route = False
|
|
modifier_down = (
|
|
rl.is_key_down(rl.KeyboardKey.KEY_LEFT_SUPER)
|
|
or rl.is_key_down(rl.KeyboardKey.KEY_RIGHT_SUPER)
|
|
or rl.is_key_down(rl.KeyboardKey.KEY_LEFT_CONTROL)
|
|
or rl.is_key_down(rl.KeyboardKey.KEY_RIGHT_CONTROL)
|
|
)
|
|
if route_dialog_open:
|
|
paste_pressed = modifier_down and rl.is_key_pressed(rl.KeyboardKey.KEY_V)
|
|
select_all_pressed = modifier_down and rl.is_key_pressed(rl.KeyboardKey.KEY_A)
|
|
codepoint = rl.get_char_pressed()
|
|
while codepoint > 0:
|
|
if not (paste_pressed or select_all_pressed) and 32 <= codepoint <= 126 and len(route_input) < 180:
|
|
if route_select_all:
|
|
route_input = ""
|
|
route_cursor = 0
|
|
route_select_all = False
|
|
route_input = route_input[:route_cursor] + chr(codepoint) + route_input[route_cursor:]
|
|
route_cursor += 1
|
|
route_error = ""
|
|
codepoint = rl.get_char_pressed()
|
|
if select_all_pressed:
|
|
route_select_all = bool(route_input)
|
|
route_cursor = len(route_input)
|
|
elif paste_pressed:
|
|
clipboard_text = rl.get_clipboard_text() or ""
|
|
printable_text = "".join(character for character in clipboard_text if 32 <= ord(character) <= 126)
|
|
if route_select_all:
|
|
route_input = ""
|
|
route_cursor = 0
|
|
inserted_text = printable_text[:180 - len(route_input)]
|
|
route_input = route_input[:route_cursor] + inserted_text + route_input[route_cursor:]
|
|
route_cursor += len(inserted_text)
|
|
route_select_all = False
|
|
route_error = ""
|
|
elif edit_key_pressed(rl.KeyboardKey.KEY_BACKSPACE):
|
|
if route_select_all:
|
|
route_input = ""
|
|
route_cursor = 0
|
|
elif route_cursor > 0:
|
|
route_input = route_input[:route_cursor - 1] + route_input[route_cursor:]
|
|
route_cursor -= 1
|
|
route_select_all = False
|
|
route_error = ""
|
|
elif edit_key_pressed(rl.KeyboardKey.KEY_DELETE):
|
|
if route_select_all:
|
|
route_input = ""
|
|
route_cursor = 0
|
|
elif route_cursor < len(route_input):
|
|
route_input = route_input[:route_cursor] + route_input[route_cursor + 1:]
|
|
route_select_all = False
|
|
route_error = ""
|
|
elif edit_key_pressed(rl.KeyboardKey.KEY_LEFT):
|
|
route_cursor = 0 if route_select_all else max(0, route_cursor - 1)
|
|
route_select_all = False
|
|
elif edit_key_pressed(rl.KeyboardKey.KEY_RIGHT):
|
|
route_cursor = len(route_input) if route_select_all else min(len(route_input), route_cursor + 1)
|
|
route_select_all = False
|
|
elif edit_key_pressed(rl.KeyboardKey.KEY_HOME):
|
|
route_cursor = 0
|
|
route_select_all = False
|
|
elif edit_key_pressed(rl.KeyboardKey.KEY_END):
|
|
route_cursor = len(route_input)
|
|
route_select_all = False
|
|
if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
|
|
open_entered_route = True
|
|
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
route_dialog_open = False
|
|
route_error = ""
|
|
route_select_all = False
|
|
elif replay_seek_active:
|
|
codepoint = rl.get_char_pressed()
|
|
while codepoint > 0:
|
|
if chr(codepoint).isdigit() and len(replay_seek_input) < 7:
|
|
replay_seek_input += chr(codepoint)
|
|
codepoint = rl.get_char_pressed()
|
|
if edit_key_pressed(rl.KeyboardKey.KEY_BACKSPACE):
|
|
replay_seek_input = replay_seek_input[:-1]
|
|
elif rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER) and replay_process is not None:
|
|
replay_seek_input, replay_seek_active = apply_replay_control(
|
|
replay_process, "seek", replay_seek_input, replay_seek_active,
|
|
)
|
|
elif rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
replay_seek_active = False
|
|
elif modifier_down and rl.is_key_pressed(rl.KeyboardKey.KEY_V):
|
|
clipboard_text = rl.get_clipboard_text() or ""
|
|
route_input = "".join(character for character in clipboard_text if 32 <= ord(character) <= 126)[:180]
|
|
route_cursor = len(route_input)
|
|
route_select_all = False
|
|
route_error = open_target(route_input)
|
|
route_dialog_open = bool(route_error)
|
|
composite_mode = composite_mode and not route_dialog_open
|
|
elif modifier_down:
|
|
pass
|
|
elif replay_process is not None and rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
|
|
replay_seek_active = True
|
|
elif replay_process is not None and (replay_command := replay_keyboard_command()) is not None:
|
|
replay_process.send(replay_command)
|
|
if open_entered_route:
|
|
route_error = open_target(route_input)
|
|
route_dialog_open = bool(route_error)
|
|
route_select_all = False
|
|
|
|
sm.update(0)
|
|
if remote_address is not None and any(sm.updated.values()):
|
|
remote_bridge_last_message = time.monotonic()
|
|
if sm.updated["carParams"]:
|
|
message_fingerprint = str(sm["carParams"].carFingerprint)
|
|
if message_fingerprint:
|
|
route_fingerprint = message_fingerprint
|
|
route_message_times = [
|
|
sm.logMonoTime[service]
|
|
for service in ("carState", "modelV2", "roadCameraState", "liveTracks")
|
|
if sm.updated[service] and sm.logMonoTime[service] > 0
|
|
]
|
|
if replay_process is not None and route_message_times:
|
|
current_route_mono = max(route_message_times)
|
|
if route_message_start_mono is None:
|
|
route_message_start_mono = current_route_mono
|
|
current_route_seconds = (
|
|
route_start_seconds(route_input) + (current_route_mono - route_message_start_mono) / 1e9
|
|
)
|
|
if (route_loading
|
|
and time.monotonic() - route_loading_started > 0.15
|
|
and any(sm.updated[service] for service in ("carState", "modelV2", "roadCameraState", "liveTracks"))):
|
|
route_loading = False
|
|
while True:
|
|
try:
|
|
can_tracks, decoded_motion_states, can_data_alive, can_data_seen, last_can_message_time = decoder_output.get_nowait()
|
|
remote_bridge_last_message = max(remote_bridge_last_message, last_can_message_time)
|
|
except queue.Empty:
|
|
break
|
|
bridge_connected = (
|
|
remote_address is not None
|
|
and bool(remote_relays)
|
|
and remote_relays[0].poll() is None
|
|
and time.monotonic() - remote_bridge_last_message < REMOTE_BRIDGE_TIMEOUT_SECONDS
|
|
)
|
|
while True:
|
|
try:
|
|
replay_timeline = timeline_output.get_nowait()
|
|
if replay_timeline.car_fingerprint:
|
|
route_fingerprint = replay_timeline.car_fingerprint
|
|
except queue.Empty:
|
|
break
|
|
if sm.updated["liveTracks"]:
|
|
live_tracks = make_radar_snapshot(sm["liveTracks"])
|
|
live_data_seen = sm.valid["liveTracks"]
|
|
|
|
window_width = rl.get_screen_width()
|
|
window_height = rl.get_screen_height()
|
|
replay_bar_height = 94.0 if replay_process is not None else 0.0
|
|
content_height = window_height - replay_bar_height
|
|
left_width = float(round(window_width * 0.58))
|
|
camera_height = float(round(content_height * 0.64))
|
|
camera_rect = rl.Rectangle(0, 0, left_width, camera_height)
|
|
table_rect = rl.Rectangle(0, camera_height, left_width, content_height - camera_height)
|
|
radar_rect = rl.Rectangle(left_width, 0, window_width - left_width, content_height)
|
|
replay_bar_rect = rl.Rectangle(0, content_height, window_width, replay_bar_height)
|
|
status_sources = can_tracks.trackSources if use_can_source else live_tracks.trackSources
|
|
status_height = 88 + max(0, len(status_sources) - 1) * 30
|
|
status_rect = rl.Rectangle(0, 0, left_width, min(status_height, camera_height))
|
|
|
|
if sm.valid["roadCameraState"] and sm.valid["liveCalibration"] and camera_view.frame:
|
|
camera_key = ("tici", str(sm["roadCameraState"].sensor))
|
|
device_camera = DEVICE_CAMERAS.get(camera_key)
|
|
is_wide_camera = camera_view.stream_type == VisionStreamType.VISION_STREAM_WIDE_ROAD
|
|
rpy_calib = np.asarray(sm["liveCalibration"].rpyCalib)
|
|
wide_from_device_euler = np.asarray(sm["liveCalibration"].wideFromDeviceEuler)
|
|
next_calibration_key = (
|
|
camera_key, camera_view.stream_type, camera_view.frame.width, camera_view.frame.height,
|
|
*rpy_calib, *wide_from_device_euler,
|
|
)
|
|
if device_camera is not None and next_calibration_key != calibration_key:
|
|
camera = device_camera.ecam if is_wide_camera else device_camera.fcam
|
|
calibration, camera_projection_height = build_camera_calibration(
|
|
camera, rpy_calib, wide_from_device_euler if is_wide_camera else None,
|
|
)
|
|
calibration_key = next_calibration_key
|
|
overlay_dirty = True
|
|
if sm.updated["modelV2"]:
|
|
overlay_dirty = True
|
|
|
|
decoded_tracks = list(can_tracks.points) if can_data_seen else []
|
|
decoded_track_signals = {signals.trackId: signals for signals in can_tracks.trackSignals}
|
|
decoded_track_locations = {
|
|
int(track.trackId): (track.canAddress, track.canBus)
|
|
for track in decoded_tracks
|
|
if track.canAddress >= 0 and track.canBus >= 0
|
|
}
|
|
if use_can_source:
|
|
selected_tracks = can_tracks
|
|
tracks = decoded_tracks
|
|
raw_signals = can_tracks.rawSignals
|
|
data_valid = can_data_seen
|
|
data_alive = can_data_alive
|
|
motion_states = decoded_motion_states
|
|
track_signals = decoded_track_signals
|
|
track_locations = decoded_track_locations
|
|
data_source = "can"
|
|
else:
|
|
selected_tracks = live_tracks
|
|
data_valid = live_data_seen
|
|
data_alive = sm.alive["liveTracks"]
|
|
tracks = list(selected_tracks.points) if data_valid else []
|
|
raw_signals = ()
|
|
motion_states = match_decoded_track_values(tracks, decoded_tracks, decoded_motion_states)
|
|
track_signals = match_decoded_track_values(tracks, decoded_tracks, decoded_track_signals)
|
|
track_locations = match_decoded_track_values(tracks, decoded_tracks, decoded_track_locations)
|
|
data_source = "liveTracks"
|
|
|
|
source_filter_values = [
|
|
source_filter_state(source_filters, radar_source_key(source))
|
|
for source in selected_tracks.trackSources
|
|
] or [DEFAULT_SOURCE_FILTERS]
|
|
hide_moving = all(filters[0] for filters in source_filter_values)
|
|
hide_stationary = all(filters[1] for filters in source_filter_values)
|
|
hide_unknown = all(filters[2] for filters in source_filter_values)
|
|
tracks = filter_tracks(
|
|
tracks, motion_states, track_locations, selected_tracks.trackSources, source_filters,
|
|
)
|
|
current_table_mode = TABLE_MODES[table_mode_index]
|
|
capacity = table_capacity(table_rect)
|
|
table_row_count = (
|
|
sum(int(track.trackId) in track_signals for track in tracks) + len(raw_signals)
|
|
if current_table_mode == "signals"
|
|
else len(tracks)
|
|
)
|
|
max_scroll = max(0, table_row_count - capacity)
|
|
if rl.check_collision_point_rec(rl.get_mouse_position(), table_rect):
|
|
table_scroll -= int(rl.get_mouse_wheel_move() * 3)
|
|
table_scroll = int(np.clip(table_scroll, 0, max_scroll))
|
|
table_hover_id = None if composite_mode or current_table_mode == "signals" else table_hovered_track_id(
|
|
tracks, table_rect, table_scroll, rl.get_mouse_position(),
|
|
)
|
|
if (table_hover_id is not None
|
|
and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)): # noqa: TID251 - standalone raylib tool
|
|
selected_track_id = table_hover_id
|
|
|
|
v_ego = float(sm["carState"].vEgo) if sm.valid["carState"] else 0.0
|
|
model = sm["modelV2"] if sm.valid["modelV2"] else None
|
|
|
|
rl.begin_drawing()
|
|
rl.clear_background(BACKGROUND)
|
|
if composite_mode:
|
|
rl.set_mouse_cursor(rl.MouseCursor.MOUSE_CURSOR_DEFAULT)
|
|
fused_bounds = rl.Rectangle(0, 0, window_width, content_height)
|
|
fused_device_camera = None
|
|
rpy_calib = np.zeros(3)
|
|
wide_from_device_euler = np.zeros(3)
|
|
if sm.valid["roadCameraState"] and sm.valid["liveCalibration"]:
|
|
fused_device_camera = DEVICE_CAMERAS.get(("tici", str(sm["roadCameraState"].sensor)))
|
|
rpy_calib = np.asarray(sm["liveCalibration"].rpyCalib)
|
|
wide_from_device_euler = np.asarray(sm["liveCalibration"].wideFromDeviceEuler)
|
|
selected_track_id = draw_fused_camera_mode(
|
|
font, road_camera_view, wide_camera_view, fused_device_camera, rpy_calib, wide_from_device_euler,
|
|
fused_bounds, tracks, show_labels, motion_states,
|
|
track_locations, selected_tracks.trackSources, selected_track_id,
|
|
)
|
|
fused_available_streams = set(road_camera_view.available_streams) | set(wide_camera_view.available_streams)
|
|
fused_streams_missing_since, streams_timed_out = fused_stream_loss_state(
|
|
fused_available_streams, fused_streams_missing_since, time.monotonic(),
|
|
)
|
|
if streams_timed_out:
|
|
composite_mode = False
|
|
fused_streams_missing_since = None
|
|
if VisionStreamType.VISION_STREAM_ROAD in fused_available_streams:
|
|
camera_view = road_camera_view
|
|
elif VisionStreamType.VISION_STREAM_WIDE_ROAD in fused_available_streams:
|
|
camera_view = wide_camera_view
|
|
table_mode = TABLE_MODES[table_mode_index]
|
|
clicked_action = draw_source_status(
|
|
font, status_rect, selected_tracks, data_valid, data_alive,
|
|
replay_process is not None or can_data_seen or live_data_seen, show_labels, data_source,
|
|
"fused", both_camera_streams_available(fused_available_streams), remote_address, bridge_connected,
|
|
tracks, motion_states, track_locations, source_filters, table_mode, rl.get_fps(),
|
|
)
|
|
if isinstance(clicked_action, tuple):
|
|
action, source_key = clicked_action
|
|
toggle_source_filter(source_filters, source_key, {"moving": 0, "stationary": 1, "unknown": 2}[action])
|
|
table_scroll = 0
|
|
elif clicked_action == "source":
|
|
use_can_source = not use_can_source
|
|
table_scroll = 0
|
|
selected_track_id = None
|
|
elif clicked_action == "labels":
|
|
show_labels = not show_labels
|
|
elif clicked_action == "camera":
|
|
composite_mode = False
|
|
fused_streams_missing_since = None
|
|
camera_view = road_camera_view
|
|
elif clicked_action == "table":
|
|
table_mode_index = (table_mode_index + 1) % len(TABLE_MODES)
|
|
elif clicked_action == "route":
|
|
composite_mode = False
|
|
route_dialog_open = True
|
|
route_error = ""
|
|
route_select_all = bool(route_input)
|
|
route_cursor = len(route_input)
|
|
if replay_process is not None:
|
|
draw_replay_event_overlay(
|
|
font, fused_bounds, status_rect.y + status_rect.height + 10,
|
|
replay_timeline, current_route_seconds,
|
|
)
|
|
if route_loading:
|
|
draw_route_loading(font, fused_bounds, route_input, time.monotonic() - route_loading_started)
|
|
if replay_process is not None:
|
|
replay_action = draw_replay_control_bar(
|
|
font, replay_bar_rect, replay_process, current_route_seconds, route_fingerprint,
|
|
replay_timeline, v_ego, route_loading, replay_seek_input, replay_seek_active, replay_scrub,
|
|
)
|
|
replay_seek_input, replay_seek_active = apply_replay_control(
|
|
replay_process, replay_action, replay_seek_input, replay_seek_active,
|
|
)
|
|
rl.end_drawing()
|
|
continue
|
|
|
|
fused_streams_missing_since = None
|
|
camera_view.render(camera_rect)
|
|
camera_draw_rect = camera_content_rect(camera_view, camera_rect)
|
|
|
|
mouse_position = rl.get_mouse_position()
|
|
camera_hovered_id = None
|
|
if (rl.check_collision_point_rec(mouse_position, camera_draw_rect)
|
|
and not rl.check_collision_point_rec(mouse_position, status_rect)):
|
|
camera_hovered_id = hovered_track_id(
|
|
tracks,
|
|
lambda track, calibration=calibration, camera_draw_rect=camera_draw_rect,
|
|
camera_projection_height=camera_projection_height: camera_track_geometry(
|
|
calibration, track, camera_draw_rect, camera_projection_height,
|
|
),
|
|
mouse_position,
|
|
)
|
|
top_down_hovered_id = None
|
|
if camera_hovered_id is None and rl.check_collision_point_rec(mouse_position, radar_rect):
|
|
top_down_hovered_id = hovered_track_id(
|
|
tracks, lambda track, radar_rect=radar_rect: top_down_track_geometry(radar_rect, track), mouse_position,
|
|
)
|
|
current_hovered_id = camera_hovered_id if camera_hovered_id is not None else top_down_hovered_id
|
|
selected_track_id = retain_selected_track_id(selected_track_id, current_hovered_id, tracks)
|
|
|
|
if selected_track_id is not None and current_table_mode != "signals":
|
|
sorted_tracks = sorted(tracks, key=lambda track: (track.dRel, track.trackId))
|
|
hovered_row = next((index for index, track in enumerate(sorted_tracks)
|
|
if int(track.trackId) == selected_track_id), None)
|
|
if hovered_row is not None:
|
|
if hovered_row < table_scroll:
|
|
table_scroll = hovered_row
|
|
elif hovered_row >= table_scroll + capacity:
|
|
table_scroll = hovered_row - capacity + 1
|
|
table_scroll = int(np.clip(table_scroll, 0, max_scroll))
|
|
|
|
if overlay_dirty:
|
|
model_pixels.fill(0)
|
|
dummy_top_down.fill(0)
|
|
if sm.valid["modelV2"]:
|
|
plot_model(sm["modelV2"], model_pixels, calibration, (None, dummy_top_down))
|
|
model_mask = np.any(model_pixels > 0, axis=2)
|
|
overlay_pixels[:, :, :3] = model_pixels
|
|
overlay_pixels[:, :, 3] = model_mask * 220
|
|
rl.update_texture(overlay_texture, rl.ffi.cast("void *", overlay_pixels.ctypes.data))
|
|
overlay_dirty = False
|
|
rl.draw_texture_pro(
|
|
overlay_texture,
|
|
rl.Rectangle(0, 0, CAMERA_BUFFER_WIDTH, camera_projection_height),
|
|
camera_draw_rect,
|
|
rl.Vector2(0, 0),
|
|
0,
|
|
WHITE,
|
|
)
|
|
|
|
draw_camera_tracks(font, calibration, tracks, camera_draw_rect, show_labels, motion_states,
|
|
track_locations, selected_tracks.trackSources,
|
|
camera_projection_height, camera_hovered_id, selected_track_id, table_hover_id)
|
|
draw_top_down(
|
|
font, radar_rect, tracks, show_labels, model, motion_states,
|
|
hide_moving, hide_stationary, hide_unknown, track_locations, selected_tracks.trackSources,
|
|
top_down_hovered_id, selected_track_id, table_hover_id,
|
|
)
|
|
table_mode = TABLE_MODES[table_mode_index]
|
|
draw_track_table(font, table_rect, tracks, motion_states, table_scroll, selected_track_id, table_hover_id,
|
|
track_signals, track_locations, table_mode, raw_signals)
|
|
clicked_action = draw_source_status(
|
|
font, status_rect, selected_tracks, data_valid, data_alive,
|
|
replay_process is not None or can_data_seen or live_data_seen, show_labels, data_source,
|
|
"wide 180" if camera_view.stream_type == VisionStreamType.VISION_STREAM_WIDE_ROAD else "road",
|
|
both_camera_streams_available(camera_view.available_streams), remote_address, bridge_connected,
|
|
tracks, motion_states, track_locations, source_filters, table_mode, rl.get_fps(),
|
|
)
|
|
if isinstance(clicked_action, tuple):
|
|
action, source_key = clicked_action
|
|
toggle_source_filter(source_filters, source_key, {"moving": 0, "stationary": 1, "unknown": 2}[action])
|
|
table_scroll = 0
|
|
elif clicked_action == "source":
|
|
use_can_source = not use_can_source
|
|
table_scroll = 0
|
|
selected_track_id = None
|
|
elif clicked_action == "labels":
|
|
show_labels = not show_labels
|
|
elif clicked_action == "camera":
|
|
if camera_view is road_camera_view:
|
|
camera_view = wide_camera_view
|
|
elif both_camera_streams_available(camera_view.available_streams):
|
|
composite_mode = True
|
|
fused_streams_missing_since = None
|
|
elif clicked_action == "table":
|
|
table_mode_index = (table_mode_index + 1) % len(TABLE_MODES)
|
|
elif clicked_action == "route":
|
|
route_dialog_open = True
|
|
route_error = ""
|
|
route_select_all = bool(route_input)
|
|
route_cursor = len(route_input)
|
|
if replay_process is not None:
|
|
draw_replay_event_overlay(
|
|
font, camera_draw_rect, status_rect.y + status_rect.height + 10,
|
|
replay_timeline, current_route_seconds,
|
|
)
|
|
if route_dialog_open:
|
|
route_action = draw_route_dialog(
|
|
font, rl.Rectangle(0, 0, window_width, window_height), route_input, route_cursor, route_error, route_select_all,
|
|
)
|
|
if route_action == "cancel":
|
|
route_dialog_open = False
|
|
route_error = ""
|
|
route_select_all = False
|
|
elif route_action == "open":
|
|
route_error = open_target(route_input)
|
|
route_dialog_open = bool(route_error)
|
|
route_select_all = False
|
|
if route_loading and not route_dialog_open:
|
|
draw_route_loading(
|
|
font, rl.Rectangle(0, 0, window_width, window_height), route_input, time.monotonic() - route_loading_started,
|
|
)
|
|
if replay_process is not None and not route_dialog_open:
|
|
replay_action = draw_replay_control_bar(
|
|
font, replay_bar_rect, replay_process, current_route_seconds, route_fingerprint,
|
|
replay_timeline, v_ego, route_loading, replay_seek_input, replay_seek_active, replay_scrub,
|
|
)
|
|
replay_seek_input, replay_seek_active = apply_replay_control(
|
|
replay_process, replay_action, replay_seek_input, replay_seek_active,
|
|
)
|
|
if table_hover_id is not None and not route_dialog_open:
|
|
rl.set_mouse_cursor(rl.MouseCursor.MOUSE_CURSOR_POINTING_HAND)
|
|
rl.end_drawing()
|
|
|
|
stop_route_replay(replay_process)
|
|
stop_timeline_process(timeline_process)
|
|
stop_remote_relays(remote_relays)
|
|
decoder_process.terminate()
|
|
decoder_process.join(timeout=1.0)
|
|
decoder_output.close()
|
|
timeline_output.close()
|
|
rl.unload_texture(overlay_texture)
|
|
rl.unload_font(font)
|
|
road_camera_view.close()
|
|
wide_camera_view.close()
|
|
rl.close_window()
|
|
|
|
|
|
def get_arg_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
description="Decode live or replay CAN with Hyundai auto radar discovery and visualize the resulting tracks.",
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
)
|
|
parser.add_argument("ip_address", nargs="?", default="127.0.0.1", help="Address publishing replay or on-car messages.")
|
|
parser.add_argument("--route", help="Route or segment ID to open in replay when the debugger starts.")
|
|
parser.add_argument("--wide", action="store_true", help="Start with the wide/full field-of-view road camera.")
|
|
parser.add_argument("--fused", action="store_true", help="Start with the full-screen fused road and wide cameras.")
|
|
return parser
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = get_arg_parser().parse_args(sys.argv[1:])
|
|
try:
|
|
ui_thread(args.ip_address, args.wide, args.fused, args.route)
|
|
except KeyboardInterrupt:
|
|
pass
|