IQ.Pilot Release Commit @ 0798119

This commit is contained in:
IQ.Lvbs history cleanup
2026-08-22 23:42:42 -05:00
commit b42569dbca
4529 changed files with 1132125 additions and 0 deletions

View File

View File

@@ -0,0 +1,67 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from __future__ import annotations
from openpilot.common.constants import CV
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
from iqdbc.car import structs
ENHANCED_STOCK_LONGITUDINAL_CONTROL_SET_SPEED_KPH_KEY = "enhancedStockLongitudinalControl.setSpeedKph"
def _float_param(key: str, value: float) -> dict[str, object]:
return {"key": key, "type": "float", "value": f"{float(value):.3f}".encode("utf-8")}
def _clamp_set_speed_kph(value: float) -> float:
return max(0.0, min(V_CRUISE_MAX, float(value)))
def build_iq_control_params_from_plan(CP: structs.CarParams, iq_plan, selfdrive_enabled: bool,
current_set_speed_kph: float, previous_sync_limit_kph: float | None,
pending_sync_limit_kph: float | None) -> tuple[list[dict[str, object]], float | None, float | None]:
if not CP.openpilotLongitudinalControl or not selfdrive_enabled:
return [], None, None
resolver = getattr(getattr(iq_plan, "speedLimit", None), "resolver", None)
assist = getattr(getattr(iq_plan, "speedLimit", None), "assist", None)
if resolver is None or assist is None:
return [], None, None
speed_limit_final_last = float(getattr(resolver, "speedLimitFinalLast", 0.0) or 0.0)
assist_enabled = bool(getattr(assist, "enabled", False))
if not assist_enabled or speed_limit_final_last <= 0.0:
return [], None, None
resolved_limit_kph = _clamp_set_speed_kph(speed_limit_final_last * CV.MS_TO_KPH)
limit_changed = previous_sync_limit_kph is None or abs(resolved_limit_kph - previous_sync_limit_kph) > 0.05
if limit_changed:
pending_sync_limit_kph = resolved_limit_kph
if pending_sync_limit_kph is not None:
if abs(current_set_speed_kph - pending_sync_limit_kph) <= 0.25:
pending_sync_limit_kph = None
set_speed_kph = _clamp_set_speed_kph(current_set_speed_kph or resolved_limit_kph)
else:
set_speed_kph = pending_sync_limit_kph
else:
set_speed_kph = _clamp_set_speed_kph(current_set_speed_kph or resolved_limit_kph)
return [_float_param(ENHANCED_STOCK_LONGITUDINAL_CONTROL_SET_SPEED_KPH_KEY, set_speed_kph)], resolved_limit_kph, pending_sync_limit_kph
def get_set_speed_kph_from_params(params) -> float | None:
for param in params:
key = param.key if hasattr(param, "key") else param.get("key")
if key != ENHANCED_STOCK_LONGITUDINAL_CONTROL_SET_SPEED_KPH_KEY:
continue
raw_value = param.value if hasattr(param, "value") else param.get("value")
try:
raw = raw_value.decode("utf-8") if isinstance(raw_value, (bytes, bytearray)) else str(raw_value)
value = float(raw)
except (AttributeError, TypeError, ValueError):
return None
return _clamp_set_speed_kph(value)
return None

View File

