mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
c532223fa0
Bundle the verified VASM model, enable it at startup, and split VASM and vision speed-sign inference across little cores when both are active. Restore VASM warnings in PiP activation, red outlines, and onroad blind-spot borders, including expanded Android Auto views. Improve cabin snapshots with longer warm-up, adjusted capture timeouts, a fresh preview frame, and clearer capture buttons and save prompts. Polish Galaxy settings with dimmed gated controls and icons, clearer vehicle fingerprinting and steering availability descriptions, rounded speed-offset ranges, and immediate refresh after saving. Improve updater startup messages and preserve update/reconnect screens across service interruptions and restarts. Add regression tests for the updated behavior.
330 lines
14 KiB
Python
330 lines
14 KiB
Python
"""Synthetic serialized cabin planes and fail-closed observation lifecycle."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from openpilot.common.params import Params
|
|
from openpilot.starpilot.spot_monitor.inference import VASMInference
|
|
from openpilot.starpilot.spot_monitor.policy import decode_annotation
|
|
from openpilot.starpilot.spot_monitor.preferences import Preferences, encode
|
|
from openpilot.starpilot.spot_monitor.runtime import CpuBudget, VASMRuntime, cabin_client_factory, copy_cabin_nv12, publish_observation
|
|
|
|
|
|
class State:
|
|
def __init__(self, **kwargs):
|
|
self.__dict__.update(kwargs)
|
|
|
|
|
|
class Messages:
|
|
def __init__(self, clock):
|
|
from opendbc.car.structs import car
|
|
self.clock = clock
|
|
self.seen = dict.fromkeys(("deviceState", "carState"), True)
|
|
self.alive = self.seen.copy()
|
|
self.valid = self.seen.copy()
|
|
self.logMonoTime = dict.fromkeys(self.seen, clock[0])
|
|
self.recv_time = dict.fromkeys(self.seen, clock[0] / 1e9)
|
|
self.values = {"deviceState": State(started=True, cpuUsagePercent=[10.0] * 8),
|
|
"carState": State(canValid=True, gearShifter=car.CarState.GearShifter.drive)}
|
|
|
|
def __getitem__(self, name):
|
|
return self.values[name]
|
|
|
|
def update(self, timeout):
|
|
assert timeout == 0
|
|
|
|
def refresh(self):
|
|
for name in self.seen:
|
|
self.logMonoTime[name] = self.clock[0]
|
|
self.recv_time[name] = self.clock[0] / 1e9
|
|
|
|
|
|
class Client:
|
|
width, height, stride, uv_offset = 32, 32, 40, 1320
|
|
|
|
def __init__(self):
|
|
self.frames = []
|
|
self.connected = True
|
|
|
|
def is_connected(self):
|
|
return self.connected
|
|
|
|
def connect(self, blocking):
|
|
assert blocking is False
|
|
self.connected = True
|
|
|
|
def recv(self, timeout):
|
|
assert timeout == 0
|
|
if not self.frames:
|
|
return None
|
|
self.timestamp_eof, self.frame_id, raw = self.frames.pop(0)
|
|
return State(data=raw)
|
|
|
|
def add(self, eof, frame_id):
|
|
raw = bytearray(self.uv_offset + self.stride * self.height // 2)
|
|
for y in range(self.height):
|
|
raw[y * self.stride:y * self.stride + self.width] = bytes([y]) * self.width
|
|
raw[self.uv_offset:] = bytes([128]) * (len(raw) - self.uv_offset)
|
|
self.frames.append((eof, frame_id, bytes(raw)))
|
|
|
|
|
|
class Model(VASMInference):
|
|
def __init__(self):
|
|
super().__init__(Path("/external/model.onnx"))
|
|
self.loaded_sha256 = "5d20cdbb457ba18db51a537ee2e305bbe442264b1613956068d473e35d15900d"
|
|
|
|
def load(self):
|
|
return True
|
|
|
|
def infer_nv12(self, frame, **kwargs):
|
|
assert frame.shape == (48, 32) and frame[31, 0] == 31 and frame[32, 0] == 128
|
|
return 0.96
|
|
|
|
|
|
def test_stride_uv_offset_copy_and_short_buffer_refused():
|
|
client = Client()
|
|
client.add(2_000_000_000, 0)
|
|
copied = copy_cabin_nv12(client)
|
|
assert copied is not None
|
|
image, eof, frame_id = copied
|
|
assert image.shape == (48, 32) and eof == 2_000_000_000 and frame_id == 0
|
|
assert image[31, 0] == 31 and image[32, 0] == 128
|
|
client.frames.append((2_000_000_100, 1, b"short"))
|
|
assert copy_cabin_nv12(client) is None
|
|
|
|
|
|
def test_real_vision_stream_enum_selects_only_existing_camerad_cabin():
|
|
from openpilot.cereal.visionipc import VisionStreamType
|
|
|
|
calls = []
|
|
def constructor(*args):
|
|
calls.append(args)
|
|
return object()
|
|
cabin_client_factory(constructor, VisionStreamType)
|
|
assert calls == [("camerad", VisionStreamType.VISION_STREAM_CABIN, True)]
|
|
|
|
|
|
def test_cpu_budget_frozen_target_smoothing_and_invalid_conservative():
|
|
budget = CpuBudget()
|
|
assert budget.update([10.0] * 8, now_mono_ns=1_000_000_000) == 1.0
|
|
busy = budget.update([90.0] * 8, now_mono_ns=6_000_000_000)
|
|
assert 3.3 < busy < 3.6 # frozen hot-core target 3.5, smoothed
|
|
recovered = budget.update([0.0] * 8, now_mono_ns=11_000_000_000)
|
|
assert 1.0 < recovered < 1.1
|
|
assert budget.update([], now_mono_ns=12_000_000_000) == 4.0
|
|
assert budget.update([float("nan")], now_mono_ns=13_000_000_000) == 4.0
|
|
assert budget.update([True], now_mono_ns=14_000_000_000) == 4.0
|
|
|
|
|
|
def test_vasm_uses_its_own_core_with_vision_and_restores_solo_cores(tmp_path):
|
|
params = Params(str(tmp_path / "params"))
|
|
clock = [1_000_000_000]
|
|
runtime = VASMRuntime(params, Messages(clock), Client, lambda _event: None)
|
|
with patch('openpilot.starpilot.spot_monitor.runtime.feature_requested', side_effect=[True, False]), \
|
|
patch('openpilot.starpilot.spot_monitor.runtime.set_core_affinity') as affinity:
|
|
runtime._refresh_settings(clock[0])
|
|
assert runtime.inference_cores == (2,)
|
|
clock[0] += 2_000_000_000
|
|
runtime._refresh_settings(clock[0])
|
|
assert runtime.inference_cores == (0, 1, 2)
|
|
assert [call.args[0] for call in affinity.call_args_list] == [[2], [0, 1, 2]]
|
|
|
|
|
|
def test_enabled_fresh_drive_new_frames_and_stale_reset(tmp_path):
|
|
from opendbc.car.structs import car
|
|
|
|
annotation = decode_annotation(b'{"version":1,"width":32,"height":32,"poly_left":[[0,0],[32,0],[32,32],[0,32]],"poly_right":[]}')
|
|
params = Params(str(tmp_path / "params"))
|
|
path = Path(params.get_param_path("VASMPreferences"))
|
|
path.write_bytes(encode(Preferences(True, annotation)))
|
|
clock = [1_000_000_000]
|
|
messages = Messages(clock)
|
|
client = Client()
|
|
client.add(2_000_000_000, 0)
|
|
events = []
|
|
runtime = VASMRuntime(params, messages, lambda: client, events.append, model_path=Path("/external/model.onnx"),
|
|
mono_clock=lambda: clock[0], pair_clock=lambda: (clock[0], clock[0] + 1_000_000_000),
|
|
model_factory=lambda _: Model())
|
|
event = runtime.tick()
|
|
assert event is not None and event.status == "valid" and event.frame_id == 0
|
|
assert event.display_right and not event.display_left
|
|
assert event.left_frame_id == 0 and event.left_eof_boot_ns == 2_000_000_000
|
|
assert event.observed_mono_ns == clock[0] and event.observed_boot_ns == clock[0] + 1_000_000_000
|
|
assert event.sequence == 1
|
|
assert len(event.settings_fingerprint) == 64 and len(event.session) == 32
|
|
sent = []
|
|
publish_observation(State(send=lambda service, message: sent.append((service, message))), event)
|
|
service, envelope = sent[0]
|
|
assert service == "spotMonitorState" and envelope.valid and envelope.logMonoTime == event.observed_mono_ns
|
|
assert envelope.spotMonitorState.desiredCurvature == 0
|
|
wire = envelope.spotMonitorState.observation
|
|
assert wire.version == 1 and wire.sequence == 1 and wire.sourceFrameId == 0
|
|
assert wire.right.status == "warning" and wire.right.sourceFrameEofBootTime == event.left_eof_boot_ns
|
|
assert wire.left.status == "unknown" and wire.left.sourceFrameEofBootTime == 0
|
|
clock[0] += 1_100_000_000
|
|
messages.refresh()
|
|
client.add(clock[0] + 1_000_000_000, 0)
|
|
assert runtime.tick() is None # duplicate frame cannot renew held warning
|
|
assert events[-1].status == "stale" and not events[-1].display_right
|
|
clock[0] += 600_000_000
|
|
messages.refresh()
|
|
messages.values["carState"].gearShifter = car.CarState.GearShifter.park
|
|
assert runtime.tick() is None
|
|
assert events[-1].status == "unavailable"
|
|
assert not events[-1].display_right
|
|
|
|
|
|
def test_alternating_sides_do_not_renew_other_source_and_resume_resets(tmp_path):
|
|
annotation = decode_annotation(b'{"version":1,"width":32,"height":32,"poly_left":[[0,0],[32,0],[32,32],[0,32]],"poly_right":[[0,0],[32,0],[32,32],[0,32]]}')
|
|
params = Params(str(tmp_path / "params"))
|
|
path = Path(params.get_param_path("VASMPreferences"))
|
|
path.write_bytes(encode(Preferences(True, annotation)))
|
|
clock = [1_000_000_000]
|
|
offset = [1_000_000_000]
|
|
messages = Messages(clock)
|
|
client = Client()
|
|
client.add(2_000_000_000, 0)
|
|
events = []
|
|
runtime = VASMRuntime(params, messages, lambda: client, events.append, model_path=Path("/external/model.onnx"),
|
|
mono_clock=lambda: clock[0], pair_clock=lambda: (clock[0], clock[0] + offset[0]),
|
|
model_factory=lambda _: Model())
|
|
first = runtime.tick()
|
|
assert first is not None and first.display_right and not first.display_left
|
|
clock[0] += 350_000_000
|
|
messages.refresh()
|
|
client.add(clock[0] + offset[0], 1)
|
|
second = runtime.tick()
|
|
assert second is not None and second.display_right and second.display_left
|
|
assert second.left_eof_boot_ns == first.left_eof_boot_ns
|
|
assert second.right_eof_boot_ns > second.left_eof_boot_ns
|
|
sent = []
|
|
publish_observation(State(send=lambda service, message: sent.append(message)), second)
|
|
wire = sent[0].spotMonitorState.observation
|
|
assert wire.right.sourceFrameId == 0 and wire.left.sourceFrameId == 1
|
|
assert wire.right.validUntilBootTime == first.left_eof_boot_ns + 3_000_000_000
|
|
|
|
clock[0] += 1_100_000_000
|
|
messages.refresh()
|
|
offset[0] += 9_000_000_000 # suspend/resume clock-pair discontinuity
|
|
client.add(clock[0] + offset[0], 2)
|
|
assert runtime.tick() is None
|
|
assert events[-1].status == "stale" and not events[-1].display_left and not events[-1].display_right
|
|
|
|
|
|
def test_authority_or_saved_doc_change_during_inference_denies_output(tmp_path):
|
|
from opendbc.car.structs import car
|
|
|
|
annotation = decode_annotation(b'{"version":1,"width":32,"height":32,"poly_left":[[0,0],[32,0],[32,32],[0,32]],"poly_right":[]}')
|
|
params = Params(str(tmp_path / "params"))
|
|
path = Path(params.get_param_path("VASMPreferences"))
|
|
path.write_bytes(encode(Preferences(True, annotation)))
|
|
clock = [1_000_000_000]
|
|
messages = Messages(clock)
|
|
client = Client()
|
|
client.add(2_000_000_000, 0)
|
|
events = []
|
|
|
|
class ChangingModel(Model):
|
|
def infer_nv12(self, frame, **kwargs):
|
|
messages.values["carState"].gearShifter = car.CarState.GearShifter.park
|
|
return super().infer_nv12(frame, **kwargs)
|
|
|
|
runtime = VASMRuntime(params, messages, lambda: client, events.append, model_path=Path("/external/model.onnx"),
|
|
mono_clock=lambda: clock[0], pair_clock=lambda: (clock[0], clock[0] + 1_000_000_000),
|
|
model_factory=lambda _: ChangingModel())
|
|
assert runtime.tick() is None
|
|
assert all(event.status != "valid" for event in events)
|
|
|
|
messages.values["carState"].gearShifter = car.CarState.GearShifter.drive
|
|
client.add(2_000_000_100, 1)
|
|
|
|
class SavingModel(Model):
|
|
def infer_nv12(self, frame, **kwargs):
|
|
path.write_bytes(encode(Preferences(False, annotation)))
|
|
return super().infer_nv12(frame, **kwargs)
|
|
|
|
runtime.model = SavingModel()
|
|
assert runtime.tick() is None
|
|
assert all(event.status != "valid" for event in events)
|
|
|
|
|
|
def test_reconnect_rejects_queued_preconnection_frame(tmp_path):
|
|
annotation = decode_annotation(b'{"version":1,"width":32,"height":32,"poly_left":[[0,0],[32,0],[32,32],[0,32]],"poly_right":[]}')
|
|
params = Params(str(tmp_path / "params"))
|
|
path = Path(params.get_param_path("VASMPreferences"))
|
|
path.write_bytes(encode(Preferences(True, annotation)))
|
|
clock = [1_000_000_000]
|
|
messages = Messages(clock)
|
|
client = Client()
|
|
client.add(2_000_000_000, 0)
|
|
events = []
|
|
runtime = VASMRuntime(params, messages, lambda: client, events.append, model_path=Path("/external/model.onnx"),
|
|
mono_clock=lambda: clock[0], pair_clock=lambda: (clock[0], clock[0] + 1_000_000_000),
|
|
model_factory=lambda _: Model())
|
|
assert runtime.tick() is not None
|
|
client.connected = False
|
|
clock[0] += 100_000_000
|
|
messages.refresh()
|
|
client.add(2_050_000_000, 1) # queued from before the new connection
|
|
assert runtime.tick() is None
|
|
clock[0] += 100_000_000
|
|
messages.refresh()
|
|
assert runtime.tick() is None and events[-1].status == "stale"
|
|
assert all(event.sequence <= 1 for event in events)
|
|
|
|
|
|
def test_model_load_failure_retries_only_after_bounded_backoff(tmp_path):
|
|
annotation = decode_annotation(b'{"version":1,"width":32,"height":32,"poly_left":[[0,0],[32,0],[32,32],[0,32]],"poly_right":[]}')
|
|
params = Params(str(tmp_path / "params"))
|
|
path = Path(params.get_param_path("VASMPreferences"))
|
|
path.write_bytes(encode(Preferences(True, annotation)))
|
|
clock = [1_000_000_000]
|
|
messages = Messages(clock)
|
|
attempts = []
|
|
|
|
class BadModel(Model):
|
|
def load(self):
|
|
attempts.append(clock[0])
|
|
return False
|
|
|
|
runtime = VASMRuntime(params, messages, Client, lambda event: None,
|
|
model_path=Path("/external/model.onnx"), mono_clock=lambda: clock[0],
|
|
pair_clock=lambda: (clock[0], clock[0] + 1_000_000_000), model_factory=lambda _: BadModel())
|
|
assert runtime.tick() is None
|
|
clock[0] += 4_000_000_000
|
|
messages.refresh()
|
|
assert runtime.tick() is None
|
|
assert attempts == [1_000_000_000]
|
|
clock[0] += 1_000_000_000
|
|
messages.refresh()
|
|
assert runtime.tick() is None
|
|
assert attempts == [1_000_000_000, 6_000_000_000]
|
|
|
|
|
|
def test_source_timestamp_advancing_during_post_inference_update_is_checked_with_final_clock(tmp_path):
|
|
annotation = decode_annotation(b'{"version":1,"width":32,"height":32,"poly_left":[[0,0],[32,0],[32,32],[0,32]],"poly_right":[]}')
|
|
params = Params(str(tmp_path / "params"))
|
|
path = Path(params.get_param_path("VASMPreferences"))
|
|
path.write_bytes(encode(Preferences(True, annotation)))
|
|
clock = [1_000_000_000]
|
|
|
|
class UpdatingMessages(Messages):
|
|
calls = 0
|
|
|
|
def update(self, timeout):
|
|
super().update(timeout)
|
|
self.calls += 1
|
|
if self.calls == 3: # final update after inference receives a newer state
|
|
clock[0] += 100_000_000
|
|
self.refresh()
|
|
|
|
messages = UpdatingMessages(clock)
|
|
client = Client()
|
|
client.add(2_000_000_000, 0)
|
|
runtime = VASMRuntime(params, messages, lambda: client, lambda event: None,
|
|
model_path=Path("/external/model.onnx"), mono_clock=lambda: clock[0],
|
|
pair_clock=lambda: (clock[0], clock[0] + 1_000_000_000), model_factory=lambda _: Model())
|
|
event = runtime.tick()
|
|
assert event is not None and event.status == "valid"
|
|
assert event.observed_mono_ns == 1_100_000_000 and event.observed_boot_ns == 2_100_000_000
|