IQ.Pilot Release Commit @ d23cc80

This commit is contained in:
IQ.Lvbs CI [bot]
2026-07-27 18:01:16 -05:00
parent 46277b19ec
commit a70938008a
74 changed files with 715 additions and 1296 deletions

View File

@@ -214,10 +214,9 @@ class NoEntryCard(AlertCard):
def __init__(self,
alert_text_2: str,
alert_text_1: str = "openpilot Unavailable",
visual_alert: car.CarControl.HUDControl.VisualAlert = VisualAlert.none,
priority: Tier = Tier.LOW):
visual_alert: car.CarControl.HUDControl.VisualAlert = VisualAlert.none):
primary, secondary, size = _mici_reframe(alert_text_1, alert_text_2)
super().__init__(primary, secondary, AlertStatus.normal, size, priority, visual_alert, AudibleAlert.refuse, 3.0)
super().__init__(primary, secondary, AlertStatus.normal, size, Tier.LOW, visual_alert, AudibleAlert.refuse, 3.0)
class GentleDisableCard(AlertCard):

View File

@@ -12,11 +12,11 @@ _ANGLE = _dbc.CarParams.SteerControlType.angle
# Port tunables surfaced to the fingerprint step, flat so the read is one pass.
_TUNABLES = (
"HyundaiLongitudinalTuning",
"SubaruStopAndGo",
"SubaruStopAndGoManualParkingBrake",
"TeslaCoopSteering",
"ToyotaEnforceStockLongitudinal",
"IQHyundaiLongTune",
"IQSubaruCreepAssist",
"IQSubaruCreepAssistManualBrake",
"IQTeslaTorqueBlend",
"IQToyotaFactoryLong",
"ToyotaSnGHack",
)

View File

@@ -81,5 +81,5 @@ def _write(vehicles: dict[str, dict], basedir: str = BASEDIR) -> str:
if __name__ == "__main__":
from iqdbc.lvbs.car.platform_list import get_car_list
print("wrote", _write(get_car_list()))
from iqdbc.lvbs.car.car_catalog import build_car_catalog
print("wrote", _write(build_car_catalog()))

View File

@@ -43,6 +43,7 @@ from openpilot.iqpilot.selfdrive.iqmodeld.models.helpers import (
bundle_files_ready,
get_active_bundle,
get_runtime_bundle_upgrade,
is_default_bundle,
persist_active_bundle,
)
@@ -56,6 +57,7 @@ class IQModelManager(_BaseIQModelManager):
def __init__(self):
super().__init__()
self._validated_active_key: tuple[tuple[str, str], ...] | None = None
self._manifest_refresh_key: tuple[tuple[str, str], ...] | None = None
@staticmethod
def _bundle_index(bundle) -> int | None:
@@ -190,6 +192,58 @@ class IQModelManager(_BaseIQModelManager):
if bundle_index is not None and self._download_index() is None:
self.params.put(_DOWNLOAD_INDEX_KEY, bundle_index)
def _find_manifest_counterpart(self, target):
# never match by index: indexes shift between manifest generations, and a
# positional match could redownload a different model than the user selected
for attr in ("ref", "internalName", "displayName"):
value = getattr(target, attr, None)
if not value:
continue
for bundle in self.available_models:
if getattr(bundle, attr, None) == value:
return bundle
return None
def _queue_active_manifest_refresh(self) -> None:
active = self.active_bundle
if active is None or is_default_bundle(active):
return
if self._download_index() is not None:
return
counterpart = self._find_manifest_counterpart(active)
if counterpart is None:
return
counterpart_index = self._bundle_index(counterpart)
if counterpart_index is None:
return
active_files = dict(self._bundle_files(active))
stale = False
for filename, sha in self._bundle_files(counterpart):
if not sha:
continue
active_sha = active_files.get(filename)
# an empty recorded hash can't prove a mismatch, so it never triggers a redownload
if active_sha is None or (active_sha and active_sha.lower() != sha.lower()):
stale = True
break
if not stale:
self._manifest_refresh_key = None
return
# the manifest may be an expired offline cache, so keep the active bundle and its
# files in place: the download flow replaces artifacts atomically and only persists
# the counterpart as active once everything landed. One attempt per bundle per run
# so a dead network doesn't turn the 1Hz loop into a download-retry storm.
key = self._bundle_validation_key(active)
if key == self._manifest_refresh_key:
return
self._manifest_refresh_key = key
cloudlog.warning(f"Active model {_display_bundle_name(active)} artifacts are stale vs current manifest; queueing redownload")
self.params.put(_DOWNLOAD_INDEX_KEY, counterpart_index)
async def _download_file(self, url: str, path: str, model) -> None:
temp_path = f"{path}.download"
self._download_start_times[model.fileName] = time.monotonic()
@@ -302,6 +356,7 @@ class IQModelManager(_BaseIQModelManager):
self.active_bundle = get_active_bundle(self.params)
self._queue_active_redownload_if_invalid()
self._queue_tinygrad_upgrade()
self._queue_active_manifest_refresh()
if (index_to_download := self._download_index()) is not None:
if model_to_download := next((model for model in self.available_models if model.index == index_to_download), None):

