IQ.Pilot Release Commit @ 98a2c61

This commit is contained in:
IQ.Lvbs CI [bot]
2026-08-19 14:10:31 -05:00
parent dca8172e55
commit 7e3d70380a
55 changed files with 699 additions and 114 deletions

View File

@@ -12,7 +12,7 @@ from iqpilot.cereal import car, log, custom
from iqpilot.common.iq_perf import PerfSample, PerfTraceEmitter, PerfTraceRing
from iqpilot.common.params import Params, UnknownKeyName
from iqpilot.common.realtime import config_realtime_process, lock_memory, Priority, Ratekeeper
from iqpilot.common.realtime import config_background_thread, config_realtime_process, lock_memory, Priority, Ratekeeper
from iqpilot.common.swaglog import cloudlog, ForwardingHandler
from iqdbc.car import DT_CTRL, structs
@@ -497,6 +497,7 @@ class Car:
pass
def params_thread(self, evt):
config_background_thread()
while not evt.is_set():
self.is_metric = self.params.get_bool("IsMetric")
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl

View File

@@ -91,9 +91,5 @@ class LongControl:
freeze_integrator=gas_override)
self.smooth.reset()
if gas_override:
# safety blocks braking while the gas is pressed, and a blocked tx drops the whole frame
output_accel = max(output_accel, 0.0)
self.last_output_accel = np.clip(output_accel, accel_limits[0], accel_limits[1])
return self.last_output_accel

View File

@@ -16,17 +16,17 @@ from iqpilot.selfdrive.controls.lib.latcontrol_torque import TORQUE_NN_MODEL_PAT
# layers). Used as a fallback so the loader logic is still exercised when no trained
# models are shipped (they are removed pending retraining and re-added over time).
_SYNTHETIC_MODEL = {
"input_size": 4,
"input_size": 18,
"output_size": 1,
"input_mean": [[0.0], [0.0], [0.0], [0.0]],
"input_std": [[1.0], [1.0], [1.0], [1.0]],
"input_mean": [[0.0]] * 18,
"input_std": [[1.0]] * 18,
"layers": [
{"dense_1_W": [[0.5, 0.5, 0.5, 0.5], [0.5, 0.5, 0.5, 0.5]], "dense_1_b": [[0.0], [0.0]], "activation": "sigmoid"},
{"dense_1_W": [[0.5] * 18, [0.5] * 18], "dense_1_b": [[0.0], [0.0]], "activation": "sigmoid"},
{"dense_2_W": [[2.0, 2.0]], "dense_2_b": [[-1.0]], "activation": "identity"},
],
}
MODEL_FILES = sorted(f for f in os.listdir(TORQUE_NN_MODEL_PATH) if f.endswith(".json"))
MODEL_FILES = sorted(f for f in os.listdir(TORQUE_NN_MODEL_PATH) if f.endswith(".json")) if os.path.isdir(TORQUE_NN_MODEL_PATH) else []
if MODEL_FILES:
_MODEL_DIR = TORQUE_NN_MODEL_PATH
_NAMES = MODEL_FILES

View File

@@ -15,15 +15,9 @@ from iqpilot.common.params import Params
from iqpilot.common.pid import PIDController
from iqpilot.selfdrive.iqmodeld.config import ModelConstants
from iqpilot.selfdrive.controls.lib.latcontrol_torque import NeuralNetworkFeedForward
from iqpilot.selfdrive.controls.lib.latcontrol_torque import TORQUE_NN_MODEL_PATH
from iqpilot.selfdrive.controls.lib.neural_network_feed_forward.tests.test_network import _MODEL_DIR, _NAMES
_REAL_MODEL = next((f for f in sorted(os.listdir(TORQUE_NN_MODEL_PATH))
if f.endswith(".json") and f != "MOCK.json"), None)
# Models are shipped separately and re-added as retrained; with none present,
# NNFF is a no-op (falls back to stock torque FF), so the assembly tests skip.
pytestmark = pytest.mark.skipif(_REAL_MODEL is None,
reason="no NNFF models present (nuked pending retraining)")
_REAL_MODEL = next((f for f in _NAMES if f != "MOCK.json"), _NAMES[0])
def _torque_fn():
@@ -53,7 +47,7 @@ def _model_v2():
def _make_controller(model_file):
Params().put_bool("NeuralNetworkFeedForward", True)
path = os.path.join(TORQUE_NN_MODEL_PATH, model_file)
path = os.path.join(_MODEL_DIR, model_file)
cp = SimpleNamespace(steerActuatorDelay=0.15)
cp_iq = SimpleNamespace(iqLateralNet=SimpleNamespace(
model=SimpleNamespace(path=path, name=os.path.splitext(model_file)[0])))
@@ -84,7 +78,10 @@ class TestControllerWiring:
def test_mock_model_reports_absent(self):
nnff = _make_controller("MOCK.json")
assert nnff.has_nn_model is False
assert nnff.model.input_size >= 2 # MOCK still loads as a valid net
if "MOCK.json" in _NAMES:
assert nnff.model.input_size >= 2
else:
assert nnff.model is None
def test_update_returns_finite_torque(self):
nnff = _make_controller(_REAL_MODEL)

View File

