From e62a51821dce9fd9635e368fba8ff650dfac5eb9 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:09:27 -0500 Subject: [PATCH] Auto Print Population --- openpilot/starpilot/galaxy/evidence.py | 2 +- openpilot/starpilot/galaxy/settings.py | 27 +++- .../tests/test_auto_vehicle_population.py | 123 ++++++++++++++++++ .../galaxy/tests/test_evidence_lifecycle.py | 2 +- 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 openpilot/starpilot/galaxy/tests/test_auto_vehicle_population.py diff --git a/openpilot/starpilot/galaxy/evidence.py b/openpilot/starpilot/galaxy/evidence.py index 5f2bc9ba93..c843c774f0 100644 --- a/openpilot/starpilot/galaxy/evidence.py +++ b/openpilot/starpilot/galaxy/evidence.py @@ -18,7 +18,7 @@ class EvidenceSnapshot(SimpleNamespace): class EvidenceSource: - SERVICES = ('deviceState', 'pandaStates', 'carState', 'selfdriveState') + SERVICES = ('deviceState', 'pandaStates', 'carState', 'selfdriveState', 'carParams') def __init__(self, messages=None, *, mono=time.monotonic_ns, boot=lambda: time.clock_gettime_ns(getattr(time, 'CLOCK_BOOTTIME', time.CLOCK_MONOTONIC))): diff --git a/openpilot/starpilot/galaxy/settings.py b/openpilot/starpilot/galaxy/settings.py index 70f94a61a0..267c35cfba 100644 --- a/openpilot/starpilot/galaxy/settings.py +++ b/openpilot/starpilot/galaxy/settings.py @@ -133,6 +133,7 @@ class LiveContextSource: """Samples registered Params and the two actual publisher clock domains.""" MAX_CP_BYTES = 1_048_576 + MAX_LIVE_CP_AGE_NS = 100_000_000_000 # Card republishes finalized parameters every 50 seconds. def __init__(self, params, messages=None, *, mono_clock=time.monotonic_ns, boot_clock=boot_time_ns, evidence_wait_ms=750, wait_clock=time.monotonic, borrowed_messages=False): @@ -159,7 +160,7 @@ class LiveContextSource: return None if self.messages is None: from openpilot.cereal import messaging - self.messages = messaging.SubMaster(["deviceState", "pandaStates", "carState", "selfdriveState"]) + self.messages = messaging.SubMaster(["deviceState", "pandaStates", "carState", "selfdriveState", "carParams"]) return cast(Callable[[], object], self.messages.snapshot)() if hasattr(self.messages, "snapshot") else self.messages def close(self) -> None: @@ -280,6 +281,30 @@ class LiveContextSource: return False def runtime_vehicle(self): + try: + with self.lock: + if self.closed: + return None, None + sm = self._messages() if self.messages is not None else None + if sm is not None: + if not self.borrowed_messages and not hasattr(self.messages, 'snapshot'): + sm.update(0) + now = self.mono_clock() + stamp = int(sm.logMonoTime.get('carParams', 0)) + floor = max(self.device_after_mono_ns, getattr(sm, 'after_mono_ns', 0)) + if (sm.seen.get('carParams', False) and sm.valid.get('carParams', False) and sm.alive.get('carParams', False) and + floor < stamp <= now and now - stamp <= self.MAX_LIVE_CP_AGE_NS): + from opendbc.car.values import PLATFORMS + from opendbc.car.mock.values import CAR as MOCK + from opendbc.car.structs import car + cp = sm['carParams'] + if (cp.schema.node.id == car.CarParams.schema.node.id and not cp.notCar and + cp.carFingerprint in PLATFORMS and cp.carFingerprint != MOCK.MOCK): + raw = cp.as_builder().to_bytes() + if len(raw) <= self.MAX_CP_BYTES: + return cp, b'live-vehicle:' + raw + except (OSError, RuntimeError, AttributeError, KeyError, TypeError, ValueError, OverflowError): + pass cp_raw, readable = read_saved(self.params, "CarParamsPersistent", self.MAX_CP_BYTES) cp = None if readable and cp_raw is not None: diff --git a/openpilot/starpilot/galaxy/tests/test_auto_vehicle_population.py b/openpilot/starpilot/galaxy/tests/test_auto_vehicle_population.py new file mode 100644 index 0000000000..5ed38e669b --- /dev/null +++ b/openpilot/starpilot/galaxy/tests/test_auto_vehicle_population.py @@ -0,0 +1,123 @@ +"""Detected vehicle telemetry populates Galaxy without changing Auto selection.""" +import json +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest +from opendbc.car import gen_empty_fingerprint +from opendbc.car.car_helpers import interfaces +from openpilot.cereal import messaging +from openpilot.common.params import Params +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.starpilot.galaxy.settings import LiveContextSource, SettingsChanged, SettingsGateway +from openpilot.starpilot.schema_cache import put_cache +from openpilot.starpilot.vehicle_selection import encode + + +@pytest.fixture +def detected(): + with OpenpilotPrefix(), tempfile.TemporaryDirectory() as directory: + params = Params(directory) + params.put_bool('OpenpilotEnabledToggle', True, block=True) + now = [100_000_000_000] + sm = messaging.SubMaster(['deviceState', 'pandaStates', 'carState', 'selfdriveState', 'carParams']) + source = LiveContextSource(params, messages=sm, borrowed_messages=True, mono_clock=lambda: now[0]) + gateway = SettingsGateway(params, source) + with patch.object(source, 'offroad', return_value=True): + yield params, now, sm, source, gateway + gateway.close() + + +def publish(detected, identity='HYUNDAI_IONIQ_6', *, valid=True, stamp=None): + _, now, sm, _, _ = detected + cp = interfaces[identity].get_params(identity, gen_empty_fingerprint(), [], False, False, False) + cp.fingerprintSource = 'can' + now[0] += 1_000_000_000 + event = messaging.new_message('carParams', valid=valid, logMonoTime=now[0] if stamp is None else stamp) + event.carParams = cp + sm.update_msgs(now[0] / 1e9, [event.as_reader()]) + return cp + + +@pytest.mark.parametrize('identity', ['HYUNDAI_IONIQ_6', 'CHEVROLET_VOLT_ASCM', 'TOYOTA_COROLLA_TSS2']) +def test_actual_gateway_auto_exposes_learning_and_same_vehicle_fields_as_manual(detected, identity): + params, _, _, source, gateway = detected + assert source.sample().cp is None + cp = publish(detected, identity) + before = cp.to_dict() + pages = {name: gateway.page(name, 'session', b'generation') for name in ('torque', 'vehicle')} + learning = next(row for row in pages['torque']['rows'] if row['label'] == 'Use StarPilot Auto Learn') + assert learning['available'] + params.put('VehicleSelection', json.loads(encode(identity)), block=True) + for name, automatic in pages.items(): + manual = gateway.page(name, 'session', b'generation') + assert [(r['label'], r['available'], r['value']) for r in automatic['rows']] == [ + (r['label'], r['available'], r['value']) for r in manual['rows']] + assert cp.to_dict() == before + for key in ('CarParams', 'CarParamsPersistent', 'CarParamsCache'): + assert not Path(params.get_param_path(key)).exists() + + +def test_auto_learning_can_be_saved_and_republication_does_not_reset_context(detected): + params, _, _, source, gateway = detected + publish(detected) + token = source.sample().cp_raw + page = gateway.page('torque', 'session', b'generation') + index = next(i for i, row in enumerate(page['rows']) if row['label'] == 'Use StarPilot Auto Learn') + intent = gateway.preview(page['view'], index, 0, 'session', b'generation', value='Off') + publish(detected) + assert source.sample().cp_raw == token + assert gateway.confirm(intent['intent'], 'session', b'generation') + saved = gateway.page('torque', 'session', b'generation') + assert next(row for row in saved['rows'] if row['label'] == 'Use StarPilot Auto Learn')['value'] == 'Off' + assert params.get('VehicleSelection') is None + + +def test_new_detected_vehicle_invalidates_previous_vehicle_edit(detected): + _, _, _, _, gateway = detected + publish(detected) + page = gateway.page('torque', 'session', b'generation') + index = next(i for i, row in enumerate(page['rows']) if row['label'] == 'Use StarPilot Auto Learn') + intent = gateway.preview(page['view'], index, 0, 'session', b'generation', value='Off') + publish(detected, 'TOYOTA_COROLLA_TSS2') + with pytest.raises(SettingsChanged): + gateway.confirm(intent['intent'], 'session', b'generation') + + +@pytest.mark.parametrize('variation', ['invalid', 'future', 'old', 'stale', 'not_seen', 'not_alive', 'oversized', 'reset']) +def test_unusable_live_vehicle_falls_back_to_verified_cache(detected, variation): + params, now, sm, source, _ = detected + cached = interfaces['TOYOTA_COROLLA_TSS2'].get_params('TOYOTA_COROLLA_TSS2', gen_empty_fingerprint(), [], False, False, False) + put_cache(params, 'CarParamsPersistent', cached, block=True) + before = Path(params.get_param_path('CarParamsPersistent')).read_bytes() + publish(detected) + if variation == 'invalid': + sm.valid['carParams'] = False + elif variation == 'future': + sm.logMonoTime['carParams'] = now[0] + 1 + elif variation == 'old': + sm.logMonoTime['carParams'] = source.device_after_mono_ns + elif variation == 'stale': + now[0] += source.MAX_LIVE_CP_AGE_NS + 1 + elif variation == 'not_seen': + sm.seen['carParams'] = False + elif variation == 'not_alive': + sm.alive['carParams'] = False + elif variation == 'oversized': + source.MAX_CP_BYTES = 1 + assert source.runtime_vehicle()[0] is None + return + elif variation == 'reset': + sm.after_mono_ns = now[0] + cp, _ = source.runtime_vehicle() + assert cp.carFingerprint == cached.carFingerprint + assert Path(params.get_param_path('CarParamsPersistent')).read_bytes() == before + + +def test_explicit_manual_choice_remains_an_offroad_override(detected): + params, _, _, source, _ = detected + publish(detected) + params.put('VehicleSelection', json.loads(encode('TOYOTA_COROLLA_TSS2')), block=True) + assert source.runtime_vehicle()[0].carFingerprint == 'HYUNDAI_IONIQ_6' + assert source.sample().cp.carFingerprint == 'TOYOTA_COROLLA_TSS2' diff --git a/openpilot/starpilot/galaxy/tests/test_evidence_lifecycle.py b/openpilot/starpilot/galaxy/tests/test_evidence_lifecycle.py index cb5e1d4ea8..1ace83a13f 100644 --- a/openpilot/starpilot/galaxy/tests/test_evidence_lifecycle.py +++ b/openpilot/starpilot/galaxy/tests/test_evidence_lifecycle.py @@ -27,7 +27,7 @@ class Messages: self.data = {'deviceState': NS(started=started), 'pandaStates': [NS(pandaType='uno', ignitionLine=ignition, ignitionCan=False, controlsAllowed=False, safetyModel='noOutput')], 'carState': NS(canValid=True, canTimeout=False, standstill=True, gearShifter='park'), - 'selfdriveState': NS(enabled=False, active=False)} + 'selfdriveState': NS(enabled=False, active=False), 'carParams': NS(notCar=True)} self.seen = self.alive = self.valid = dict.fromkeys(EvidenceSource.SERVICES, True) self.updated = dict(self.seen) self.logMonoTime = {name: boot if name == 'pandaStates' else mono for name in EvidenceSource.SERVICES}