View File

@@ -0,0 +1,150 @@
"""
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from dataclasses import dataclass, field
from openpilot.iqpilot.selfdrive.iqmodeld.models.manager import IQModelManager, _DOWNLOAD_INDEX_KEY
@dataclass
class _DownloadUri:
sha256: str = ""
uri: str = ""
@dataclass
class _Artifact:
fileName: str = ""
downloadUri: _DownloadUri = field(default_factory=_DownloadUri)
@dataclass
class _Model:
artifact: _Artifact = field(default_factory=_Artifact)
metadata: _Artifact | None = None
@dataclass
class _Bundle:
index: int = 0
ref: str = ""
internalName: str = ""
displayName: str = ""
models: list = field(default_factory=list)
class _FakeParams:
def __init__(self):
self.store = {}
def get(self, key):
return self.store.get(key)
def put(self, key, value):
self.store[key] = value
def remove(self, key):
self.store.pop(key, None)
def _bundle(index, name, sha, filename="driving_vision_test_tinygrad.pkl"):
return _Bundle(
index=index,
ref=f"ref-{name}",
internalName=name,
displayName=f"{name} display",
models=[_Model(artifact=_Artifact(fileName=filename, downloadUri=_DownloadUri(sha256=sha)))],
)
def _manager(active, available):
mgr = IQModelManager.__new__(IQModelManager)
mgr.params = _FakeParams()
mgr.active_bundle = active
mgr.available_models = available
mgr._validated_active_key = None
mgr._manifest_refresh_key = None
return mgr
def test_stale_active_bundle_queues_redownload_at_current_index():
active = _bundle(55, "WMIV12", "a" * 64)
counterpart = _bundle(12, "WMIV12", "b" * 64)
mgr = _manager(active, [counterpart])
mgr._queue_active_manifest_refresh()
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) == 12
assert mgr.active_bundle is active
def test_matching_shas_do_not_queue():
active = _bundle(55, "WMIV12", "a" * 64)
counterpart = _bundle(12, "WMIV12", "A" * 64)
mgr = _manager(active, [counterpart])
mgr._queue_active_manifest_refresh()
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) is None
def test_retired_bundle_is_left_alone():
active = _bundle(55, "WMIV12", "a" * 64)
mgr = _manager(active, [_bundle(12, "OtherModel", "b" * 64)])
mgr._queue_active_manifest_refresh()
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) is None
assert mgr.active_bundle is active
def test_default_bundle_is_never_refreshed():
active = _bundle(0, "Default", "a" * 64)
active.ref = "default"
mgr = _manager(active, [_bundle(0, "Default", "b" * 64)])
mgr._queue_active_manifest_refresh()
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) is None
def test_pending_download_blocks_refresh():
active = _bundle(55, "WMIV12", "a" * 64)
mgr = _manager(active, [_bundle(12, "WMIV12", "b" * 64)])
mgr.params.put(_DOWNLOAD_INDEX_KEY, 3)
mgr._queue_active_manifest_refresh()
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) == 3
def test_empty_manifest_hash_never_triggers():
active = _bundle(55, "WMIV12", "a" * 64)
mgr = _manager(active, [_bundle(12, "WMIV12", "")])
mgr._queue_active_manifest_refresh()
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) is None
def test_refresh_queued_once_per_run():
active = _bundle(55, "WMIV12", "a" * 64)
mgr = _manager(active, [_bundle(12, "WMIV12", "b" * 64)])
mgr._queue_active_manifest_refresh()
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) == 12
mgr.params.remove(_DOWNLOAD_INDEX_KEY)
mgr._queue_active_manifest_refresh()
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) is None
def test_counterpart_matched_by_name_not_index():
active = _bundle(55, "WMIV12", "a" * 64)
imposter = _bundle(55, "OtherModel", "c" * 64)
counterpart = _bundle(12, "WMIV12", "b" * 64)
mgr = _manager(active, [imposter, counterpart])
mgr._queue_active_manifest_refresh()
assert mgr.params.get(_DOWNLOAD_INDEX_KEY) == 12

View File

@@ -337,6 +337,13 @@ _NOTICE_EVENTS: EVENTS_IQ_TYPE = {
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.),
},
# outranks the generic processNotRunning alert so the driver sees why engagement is blocked
EventNameIQ.modelUpdating: {
ET.NO_ENTRY: NoEntryAlert("Update finishes while parked with internet",
alert_text_1="Driving Model Updating",
priority=Priority.MID),
},
}
EVENTS_IQ: EVENTS_IQ_TYPE = {**_GUIDANCE_EVENTS, **_ENGAGE_EVENTS, **_CABIN_BLOCK_EVENTS, **_NOTICE_EVENTS}

View File

@@ -194,7 +194,7 @@ CHEVRON_INFO_DESCRIPTION = {
# param key -> (title fn, description fn)
_HUD_TOGGLES = {
"BlindSpot": (
"IQBlindSpotAlerts": (
lambda: tr("Blind Spot Alerts"),
lambda: tr("Flashes a side warning whenever the car reports something sitting in your blind spot (BSM-equipped cars only)."),
),
@@ -202,20 +202,20 @@ _HUD_TOGGLES = {
lambda: tr("Expanded Status Bar"),
lambda: tr("Bring back the classic UI's wide offroad status strip: temperature, vehicle, and Konn3kt state at a glance."),
),
"TorqueBar": (
"IQSteerEffortArc": (
lambda: tr("Steering Effort Arc"),
lambda: tr("Trace an arc over the road view showing how much steering IQ.Pilot is applying while lateral control runs."),
),
"RoadNameToggle": (
"IQRoadNameOverlay": (
lambda: tr("Road Name Overlay"),
lambda: tr("Show the current road's name over the driving view."
"<br>Requires offline map data for your region to be installed."),
),
"ShowTurnSignals": (
"IQBlinkerIndicators": (
lambda: tr("Blinker Indicators"),
lambda: tr("Mirror the car's blinkers as arrows on the driving screen."),
),
"RocketFuel": (
"IQAccelMeter": (
lambda: tr("Acceleration Meter"),
lambda: tr("Draw a bar along the left edge tracking measured acceleration and braking — what the car is actually "
"doing right now, not the planner's request."),
@@ -243,7 +243,7 @@ class VisualsLayout(Widget):
title=lambda: tr("Lead Vehicle Readouts"),
description="",
buttons=[lambda: tr("Off"), lambda: tr("Distance"), lambda: tr("Speed"), lambda: tr("Time"), lambda: tr("All")],
param="ChevronInfo",
param="IQLeadReadouts",
inline=False,
)
self._dev_ui_info = toggle_item(
@@ -270,12 +270,12 @@ class VisualsLayout(Widget):
def _sync_chevron_row(self):
if ui_state.has_longitudinal_control:
self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["enabled"]))
self._chevron_info.action_item.set_selected_button(ui_state.params.get("ChevronInfo", return_default=True))
self._chevron_info.action_item.set_selected_button(ui_state.params.get("IQLeadReadouts", return_default=True))
self._chevron_info.action_item.set_enabled(True)
else:
self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["disabled"]))
self._chevron_info.action_item.set_enabled(False)
ui_state.params.put("ChevronInfo", 0)
ui_state.params.put("IQLeadReadouts", 0)
def _update_state(self):
super()._update_state()
@@ -1388,7 +1388,7 @@ class IQDeviceLayout(DeviceLayout):
DeviceLayout._initialize_items(self)
# Using dual button with no right button for better alignment
self._always_offroad_btn = self._left_button(lambda: tr("Force Offroad Mode"), self._handle_always_offroad)
self._always_offroad_btn = self._left_button(lambda: tr("Keep Device Offroad"), self._handle_always_offroad)
self._force_onroad_btn = self._left_button(lambda: tr("Force On-Road (10 min)"), self._handle_force_onroad)
self._max_time_offroad = option_item(
@@ -1592,7 +1592,7 @@ class IQDeviceLayout(DeviceLayout):
force_onroad_active = force_onroad_until > now
# Text & Color
offroad_mode_btn_text = tr("Exit Offroad Mode") if always_offroad else tr("Force Offroad Mode")
offroad_mode_btn_text = tr("Exit Offroad Mode") if always_offroad else tr("Keep Device Offroad")
offroad_mode_btn_style = ButtonStyle.PRIMARY if always_offroad else ButtonStyle.DANGER
self._always_offroad_btn.action_item.left_button.set_text(offroad_mode_btn_text)
self._always_offroad_btn.action_item.left_button.set_button_style(offroad_mode_btn_style)
@@ -1957,12 +1957,12 @@ class ModelsLayout(Widget):
return folders_list
def _handle_current_model_clicked(self):
favs = ui_state.params.get("ModelManager_Favs")
favs = ui_state.params.get("IQModelFavorites")
favorites = set(favs.split(';')) if favs else set()
folders_list = self._get_folders(favorites)
active_ref = self.model_manager.activeBundle.ref if self._has_active_bundle_param() and self.model_manager.activeBundle else "Default"
self.model_dialog = PickerDialog(tr("Choose a Model"), folders_list, active_ref, "ModelManager_Favs",
self.model_dialog = PickerDialog(tr("Choose a Model"), folders_list, active_ref, "IQModelFavorites",
get_folders_fn=self._get_folders, on_exit=self._on_model_selected)
gui_app.set_modal_overlay(self.model_dialog, callback=self._on_model_selected)
@@ -2191,8 +2191,8 @@ class HyundaiSettings(BrandPanel):
self.longitudinal_tuning_item = multiple_button_item(
tr("Longitudinal Tune Profile"), "",
[tr("Off"), tr("Dynamic"), tr("Predictive")],
button_width=300, param="HyundaiLongitudinalTuning", inline=False,
callback=lambda index: ui_state.params.put("HyundaiLongitudinalTuning", index))
button_width=300, param="IQHyundaiLongTune", inline=False,
callback=lambda index: ui_state.params.put("IQHyundaiLongTune", index))
self.items = [self.longitudinal_tuning_item]
def _alpha_long_supported(self) -> bool:
@@ -2204,7 +2204,7 @@ class HyundaiSettings(BrandPanel):
def update_settings(self):
self.alpha_long_available = self._alpha_long_supported()
selected = int(ui_state.params.get("HyundaiLongitudinalTuning") or "0")
selected = int(ui_state.params.get("IQHyundaiLongTune") or "0")
if not ui_state.is_offroad():
desc, usable = tr("Unavailable while the car is onroad."), False
@@ -2231,11 +2231,11 @@ class SubaruSettings(BrandPanel):
def __init__(self):
super().__init__()
self._supported = False
self.stop_and_go_toggle = toggle_item(tr("Creep from Standstill (Beta)"), "", param="SubaruStopAndGo",
self.stop_and_go_toggle = toggle_item(tr("Creep from Standstill (Beta)"), "", param="IQSubaruCreepAssist",
callback=lambda _: self.update_settings())
self.stop_and_go_manual_parking_brake_toggle = toggle_item(
tr("Creep from Standstill — Manual Handbrake (Beta)"), "",
param="SubaruStopAndGoManualParkingBrake", callback=lambda _: self.update_settings())
param="IQSubaruCreepAssistManualBrake", callback=lambda _: self.update_settings())
self.items = [self.stop_and_go_toggle, self.stop_and_go_manual_parking_brake_toggle]
def _platform_flags(self) -> int:
@@ -2286,8 +2286,8 @@ def _speed_text(kmh: int) -> str:
class TeslaSettings(BrandPanel):
def __init__(self):
super().__init__()
self.coop_steering_toggle = toggle_item(tr("VTB (Virtual Torque Blending)"), "", param="TeslaCoopSteering")
self.items = [self.coop_steering_toggle]
self.torque_blend_toggle = toggle_item(tr("VTB (Virtual Torque Blending)"), "", param="IQTeslaTorqueBlend")
self.items = [self.torque_blend_toggle]
def update_settings(self):
caution = tr("Warning: steering may oscillate in turns below {}; turn this off if you feel it.").format(
@@ -2300,8 +2300,8 @@ class TeslaSettings(BrandPanel):
blocker = tr("Flip on Always Offroad from the Device panel, or power the car down, to change this.")
body = f"<b>{blocker}</b><br><br>{body}"
self.coop_steering_toggle.set_description(body)
self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad())
self.torque_blend_toggle.set_description(body)
self.torque_blend_toggle.action_item.set_enabled(ui_state.is_offroad())
# ===== vehicle_brands_toyota =====
@@ -2312,7 +2312,7 @@ class ToyotaSettings(BrandPanel):
self.enforce_stock_longitudinal = toggle_item(
lambda: tr("Keep Factory Gas and Brake"),
description=lambda: tr("Keeps gas and brakes with the factory Toyota system; IQ.Pilot steers only."),
initial_state=ui_state.params.get_bool("ToyotaEnforceStockLongitudinal"),
initial_state=ui_state.params.get_bool("IQToyotaFactoryLong"),
callback=self._on_toggled,
enabled=lambda: not ui_state.engaged,
)
@@ -2320,7 +2320,7 @@ class ToyotaSettings(BrandPanel):
@staticmethod
def _apply(enabled: bool):
ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", enabled)
ui_state.params.put_bool("IQToyotaFactoryLong", enabled)
if enabled and ui_state.params.get_bool("AlphaLongitudinalEnabled"):
ui_state.params.put_bool("AlphaLongitudinalEnabled", False)
ui_state.params.put_bool("OnroadCycleRequested", True)

View File

@@ -1,10 +0,0 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from openpilot.iqpilot.ui.onroad.hud_overlays import ChevronMetrics
from openpilot.iqpilot.ui.onroad.rainbow_path import RainbowPath
class IQModelRenderer:
def __init__(self):
self.rainbow_path = RainbowPath()

View File

@@ -1,41 +0,0 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
import colorsys
import time
import pyray as rl
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
# Scrolling spectrum along the driving path: a fixed set of stops from the
# bottom (1.0) to the top (0.0) of the path, each a full-saturation swatch whose
# hue advances with time and whose opacity thins toward the horizon.
_SEGMENTS = 8
_SCROLL_DEG_PER_S = 50.0
_SATURATION = 0.9
_LIGHTNESS = 0.6
_ALPHA_NEAR = 0.8 # opacity at the bottom of the path
_ALPHA_HORIZON_FRACTION = 0.3 # how much of that opacity is shed by the top
# Stop offsets are constant, so resolve them once.
_STOP_OFFSETS = tuple(i / (_SEGMENTS - 1) for i in range(_SEGMENTS))
def _swatch(hue_turns: float, alpha: float) -> rl.Color:
r, g, b = colorsys.hls_to_rgb(hue_turns, _LIGHTNESS, _SATURATION)
return rl.Color(int(r * 255), int(g * 255), int(b * 255), int(alpha * 255))
def _spectrum_gradient() -> Gradient:
scroll_deg = (time.monotonic() * _SCROLL_DEG_PER_S) % 360.0
colors = []
for offset in _STOP_OFFSETS:
hue_deg = (scroll_deg + offset * 360.0) % 360.0
alpha = _ALPHA_NEAR * (1.0 - offset * _ALPHA_HORIZON_FRACTION)
colors.append(_swatch(hue_deg / 360.0, alpha))
return Gradient(start=(0.0, 1.0), end=(0.0, 0.0), colors=colors, stops=list(_STOP_OFFSETS))
class RainbowPath:
def draw_rainbow_path(self, rect, path):
draw_polygon(rect, path.projected_points, gradient=_spectrum_gradient())