@@ -1,5 +1,7 @@
from types import SimpleNamespace
from iqpilot.cereal import custom
from iqpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans
from iqpilot.selfdrive.controls.lib.longcontrol import LongControl, LongCtrlState, long_control_state_trans
class TestLongControlStateTransition:
@@ -41,3 +43,30 @@ def test_engage():
next_state = long_control_state_trans(CP_IQ, active, current_state,
should_stop=False, brake_pressed=False, cruise_standstill=False)
assert next_state == LongCtrlState.pid
def test_gas_override_preserves_negative_accel_command():
pid_calls = []
control = object.__new__(LongControl)
control.CP = SimpleNamespace(stopAccel=-0.55)
control.CP_IQ = SimpleNamespace(enableGasInterceptor=False)
control.long_control_state = LongCtrlState.pid
control.pid = SimpleNamespace(
update=lambda error, **kwargs: pid_calls.append((error, kwargs)) or -0.5,
reset=lambda: None,
)
control.last_output_accel = -0.4
control.stopping_decel_rate = 1.0
control.smooth = SimpleNamespace(enabled=False, update=lambda: None, reset=lambda: None)
car_state = SimpleNamespace(
vEgo=15.0,
aEgo=0.0,
brakePressed=False,
standstill=False,
cruiseState=SimpleNamespace(standstill=False),
)
output = control.update(True, car_state, -0.5, False, (-3.5, 2.0), gas_override=True)
assert output == -0.5
assert pid_calls == [(-0.5, {"speed": 15.0, "feedforward": -0.5, "freeze_integrator": True})]

View File

@@ -11,6 +11,7 @@ MODELS_DIR = MODELD_DIR / 'models'
BASEDIR = MODELD_DIR.parents[2]
METADATA_SCRIPT = MODELD_DIR / 'get_model_metadata.py'
PYPROJECT = BASEDIR / 'pyproject.toml'
TINYGRAD_REVISION_FILE = BASEDIR / 'artifacts/package_sources/tinygrad/.iqpilot-revision'
MODEL_NAMES = ['dmonitoring_model']
@@ -30,9 +31,16 @@ def _file_sha256(path: Path) -> str:
@functools.lru_cache(maxsize=1)
def _tinygrad_revision() -> str:
match = re.search(r'"tinygrad @ git\+https://[^@]+@([0-9a-f]{40})"', PYPROJECT.read_text())
if match is None:
raise RuntimeError("missing pinned tinygrad revision")
return match.group(1)
if match is not None:
return match.group(1)
try:
revision = TINYGRAD_REVISION_FILE.read_text().strip()
except OSError as e:
raise RuntimeError("missing pinned tinygrad revision") from e
if re.fullmatch(r'[0-9a-f]{40}', revision) is None:
raise RuntimeError("invalid pinned tinygrad revision")
return revision
CHECK_PATH = MODELS_DIR / 'prebuilt_check.json'

View File

@@ -49,3 +49,17 @@ def test_source_checkout_is_not_packaged_prebuilt(tmp_path, monkeypatch):
monkeypatch.setattr(prebuilt_models, 'CHECK_PATH', check_path)
assert not prebuilt_models.packaged_prebuilt_matches('dmonitoring_model')
def test_vendored_tinygrad_revision(tmp_path, monkeypatch):
revision = '0123456789abcdef0123456789abcdef01234567'
pyproject = tmp_path / 'pyproject.toml'
revision_file = tmp_path / '.iqpilot-revision'
pyproject.write_text('dependencies = ["tinygrad"]\n')
revision_file.write_text(f'{revision}\n')
monkeypatch.setattr(prebuilt_models, 'PYPROJECT', pyproject)
monkeypatch.setattr(prebuilt_models, 'TINYGRAD_REVISION_FILE', revision_file)
prebuilt_models._tinygrad_revision.cache_clear()
assert prebuilt_models._tinygrad_revision() == revision
prebuilt_models._tinygrad_revision.cache_clear()

View File

@@ -12,7 +12,7 @@ from msgq.visionipc import VisionIpcClient
from iqpilot.common.params import Params
from iqpilot.common.issue_debug import log_issue_limited
from iqpilot.common.realtime import config_realtime_process, lock_memory, Priority, Ratekeeper, DT_CTRL
from iqpilot.common.realtime import config_background_thread, config_realtime_process, lock_memory, Priority, Ratekeeper, DT_CTRL
from iqpilot.common.swaglog import cloudlog
from iqpilot.common.gps import get_gps_location_service
@@ -755,6 +755,7 @@ class SelfdriveD(GapButtonActions):
)
def params_thread(self, evt):
config_background_thread()
while not evt.is_set():
self.is_metric = self.params.get_bool("IsMetric")
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")

View File

@@ -98,7 +98,7 @@ class ChangelogWidget(Widget):
return
self._last_load = now
paths = [os.path.join(BASEDIR, "docs", "CHANGELOG.md")]
paths = [os.path.join(BASEDIR, "iqpilot", "docs", "CHANGELOG.md")]
content = ""
for p in paths:
try:
@@ -110,7 +110,7 @@ class ChangelogWidget(Widget):
pass
if not content:
content = "No changelog found.\n\nAdd docs/CHANGELOG.md."
content = "No changelog found.\n\nAdd iqpilot/docs/CHANGELOG.md."
ordered = self._reorder_sections_newest_first(content)
self._latest_text = self._build_latest_text(ordered)

View File

@@ -60,11 +60,6 @@ sound_list: dict[int, tuple[str, int | None, float]] = {
**sound_list_iq,
}
if HARDWARE.get_device_type() in ("tizi", "tici"):
sound_list.update({
AudibleAlert.engage: ("engage.wav", 1, MAX_VOLUME),
AudibleAlert.disengage: ("disengage.wav", 1, MAX_VOLUME),
})
def check_selfdrive_timeout_alert(sm):
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']