@@ -0,0 +1,52 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
Maps the distance/gap steering-wheel button to an IQ.Pilot action: holding it for
long enough toggles Experimental mode exactly once per hold. Only active when
IQ.Pilot owns longitudinal control and cruise is available.
"""
from cereal import car, custom
from iqdbc.car import structs
from openpilot.common.params import Params
_Button = car.CarState.ButtonEvent.Type
_IQEvent = custom.IQOnroadEvent.EventName
_GAP_BUTTON = _Button.gapAdjustCruise
HOLD_FRAMES_TO_TOGGLE = 50
class GapButtonActions:
def __init__(self, CP: structs.CarParams):
self._CP = CP
self._params = Params()
self._gap_hold_frames = 0
self._already_toggled = False
# read (and cleared) by the personality-decrement handler in selfdrived so a
# release that ends a toggle-hold does not also decrement personality
self.experimental_mode_switched = False
def update(self, CS, events, experimental_mode) -> None:
if not (self._CP.openpilotLongitudinalControl and CS.cruiseState.available):
return
self._advance_hold(CS)
self._toggle_experimental_on_long_hold(events, experimental_mode)
def _advance_hold(self, CS) -> None:
# once counting, keep incrementing each frame the hold persists
if self._gap_hold_frames > 0:
self._gap_hold_frames += 1
# a fresh press seeds the counter; a release zeroes it
for be in CS.buttonEvents:
if be.type.raw == _GAP_BUTTON:
self._gap_hold_frames = int(be.pressed)
if not be.pressed:
self._already_toggled = False
def _toggle_experimental_on_long_hold(self, events, experimental_mode) -> None:
if self._already_toggled or self._gap_hold_frames < HOLD_FRAMES_TO_TOGGLE:
return
self._params.put_bool_nonblocking("ExperimentalMode", not experimental_mode)
events.add(_IQEvent.experimentalToggled)
self._already_toggled = True
self.experimental_mode_switched = True

View File

@@ -0,0 +1,76 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from iqdbc.car import structs as _dbc
from openpilot.common.params import Params as _Store
from openpilot.common.swaglog import cloudlog as _log
from openpilot.selfdrive.controls.lib.latcontrol_torque import get_nn_model_path as _resolve_nn
import openpilot.system.sentry as _telemetry
_ANGLE = _dbc.CarParams.SteerControlType.angle
# Port tunables surfaced to the fingerprint step, flat so the read is one pass.
_TUNABLES = (
"IQHyundaiLongTune",
"IQSubaruCreepAssist",
"IQSubaruCreepAssistManualBrake",
"IQTeslaTorqueBlend",
"IQToyotaFactoryLong",
"ToyotaSnGHack",
)
def initialize_params(store):
return [{name: store.get(name, return_default=True)} for name in _TUNABLES]
def log_fingerprint(cp) -> None:
ident = cp.carFingerprint
if ident == "MOCK":
_telemetry.capture_fingerprint_mock()
else:
_telemetry.capture_fingerprint(ident, cp.brand)
def set_speed_limit_controller_availability(cp, cp_iq, store=None) -> bool:
"""Gate the speed-limit controller off on platforms that can't run it, dropping a
stuck 'control' mode down to 'warning'."""
store = store or _Store()
brand = cp.brand
off = (brand == "rivian"
or (brand == "tesla" and store.get_bool("IsReleaseIqBranch"))
or (not cp.openpilotLongitudinalControl and cp_iq.pcmCruiseSpeed))
if off and store.get("IQSpeedAssistMode", return_default=True) == 3: # control -> warning
store.put("IQSpeedAssistMode", 2)
return not off
def _stamp_lateral_model(cp, cp_iq, store) -> bool:
where, label, precise = _resolve_nn(cp)
nn = cp_iq.iqLateralNet
nn.model.path, nn.model.name, nn.fuzzyFingerprint = where, label, not precise
if label == "MOCK":
_log.error({"nnff event": "car doesn't match any Neural Network model"})
return False
return cp.steerControlType != _ANGLE and store.get_bool("NeuralNetworkFeedForward")
def _cleanup_unsupported_params(cp, cp_iq, store=None) -> None:
store = store or _Store()
doomed = {
"NeuralNetworkFeedForward": cp.steerControlType == _ANGLE,
"LongIncrementsEnabled": not cp.openpilotLongitudinalControl and cp_iq.pcmCruiseSpeed,
}
for name, gone in doomed.items():
if gone:
_log.warning(f"unsupported on this port, clearing {name}")
store.remove(name)
set_speed_limit_controller_availability(cp, cp_iq, store)
def apply_iq_car_config(ci, store=None) -> None:
store = store or _Store()
if _stamp_lateral_model(ci.CP, ci.CP_IQ, store):
ci.configure_torque_tune(ci.CP.carFingerprint, ci.CP.lateralTuning)
_cleanup_unsupported_params(ci.CP, ci.CP_IQ, store)

View File

@@ -0,0 +1,58 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from dataclasses import dataclass
from openpilot.common.params import Params
# Cruise set-speed step (in the caller's working unit, kph or the imperial increment)
# a user is allowed to dial in for the accel/decel cruise buttons.
MIN_BUTTON_STEP = 1
MAX_BUTTON_STEP = 10
# Once the resolved step reaches this size, we snap the set speed to the nearest
# multiple of the step (e.g. a step of 5 lands on 45/50/55...) instead of just
# adding it on top of whatever odd number the set speed currently sits at.
SNAP_TO_GRID_THRESHOLD = 5
# Stock behavior (feature disabled): tap moves by one unit, a held button moves
# five times faster. This mirrors what every other unmodified button-input car
# already does, so it's kept as the fallback rather than living in this module.
STOCK_HOLD_MULTIPLIER = 5
@dataclass(frozen=True)
class LongIncrementConfig:
enabled: bool
tap_step: int
hold_step: int
def _clamp_step(value) -> int:
try:
step = int(value)
except (TypeError, ValueError):
return MIN_BUTTON_STEP
return min(max(step, MIN_BUTTON_STEP), MAX_BUTTON_STEP)
def read_long_increment_config(params: Params) -> LongIncrementConfig:
return LongIncrementConfig(
enabled=params.get_bool("LongIncrementsEnabled"),
tap_step=_clamp_step(params.get("LongIncrementTapStep", return_default=True)),
hold_step=_clamp_step(params.get("LongIncrementHoldStep", return_default=True)),
)
def resolve_button_step(config: LongIncrementConfig, held: bool, unit_step: float) -> tuple[bool, float]:
"""
Turn a single tap/hold cruise button event into a (snap_to_grid, delta) pair,
where delta is expressed in the same unit as unit_step (kph, or the mph-derived
increment used for imperial cars).
"""
if not config.enabled:
return held, unit_step * (STOCK_HOLD_MULTIPLIER if held else 1)
multiplier = config.hold_step if held else config.tap_step
snap_to_grid = multiplier >= SNAP_TO_GRID_THRESHOLD
return snap_to_grid, unit_step * multiplier

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.iqpilot.selfdrive.car.vehicle_catalog import load_catalog
def refresh_car_list_param() -> None:
platforms = load_catalog()
if not platforms:
cloudlog.warning("vehicle catalog not found; leaving CarList param unchanged")
return
params = Params()
if params.get("CarList") == platforms:
cloudlog.warning("CarList param already current, nothing to write")
return
params.put("CarList", platforms)
cloudlog.warning("CarList param refreshed from vehicle catalog")
if __name__ == "__main__":
refresh_car_list_param()

View File

View File

@@ -0,0 +1,38 @@
from cereal import custom
from iqdbc.car import structs
from openpilot.iqpilot.selfdrive.car.interfaces import _cleanup_unsupported_params
class DummyParams:
def __init__(self):
self.removed: list[str] = []
self.values: dict[str, object] = {}
def remove(self, key: str) -> None:
self.removed.append(key)
def get_bool(self, key: str) -> bool:
return bool(self.values.get(key, False))
def get(self, key: str, return_default: bool = False):
return self.values.get(key)
def put(self, key: str, value) -> None:
self.values[key] = value
class TestLongitudinalModePersistence:
def test_iq_dynamic_mode_is_not_removed_when_openpilot_long_is_unavailable(self):
params = DummyParams()
cp = structs.CarParams()
cp.openpilotLongitudinalControl = False
cp.steerControlType = structs.CarParams.SteerControlType.torque
cp_iq = custom.IQCarParams()
cp_iq.pcmCruiseSpeed = True
_cleanup_unsupported_params(cp, cp_iq, params)
assert "IQDynamicMode" not in params.removed
assert "LongIncrementsEnabled" in params.removed

View File

@@ -0,0 +1,121 @@
from types import SimpleNamespace
import pytest
from cereal import car, custom
from openpilot.common.constants import CV
from openpilot.iqpilot.selfdrive.car.enhanced_stock_longitudinal_control import build_iq_control_params_from_plan
from openpilot.selfdrive.car.cruise import VCruiseHelper
class TestSpeedLimitSetSpeedMirror:
def setup_method(self):
self.CP = car.CarParams(pcmCruise=True, openpilotLongitudinalControl=True)
self.CP_IQ = custom.IQCarParams(pcmCruiseSpeed=True)
self.v_cruise_helper = VCruiseHelper(self.CP, self.CP_IQ)
self.v_cruise_helper.set_speed_to_limit = True
@staticmethod
def _iq_plan(limit_mps: float, state) -> SimpleNamespace:
resolver = SimpleNamespace(
speedLimitValid=limit_mps > 0,
speedLimitLastValid=limit_mps > 0,
speedLimitFinalLast=limit_mps,
)
assist = SimpleNamespace(state=state)
return SimpleNamespace(speedLimit=SimpleNamespace(resolver=resolver, assist=assist))
def test_op_long_mirrors_active_speed_limit_target_into_cluster_speed(self):
self.v_cruise_helper.update_speed_limit_assist(False, self._iq_plan(17.88, custom.IQPlan.SpeedLimit.AssistState.active))
CS = car.CarState(cruiseState={"available": True, "speed": 22.35, "speedCluster": 22.35})
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False)
assert self.v_cruise_helper.v_cruise_kph == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
assert self.v_cruise_helper.v_cruise_cluster_kph == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
def test_op_long_syncs_to_new_limit_even_when_assist_not_active(self):
self.v_cruise_helper.update_speed_limit_assist(False, self._iq_plan(17.88, custom.IQPlan.SpeedLimit.AssistState.inactive))
CS = car.CarState(cruiseState={"available": True, "speed": 22.35, "speedCluster": 22.35})
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False)
assert self.v_cruise_helper.v_cruise_kph == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
assert self.v_cruise_helper.v_cruise_cluster_kph == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
def test_op_long_allows_manual_set_speed_changes_between_limit_changes(self):
self.v_cruise_helper.update_speed_limit_assist(False, self._iq_plan(17.88, custom.IQPlan.SpeedLimit.AssistState.inactive))
# First cycle after a valid limit appears will sync to the resolved target.
CS = car.CarState(cruiseState={"available": True, "speed": 22.35, "speedCluster": 22.35})
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False)
# On later cycles with the same limit, manual set speed changes should be preserved.
CS = car.CarState(cruiseState={"available": True, "speed": 15.64, "speedCluster": 15.64})
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False)
assert self.v_cruise_helper.v_cruise_kph == pytest.approx(15.64 * CV.MS_TO_KPH, abs=0.1)
assert self.v_cruise_helper.v_cruise_cluster_kph == pytest.approx(15.64 * CV.MS_TO_KPH, abs=0.1)
def test_op_long_resyncs_when_limit_changes(self):
self.v_cruise_helper.update_speed_limit_assist(False, self._iq_plan(17.88, custom.IQPlan.SpeedLimit.AssistState.inactive))
CS = car.CarState(cruiseState={"available": True, "speed": 22.35, "speedCluster": 22.35})
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False)
CS = car.CarState(cruiseState={"available": True, "speed": 15.64, "speedCluster": 15.64})
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False)
self.v_cruise_helper.update_speed_limit_assist(False, self._iq_plan(13.41, custom.IQPlan.SpeedLimit.AssistState.inactive))
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False)
assert self.v_cruise_helper.v_cruise_kph == pytest.approx(13.41 * CV.MS_TO_KPH, abs=0.1)
assert self.v_cruise_helper.v_cruise_cluster_kph == pytest.approx(13.41 * CV.MS_TO_KPH, abs=0.1)
def test_set_speed_does_not_follow_limit_when_feature_off():
# Default off: set speed must stay the driver's value (limiter-only via planner min-blend).
CP = car.CarParams(pcmCruise=True, openpilotLongitudinalControl=True)
CP_IQ = custom.IQCarParams(pcmCruiseSpeed=True)
helper = VCruiseHelper(CP, CP_IQ)
helper.set_speed_to_limit = False
helper.update_speed_limit_assist(False, TestSpeedLimitSetSpeedMirror._iq_plan(
17.88, custom.IQPlan.SpeedLimit.AssistState.active))
CS = car.CarState(cruiseState={"available": True, "speed": 22.35, "speedCluster": 22.35})
helper.update_v_cruise(CS, enabled=True, is_metric=False)
# Set speed tracks the car's cruise speed, NOT the 17.88 m/s limit.
assert helper.v_cruise_kph == pytest.approx(22.35 * CV.MS_TO_KPH, abs=0.1)
def test_enhanced_stock_longitudinal_control_syncs_once_then_follows_cluster_speed():
CP = car.CarParams(pcmCruise=True, openpilotLongitudinalControl=True)
resolver = SimpleNamespace(speedLimitFinalLast=17.88)
assist = SimpleNamespace(enabled=True)
iq_plan = SimpleNamespace(speedLimit=SimpleNamespace(resolver=resolver, assist=assist))
params, sync_limit, pending_limit = build_iq_control_params_from_plan(
CP, iq_plan, True, current_set_speed_kph=100.0, previous_sync_limit_kph=None, pending_sync_limit_kph=None
)
assert sync_limit == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
assert pending_limit == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
assert float(params[0]["value"].decode("utf-8")) == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
params, sync_limit, pending_limit = build_iq_control_params_from_plan(
CP, iq_plan, True, current_set_speed_kph=22.0, previous_sync_limit_kph=sync_limit, pending_sync_limit_kph=pending_limit
)
assert sync_limit == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
assert pending_limit == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
assert float(params[0]["value"].decode("utf-8")) == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
params, sync_limit, pending_limit = build_iq_control_params_from_plan(
CP, iq_plan, True, current_set_speed_kph=17.88 * CV.MS_TO_KPH, previous_sync_limit_kph=sync_limit, pending_sync_limit_kph=pending_limit
)
assert sync_limit == pytest.approx(17.88 * CV.MS_TO_KPH, abs=0.1)
assert pending_limit is None
params, sync_limit, pending_limit = build_iq_control_params_from_plan(
CP, iq_plan, True, current_set_speed_kph=22.0, previous_sync_limit_kph=sync_limit, pending_sync_limit_kph=pending_limit
)
assert float(params[0]["value"].decode("utf-8")) == pytest.approx(22.0, abs=0.1)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
import json
import os
from openpilot.common.basedir import BASEDIR
SCHEMA = "iqlvbs/supported-vehicles"
REV = 1
CATALOG_FILENAME = "vehicle_catalog.json"
_CANDIDATE_PARTS = (
("iqpilot", "selfdrive", "car", CATALOG_FILENAME),
)
# in-memory (car-interface) field -> on-disk compact key
_ATTR_TO_KEY = (
("platform", "id"),
("make", "mk"),
("brand", "grp"),
("model", "mdl"),
("year", "yrs"),
("package", "req"),
)
def _reference(platform: str, years: list[str], claimed: set[str]) -> str:
span = f"{years[0]}-{years[-1]}" if len(years) > 1 else (years[0] if years else "na")
stem = f"{platform}|{span}"
ref, bump = stem, 2
while ref in claimed:
ref = f"{stem}#{bump}"
bump += 1
claimed.add(ref)
return ref
def encode(vehicles: dict[str, dict]) -> dict:
records: dict[str, dict] = {}
claimed: set[str] = set()
for label, attrs in vehicles.items():
years = list(attrs.get("year") or [])
ref = _reference(attrs.get("platform", ""), years, claimed)
record = {"label": label}
for attr, key in _ATTR_TO_KEY:
record[key] = attrs.get(attr)
records[ref] = record
return {"catalog": SCHEMA, "rev": REV, "vehicles": records}
def decode(envelope: dict) -> dict[str, dict]:
vehicles: dict[str, dict] = {}
for record in (envelope.get("vehicles") or {}).values():
attrs = {attr: record.get(key) for attr, key in _ATTR_TO_KEY}
vehicles[record.get("label", "")] = attrs
return vehicles
def catalog_path(basedir: str = BASEDIR) -> str | None:
for parts in _CANDIDATE_PARTS:
candidate = os.path.join(basedir, *parts)
if os.path.isfile(candidate):
return candidate
return None
def load_catalog(basedir: str = BASEDIR) -> dict[str, dict]:
path = catalog_path(basedir)
if path is None:
return {}
with open(path) as handle:
return decode(json.load(handle))
def _write(vehicles: dict[str, dict], basedir: str = BASEDIR) -> str:
out = os.path.join(basedir, "iqpilot", "selfdrive", "car", CATALOG_FILENAME)
with open(out, "w") as handle:
json.dump(encode(vehicles), handle, indent=2, ensure_ascii=False)
return out
if __name__ == "__main__":
from iqdbc.lvbs.car.car_catalog import build_car_catalog
print("wrote", _write(build_car_catalog()))