diff --git a/artifacts/iqpilot_updater_private/manifest.json b/artifacts/iqpilot_updater_private/manifest.json index e8f50441f..2b060fc39 100644 --- a/artifacts/iqpilot_updater_private/manifest.json +++ b/artifacts/iqpilot_updater_private/manifest.json @@ -16,17 +16,17 @@ }, "python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "94664d1b9abd1c0cb124707bf10f746f23adf0847a890d507df86f1e4a7e8ca9", + "sha256": "50f00d356371a60a50b2395966b6b1646fd3b8306e9ad6fe0229ad0e24186683", "size": 67616 }, "python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "481235a5685e14f5b8e9216908be23be9743fe6f487f2b38fb21c33dd84e22cf", + "sha256": "57b15e47bcc1084d33bda3908fb50347c2403cde0c9c951dad1b107ef56b06f1", "size": 67616 }, "python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "75ffbf8212f805b054c97ca4723a56071db822e889cda9d6bec233b2d2c8592a", + "sha256": "1d3611c753cc94d1e22802900a2487a13c445def98fa998385fa72935a0dd110", "size": 136488 }, "runtime": { @@ -38,8 +38,8 @@ } }, "signatures": { - "python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so": "B45oDHpHNmslzjTyeuEVbE4fOytUsC/0po+CiHiLI+T01yWJIb6Dt4yFJm+qVTEqCLJfajedd1FnIQHCyUddDw==", - "python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so": "a+YcE2ik7Oo3btunKGoA+FHN+uSDoric2V6nu5SjtKSM/Nwpg1M9BX0BM55sA56sz6FgPRn93u+yPL7eryLYDw==", - "python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so": "NcdigDJOQ7d9A+HXCewPowezSML1i4cHW6uRyZ1N6vmtkXDorRk9YC4lxlM8uvoS/tB5/jQBkfc/DhkwPXAoAA==" + "python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so": "ZIgAfOcrpjAYBY/iLm9oovGs6jv7dQAToObwc9rrPpPTEgQZ/zTcTOa0TcqGTU/lK0tPhum2WVrPpxwosKUwDg==", + "python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so": "Zv7AsE+0KgGABA4n9Hf40UQVOz7k2ew/vxgKqkUpyFjZKdst6DUecAxyBPbK4Z8owTaKnnbNRxuRGiJtr+sJDA==", + "python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so": "2aLUgznDiiBF3sYwIxZKMjMW2A1S9q+tHZUnyAiEB48QbxAFjDAS6tMHUp5ej6/JAmejOzxQePYPL1jJnxKsBQ==" } } diff --git a/artifacts/iqpilot_updater_private/manifest.json.sig b/artifacts/iqpilot_updater_private/manifest.json.sig index d17c8b251..c653f680a 100644 --- a/artifacts/iqpilot_updater_private/manifest.json.sig +++ b/artifacts/iqpilot_updater_private/manifest.json.sig @@ -1 +1 @@ -54xdruMwhqg4ouUav9CwbOhcsytYbn/uy/OL1pT6Sd/JRvgsLckfe8+jsG527nyhZ7sBpxPx5dFLX3h/eOaSDg== +D9FZfSC3DUspPq4ewwPjNoCS1N8ai3jcgutVQo94CnOI6Wcx3L1H30hTZ6SS8jw9MfMsVaggFXfMo2QT4yJlBQ== diff --git a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so index acc884ee1..6cfd73b87 100755 Binary files a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so index 12129a3a5..eccb704ad 100755 Binary files a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so index 7f44ac5b7..2c111e5da 100755 Binary files a/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_updater_private/python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/package_sources/iqdbc/conftest.py b/artifacts/package_sources/iqdbc/conftest.py index a65bc88cf..dc8904cb0 100644 --- a/artifacts/package_sources/iqdbc/conftest.py +++ b/artifacts/package_sources/iqdbc/conftest.py @@ -41,6 +41,9 @@ if not _os.path.exists("/usr/libexec/iqpilot/runtime_integrity.json"): "update_mqb_carstate_alc_state": _noop, "update_mlb_carstate_alc_state": _noop, "update_pq_carstate_alc_state": _noop, + "update_turn_signals": _noop, + "create_vehicle_odometer_store": lambda *a, **k: _VehicleOdometerStoreStub(), + "vw_driver_override_threshold_cnm": lambda cs, platform, default: default, }) _stub_module("iqpilot_private.konn3kt.iqlvbs.iqlvbs_commander", { "update_turn_signals": _noop, @@ -50,13 +53,20 @@ if not _os.path.exists("/usr/libexec/iqpilot/runtime_integrity.json"): try: from iqpilot.system.proprietary_runtime import _verified_import as _vi _real_import_verified_module = _vi.import_verified_module - - def _import_verified_module(bundle, module): - return _sys.modules.get(module) or _real_import_verified_module(bundle, module) - - _vi.import_verified_module = _import_verified_module except Exception: - pass + _vi = _types.ModuleType("iqpilot.system.proprietary_runtime._verified_import") + _sys.modules["iqpilot.system.proprietary_runtime._verified_import"] = _vi + _real_import_verified_module = None + + def _import_verified_module(bundle, module): + stub = _sys.modules.get(module) + if stub is not None: + return stub + if _real_import_verified_module is None: + raise ImportError(module) + return _real_import_verified_module(bundle, module) + + _vi.import_verified_module = _import_verified_module # --- end host test harness --------------------------------------------------- _ABSTRACT_BASES = frozenset(("CarSafetyTest", "AolSafetyTestBase", "SafetyTest", "SafetyTestBase")) diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tests/routes.py b/artifacts/package_sources/iqdbc/iqdbc/car/tests/routes.py index cdb3cebbf..6ad0f6af5 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/tests/routes.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tests/routes.py @@ -353,6 +353,7 @@ routes = [ CarTestRoute("79b0a26c081d6d6c/00000083--8407f96a1e", VOLKSWAGEN.VOLKSWAGEN_PASSAT_MK7), CarTestRoute("cc7e1fc0ce4ec686/00000004--dddc5bf4b6", VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS_PLUS), CarTestRoute("07667b885add75fd/2021-01-23--19-48-42", VOLKSWAGEN.AUDI_A3_MK3), + CarTestRoute("c8b7d4cd76391e07|00000000--c42dead9c6", VOLKSWAGEN.AUDI_A4_MK4, segment=29), CarTestRoute("5432d2499e17e646/000000e8--9ce8cf6d58", VOLKSWAGEN.AUDI_Q5_MK1), CarTestRoute("6c6b466346192818/2021-06-06--14-17-47", VOLKSWAGEN.AUDI_Q2_MK1), CarTestRoute("0cd0b7f7e31a3853/2021-12-03--03-12-05", VOLKSWAGEN.AUDI_Q3_MK2), diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/torque_data/substitute.toml b/artifacts/package_sources/iqdbc/iqdbc/car/torque_data/substitute.toml index a5f5c301f..9a0858ada 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/torque_data/substitute.toml +++ b/artifacts/package_sources/iqdbc/iqdbc/car/torque_data/substitute.toml @@ -124,6 +124,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "VOLKSWAGEN_ID3_MK2" = "VOLKSWAGEN_ID4_MK2" "SKODA_ENYAQ_MK1" = "VOLKSWAGEN_ID4_MK1" "SKODA_ENYAQ_MK2" = "VOLKSWAGEN_ID4_MK2" +"AUDI_A4_MK4" = "AUDI_Q5_MK1" "AUDI_Q4_MK1" = "VOLKSWAGEN_ID4_MK1" "AUDI_Q4_MK2" = "VOLKSWAGEN_ID4_MK2" "VOLKSWAGEN_GOLF_MK8" = "VOLKSWAGEN_GOLF_MK7" diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py index c4c442368..04c967b37 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py @@ -205,6 +205,8 @@ class CarController(CarControllerBase): self._pt_tx_bus = self.CAN.aux elif CP.flags & VolkswagenFlags.MLB: self.CCS = mlbcan + if CP.flags & VolkswagenFlagsIQ.IQ_MLB_NO_ECAN: + self._pt_tx_bus = self.CAN.aux elif CP.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO): self.CCS = mebcan else: @@ -648,7 +650,8 @@ class CarController(CarControllerBase): cancel_cmd = stock_cancel_pressed or self._tap_gra_cancel(CC.cruiseControl.cancel, gra_send_ready) resume_cmd = CC.cruiseControl.resume or self._should_spam_mqb_a0_resume(CS, iq_mqb_acc_resume) if gra_send_ready and (cancel_cmd or resume_cmd): - bus_send = self.CAN.aux if self.CP.flags & VolkswagenFlags.PQ else self.CAN.ext + stalk_on_powertrain = self.CP.flags & VolkswagenFlags.PQ or self.CP.flags & VolkswagenFlagsIQ.IQ_MLB_NO_ECAN + bus_send = self.CAN.aux if stalk_on_powertrain else self.CAN.ext can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, bus_send, CS.gra_stock_values, cancel=cancel_cmd, resume=resume_cmd)) diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carstate.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carstate.py index 92fcdc7cf..6b88bfaba 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carstate.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carstate.py @@ -672,9 +672,13 @@ class CarState(CarStateBase): ret.gasPressed = pt_cp.vl["Motor_03"]["MO_Fahrpedalrohwert_01"] > 0 if self.CP.carFingerprint == CAR.PORSCHE_MACAN_MK1: ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Getriebe_03"]["GE_Waehlhebel"], None)) + elif self.CP.transmissionType == TransmissionType.manual: + reverse = bool(pt_cp.vl["Gateway_05"]["BCM1_Rueckfahrlicht_Schalter"]) + ret.gearShifter = GearShifter.reverse if reverse else GearShifter.drive else: ret.gearShifter = GearShifter.drive + cc_only = bool(self.CP.flags & (VolkswagenFlagsIQ.IQ_CC_ONLY | VolkswagenFlagsIQ.IQ_CC_ONLY_NO_RADAR)) cruise_main_switch = bool(pt_cp.vl["LS_01"]["LS_Hauptschalter"]) if not self.CP.pcmCruise: ret.cruiseState.available = cruise_main_switch @@ -687,7 +691,8 @@ class CarState(CarStateBase): else: ret.cruiseState.available = pt_cp.vl["TSK_02"]["TSK_Status"] in (0, 1, 2) ret.cruiseState.enabled = pt_cp.vl["TSK_02"]["TSK_Status"] in (1, 2) - ret.cruiseState.speed = ext_cp.vl["ACC_02"]["ACC_Wunschgeschw_02"] * CV.KPH_TO_MS + if not cc_only: + ret.cruiseState.speed = ext_cp.vl["ACC_02"]["ACC_Wunschgeschw_02"] * CV.KPH_TO_MS ret.accFaulted = pt_cp.vl["TSK_02"]["TSK_Status"] in (3,) ret.cruiseState.nonAdaptive = bool(pt_cp.vl["LS_01"]["LS_Limiter"]) @@ -758,6 +763,13 @@ class CarState(CarStateBase): def parse_mlb_mqb_steering_state(self, ret, pt_cp, drive_mode=True): ret.steeringAngleDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradwinkel"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradwinkel"])] ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])] + + if self.CP.flags & VolkswagenFlagsIQ.IQ_MLB_NO_HCA_EPS: + ret.steeringTorque = 0.0 + ret.steeringPressed = False + ret.steerFaultTemporary, ret.steerFaultPermanent = False, True + return + ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])] ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE @@ -826,13 +838,17 @@ class CarState(CarStateBase): ] else: pt_messages += [("Kombi_02", math.nan)] # Auxiliary-bus cluster odometer + if CP.flags & VolkswagenFlagsIQ.IQ_MLB_NO_HCA_EPS: + pt_messages += [("LH_EPS_03", math.nan)] if CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT: cam_messages += [ ("HCA_01", 1), # From R242 Driver assistance camera, 50Hz if steering/1Hz if not ] + pt_bus = CanBus(CP).aux if CP.flags & VolkswagenFlagsIQ.IQ_MLB_NO_ECAN else CanBus(CP).pt + return { - Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).pt), + Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, pt_bus), Bus.aux: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).aux), Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, CanBus(CP).cam), } diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/fingerprints.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/fingerprints.py index 208bc504e..f8084015b 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/fingerprints.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/fingerprints.py @@ -1046,6 +1046,14 @@ FW_VERSIONS = { b'\xf1\x875Q0907572P \xf1\x890682', ], }, + CAR.AUDI_A4_MK4: { + (Ecu.engine, 0x7e0, None): [ + b'\xf1\x878K5907115M \xf1\x890005', + ], + (Ecu.srs, 0x715, None): [ + b'\xf1\x878K0959655P \xf1\x890610\xf1\x82\x040E0D050G', + ], + }, CAR.AUDI_Q2_MK1: { (Ecu.engine, 0x7e0, None): [ b'\xf1\x8704E906027JT\xf1\x894145', diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/interface.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/interface.py index 4df23c735..3b3027977 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/interface.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/interface.py @@ -7,7 +7,8 @@ from iqdbc.car.isotp_parallel_query import IsoTpParallelQuery from iqdbc.car.volkswagen.carcontroller import CarController from iqdbc.car.volkswagen.carstate import CarState from iqdbc.car.volkswagen.values import ( - CAR, CanBus, DashcamOnlyReason, NetworkLocation, RADAR_DISABLE_STATE, TransmissionType, + CAR, CanBus, DashcamOnlyReason, MLB_ACC_COORDINATOR_MSGS, MLB_GEARBOX_MSGS, MLB_MSG_ACC_10, + MLB_MSG_LH_EPS_03, NetworkLocation, RADAR_DISABLE_STATE, TransmissionType, VolkswagenFlags, VolkswagenSafetyFlags, VolkswagenFlagsIQ, get_longitudinal_stopping_speed_override, ) from iqdbc.car.volkswagen.radar_interface import RadarInterface @@ -94,6 +95,29 @@ class CarInterface(CarInterfaceBase): ret.transmissionType = TransmissionType.automatic ret.dashcamOnly = False + ecan_msgs = fingerprint[0] | fingerprint[2] + fingerprinted = bool(ecan_msgs or fingerprint[1]) + + if fingerprinted and not ecan_msgs: + ret.flags |= VolkswagenFlagsIQ.IQ_MLB_NO_ECAN.value + safety_configs[0].safetyParam |= VolkswagenSafetyFlags.MLB_NO_ECAN.value + ret.dashcamOnly = True + + has_acc = any(msg in ecan_msgs for msg in MLB_ACC_COORDINATOR_MSGS) + if fingerprinted and not has_acc: + if MLB_MSG_ACC_10 in ecan_msgs: + ret.flags |= VolkswagenFlagsIQ.IQ_CC_ONLY.value + else: + ret.flags |= VolkswagenFlagsIQ.IQ_CC_ONLY_NO_RADAR.value + + pt_msgs = fingerprint[1] if ret.flags & VolkswagenFlagsIQ.IQ_MLB_NO_ECAN else ecan_msgs + + if fingerprinted and MLB_MSG_LH_EPS_03 not in pt_msgs: + ret.flags |= VolkswagenFlagsIQ.IQ_MLB_NO_HCA_EPS.value + + if fingerprinted and not any(msg in pt_msgs for msg in MLB_GEARBOX_MSGS): + ret.transmissionType = TransmissionType.manual + elif ret.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO): if ret.flags & VolkswagenFlags.MEB: safety_configs = [get_safety_config(structs.CarParams.SafetyModel.volkswagenMeb)] @@ -209,6 +233,11 @@ class CarInterface(CarInterfaceBase): ret.longitudinalTuning.kiV = [0.4, 0.] ret.alphaLongitudinalAvailable = ret.networkLocation == NetworkLocation.gateway or docs or bool(ret.flags & VolkswagenFlags.DISABLE_RADAR) + + if ret.flags & VolkswagenFlags.MLB and ret.flags & (VolkswagenFlagsIQ.IQ_CC_ONLY | VolkswagenFlagsIQ.IQ_CC_ONLY_NO_RADAR): + ret.alphaLongitudinalAvailable = False + alpha_long = False + if alpha_long: # Proof-of-concept, prep for E2E only. No radar points available. Panda ALLOW_DEBUG firmware required. ret.openpilotLongitudinalControl = True diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py index fbe68f0d7..b791b8fb3 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py @@ -4,11 +4,14 @@ from types import SimpleNamespace import pytest -from iqdbc.car import structs +from iqdbc.can import CANPacker +from iqdbc.car import Bus, DT_CTRL, structs from iqdbc.car.volkswagen.carstate import CarState from iqdbc.car.structs import CarParams from iqdbc.car.volkswagen.interface import CarInterface -from iqdbc.car.volkswagen.values import CAR, FW_QUERY_CONFIG, WMI, VolkswagenFlags, VolkswagenFlagsIQ, VolkswagenSafetyFlags +from iqdbc.car.volkswagen.values import (CAR, FW_QUERY_CONFIG, MLB_ACC_COORDINATOR_MSGS, MLB_MSG_ACC_10, + MLB_MSG_LH_EPS_03, WMI, VolkswagenFlags, VolkswagenFlagsIQ, + VolkswagenSafetyFlags) from iqdbc.car.volkswagen.fingerprints import FW_VERSIONS Ecu = CarParams.Ecu @@ -118,6 +121,217 @@ class TestVolkswagenPlatformConfigs: assert bool(params.safetyConfigs[-1].safetyParam & VolkswagenSafetyFlags.ALLOW_LONG_ACCEL_WITH_GAS_PRESSED) is alpha_long +def _mlb_fingerprint(bus, msgs): + fingerprints = {b: {} for b in range(7)} + fingerprints[bus] = {msg: 8 for msg in msgs} + return fingerprints + + +A4_MK4_BUS_1 = (0x040, 0x080, 0x081, 0x086, 0x100, 0x101, 0x103, 0x104, 0x105, 0x106, 0x107, 0x10B, + 0x10C, 0x10E, 0x114, 0x11D, 0x309, 0x30B, 0x30E, 0x312, 0x391, 0x392, 0x39C, 0x3BF, + 0x3C0, 0x440, 0x471, 0x520, 0x585, 0x590, 0x5F0, 0x640, 0x641, 0x643, 0x644, 0x647, + 0x670, 0x6B2, 0x6B4, 0x6B7, 0x6B8, 0x6C0, 0x6C1) + +MLB_ECAN_GATEWAY = (0x086, 0x09F, 0x102, 0x103, 0x105, 0x106, 0x10B, 0x10C, 0x30B) +MLB_ECAN_CAMERA = (0x109, 0x10D, 0x117, 0x30C, 0x30F) + + +def test_mlb_no_ecan_car_moves_to_the_powertrain_bus(): + fingerprints = _mlb_fingerprint(1, A4_MK4_BUS_1) + params = CarInterface.get_params(CAR.AUDI_A4_MK4, fingerprints, [], alpha_long=True, is_release=False, docs=False) + + assert params.flags & VolkswagenFlagsIQ.IQ_MLB_NO_ECAN + assert params.safetyConfigs[-1].safetyParam & VolkswagenSafetyFlags.MLB_NO_ECAN + assert params.flags & VolkswagenFlagsIQ.IQ_CC_ONLY_NO_RADAR + assert params.flags & VolkswagenFlagsIQ.IQ_MLB_NO_HCA_EPS + assert params.transmissionType == CarParams.TransmissionType.manual + assert not params.alphaLongitudinalAvailable + assert not params.openpilotLongitudinalControl + assert params.dashcamOnly + + +def test_mlb_without_an_extended_can_is_not_controllable(): + for msgs in (A4_MK4_BUS_1, A4_MK4_BUS_1 + (0x9F,)): + params = CarInterface.get_params(CAR.AUDI_A4_MK4, _mlb_fingerprint(1, msgs), [], + alpha_long=False, is_release=False, docs=False) + assert params.dashcamOnly + + +def test_mlb_gateway_car_keeps_the_extended_can(): + fingerprints = _mlb_fingerprint(0, MLB_ECAN_GATEWAY) + fingerprints[2] = {msg: 8 for msg in MLB_ECAN_CAMERA} + params = CarInterface.get_params(CAR.AUDI_Q5_MK1, fingerprints, [], alpha_long=False, is_release=False, docs=False) + + assert not params.flags & VolkswagenFlagsIQ.IQ_MLB_NO_ECAN + assert not params.flags & (VolkswagenFlagsIQ.IQ_CC_ONLY | VolkswagenFlagsIQ.IQ_CC_ONLY_NO_RADAR) + assert not params.flags & VolkswagenFlagsIQ.IQ_MLB_NO_HCA_EPS + assert params.transmissionType == CarParams.TransmissionType.automatic + assert params.alphaLongitudinalAvailable + assert not params.dashcamOnly + + +def test_mlb_flags_are_not_inferred_without_a_fingerprint(): + fingerprints = {bus: {} for bus in range(7)} + for platform in (CAR.AUDI_Q5_MK1, CAR.PORSCHE_MACAN_MK1, CAR.AUDI_A4_MK4): + params = CarInterface.get_params(platform, fingerprints, [], alpha_long=False, is_release=False, docs=True) + assert not params.flags & VolkswagenFlagsIQ.IQ_MLB_NO_ECAN + assert not params.flags & (VolkswagenFlagsIQ.IQ_CC_ONLY | VolkswagenFlagsIQ.IQ_CC_ONLY_NO_RADAR) + assert not params.flags & VolkswagenFlagsIQ.IQ_MLB_NO_HCA_EPS + assert params.transmissionType == CarParams.TransmissionType.automatic + + +def _build_mlb_car(platform, fingerprints): + CP = CarInterface.get_params(platform, fingerprints, [], alpha_long=False, is_release=False, docs=False) + CP_IQ = CarInterface.get_params_iq(CP, platform, fingerprints, [], alpha_long=False, is_release_iq=False, docs=False) + return CarInterface(CP, CP_IQ) + + +def _a4_mk4_car(): + return _build_mlb_car(CAR.AUDI_A4_MK4, _mlb_fingerprint(1, A4_MK4_BUS_1)) + + +def _q5_mk1_car(): + fingerprints = _mlb_fingerprint(0, MLB_ECAN_GATEWAY) + fingerprints[2] = {msg: 8 for msg in MLB_ECAN_CAMERA} + return _build_mlb_car(CAR.AUDI_Q5_MK1, fingerprints) + + +def _a4_mk4_frames(packer, reverse=False, eps_torque=None): + msgs = [ + packer.make_can_msg("ESP_03", 1, {"ESP_%s_Radgeschw" % s: 30.0 for s in ("VL", "VR", "HL", "HR")}), + packer.make_can_msg("Motor_03", 1, {"MO_Fahrpedalrohwert_01": 0, "MO_BLS": 0}), + packer.make_can_msg("ESP_05", 1, {"ESP_Bremsdruck": 0, "ESP_Fahrer_bremst": 0}), + packer.make_can_msg("ESP_01", 1, {"ESP_Tastung_passiv": 0}), + packer.make_can_msg("ESP_02", 1, {"ESP_Stillstandsflag": 0}), + packer.make_can_msg("TSK_02", 1, {"TSK_Status": 0}), + packer.make_can_msg("LS_01", 1, {"LS_Hauptschalter": 1, "LS_Codierung": 2}), + packer.make_can_msg("LWI_01", 1, {"LWI_Lenkradwinkel": 12.0, "LWI_Lenkradw_Geschw": 4.0}), + packer.make_can_msg("Kombi_01", 1, {"KBI_angez_Geschw": 30.0, "KBI_Handbremse": 0}), + packer.make_can_msg("Kombi_02", 1, {"KBI_Inhalt_Tank": 40, "KBI_Kilometerstand": 100000}), + packer.make_can_msg("Airbag_02", 1, {"AB_Gurtschloss_FA": 3}), + packer.make_can_msg("Gateway_05", 1, {"BCM1_Rueckfahrlicht_Schalter": int(reverse)}), + ] + if eps_torque is not None: + msgs.append(packer.make_can_msg("LH_EPS_03", 1, {"EPS_Lenkmoment": abs(eps_torque), + "EPS_VZ_Lenkmoment": eps_torque < 0, + "EPS_HCA_Status": 3})) + return msgs + + +def _run(car, build, frames=25): + nanos = 0 + for _ in range(frames): + nanos += int(DT_CTRL * 1e9) + ret, _ = car.update([(nanos, build())]) + return ret + + +def test_mlb_no_ecan_parsers_read_the_powertrain_bus(): + a4 = _a4_mk4_car() + assert a4.CS.get_can_parsers(a4.CP, a4.CP_IQ)[Bus.pt].bus == 1 + + q5 = _q5_mk1_car() + assert q5.CS.get_can_parsers(q5.CP, q5.CP_IQ)[Bus.pt].bus == 0 + + +def test_mlb_no_ecan_transmits_on_the_powertrain_bus(): + a4 = _a4_mk4_car() + packer = CANPacker("vw_mlb") + + CC = structs.CarControl() + CC.enabled = True + CC.latActive = True + CC.cruiseControl.cancel = True + CC = CC.as_reader() + CC_IQ = structs.IQCarControl() + + sent = [] + nanos = 0 + for _ in range(20): + nanos += int(DT_CTRL * 1e9) + a4.update([(nanos, _a4_mk4_frames(packer, eps_torque=0))]) + _, can_sends = a4.apply(CC, CC_IQ, nanos) + sent.extend(can_sends) + + assert {addr for addr, _, _ in sent} == {0x126, 0x397, 0x10B} + assert {bus for _, _, bus in sent} == {1} + + +def test_mlb_no_hca_eps_reports_a_steer_fault_without_a_can_fault(): + a4 = _a4_mk4_car() + assert a4.CP.flags & VolkswagenFlagsIQ.IQ_MLB_NO_HCA_EPS + + packer = CANPacker("vw_mlb") + ret = _run(a4, lambda: _a4_mk4_frames(packer)) + + assert ret.steerFaultPermanent + assert not ret.steerFaultTemporary + assert ret.steeringTorque == 0.0 + assert not ret.steeringPressed + assert ret.steeringAngleDeg == pytest.approx(12.0, abs=0.2) + + pt = a4.can_parsers[Bus.pt] + assert pt.message_states[MLB_MSG_LH_EPS_03].ignore_alive + assert all(parser.can_valid for parser in a4.can_parsers.values()) + + +def test_mlb_no_hca_eps_survives_the_parser_aliveness_timeout(): + a4 = _a4_mk4_car() + packer = CANPacker("vw_mlb") + pt = a4.can_parsers[Bus.pt] + + nanos = 0 + for _ in range(int(15.0 / DT_CTRL)): + nanos += int(DT_CTRL * 1e9) + ret, _ = a4.update([(nanos, _a4_mk4_frames(packer))]) + pt.vl["LH_EPS_03"]["EPS_HCA_Status"] + + assert ret.steerFaultPermanent + assert all(parser.can_valid for parser in a4.can_parsers.values()) + assert not any(parser.bus_timeout for parser in a4.can_parsers.values()) + + +def test_mlb_lane_assist_eps_restores_the_normal_torque_path(): + fingerprints = _mlb_fingerprint(1, A4_MK4_BUS_1 + (0x9F,)) + a4 = _build_mlb_car(CAR.AUDI_A4_MK4, fingerprints) + assert not a4.CP.flags & VolkswagenFlagsIQ.IQ_MLB_NO_HCA_EPS + + packer = CANPacker("vw_mlb") + ret = _run(a4, lambda: _a4_mk4_frames(packer, eps_torque=-140)) + + assert ret.steeringTorque == pytest.approx(-140, abs=1) + assert ret.steeringPressed + assert not ret.steerFaultPermanent + assert not a4.can_parsers[Bus.pt].message_states[MLB_MSG_LH_EPS_03].ignore_alive + + +def test_mlb_cc_only_never_reads_the_acc_coordinator(): + a4 = _a4_mk4_car() + assert a4.CP.flags & VolkswagenFlagsIQ.IQ_CC_ONLY_NO_RADAR + + packer = CANPacker("vw_mlb") + ret = _run(a4, lambda: _a4_mk4_frames(packer)) + + assert ret.cruiseState.available + assert not ret.cruiseState.enabled + assert ret.cruiseState.speed == 0.0 + + parsers = a4.can_parsers + for msg in MLB_ACC_COORDINATOR_MSGS + (MLB_MSG_ACC_10,): + for parser in parsers.values(): + assert msg not in parser.addresses + + +def test_mlb_manual_gear_follows_the_reverse_light_switch(): + a4 = _a4_mk4_car() + assert a4.CP.transmissionType == CarParams.TransmissionType.manual + + packer = CANPacker("vw_mlb") + assert _run(a4, lambda: _a4_mk4_frames(packer, reverse=False)).gearShifter == structs.CarState.GearShifter.drive + assert _run(a4, lambda: _a4_mk4_frames(packer, reverse=True)).gearShifter == structs.CarState.GearShifter.reverse + assert _run(a4, lambda: _a4_mk4_frames(packer, reverse=False)).gearShifter == structs.CarState.GearShifter.drive + + @pytest.mark.parametrize("button_type", ( structs.CarState.ButtonEvent.Type.setCruise, structs.CarState.ButtonEvent.Type.resumeCruise, diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/values.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/values.py index 48e4e4ebf..c540bbad5 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/values.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/values.py @@ -300,6 +300,7 @@ class VolkswagenSafetyFlags(IntFlag): PQ_NO_CAM_BUS = 128 PQ_ACC_FTS_EPB = 256 PQ_SNG_ECD = 512 + MLB_NO_ECAN = 1024 class VolkswagenFlags(IntFlag): @@ -331,10 +332,25 @@ class VolkswagenFlagsIQ(IntFlag): IQ_PQ_ACC_FTS_EPB = 1 << 18 # B7 TRW450: ACC FtS + EPB hold, Motor_1 resume spoof on bus 1 IQ_PQ_SNG_ECD = 1 << 19 IQ_PQ_TIMEBOMB = 1 << 20 + IQ_MLB_NO_ECAN = 1 << 21 + IQ_MLB_NO_HCA_EPS = 1 << 22 RADAR_DISABLE_STATE = {"error": False} +MLB_MSG_LH_EPS_03 = 0x9F +MLB_MSG_GETRIEBE_01 = 0x82 +MLB_MSG_GETRIEBE_02 = 0x83 +MLB_MSG_GETRIEBE_03 = 0x102 +MLB_MSG_GETRIEBE_04 = 0x441 +MLB_MSG_ACC_01 = 0x109 +MLB_MSG_ACC_05 = 0x10D +MLB_MSG_ACC_02 = 0x30C +MLB_MSG_ACC_10 = 0x117 + +MLB_ACC_COORDINATOR_MSGS = (MLB_MSG_ACC_01, MLB_MSG_ACC_05, MLB_MSG_ACC_02) +MLB_GEARBOX_MSGS = (MLB_MSG_GETRIEBE_01, MLB_MSG_GETRIEBE_02, MLB_MSG_GETRIEBE_03, MLB_MSG_GETRIEBE_04) + @dataclass class VolkswagenMLBPlatformConfig(PlatformConfig): @@ -671,6 +687,12 @@ class CAR(Platforms): chassis_codes={"8V", "FF"}, wmis={WMI.AUDI_GERMANY_CAR, WMI.AUDI_SPORT}, ) + AUDI_A4_MK4 = VolkswagenMLBPlatformConfig( + [VWCarDocs("Audi A4 2013-16", package="Cruise Control")], + VolkswagenCarSpecs(mass=1610, wheelbase=2.81, steerRatio=15.9), + chassis_codes={"8K", "FL"}, + wmis={WMI.AUDI_GERMANY_CAR}, + ) AUDI_Q2_MK1 = VolkswagenMQBPlatformConfig( [VWCarDocs("Audi Q2 2018")], VolkswagenCarSpecs(mass=1205, wheelbase=2.61), diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/modes/volkswagen_common.h b/artifacts/package_sources/iqdbc/iqdbc/safety/modes/volkswagen_common.h index cc92e65d7..d617e8aeb 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/modes/volkswagen_common.h +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/modes/volkswagen_common.h @@ -18,6 +18,8 @@ extern const uint16_t FLAG_VOLKSWAGEN_PQ_ACC_FTS_EPB; const uint16_t FLAG_VOLKSWAGEN_PQ_ACC_FTS_EPB = 256; extern const uint16_t FLAG_VOLKSWAGEN_PQ_SNG_ECD; const uint16_t FLAG_VOLKSWAGEN_PQ_SNG_ECD = 512; +extern const uint16_t FLAG_VOLKSWAGEN_MLB_NO_ECAN; +const uint16_t FLAG_VOLKSWAGEN_MLB_NO_ECAN = 1024; static uint8_t volkswagen_crc8_lut_8h2f[256]; // Static lookup table for CRC8 poly 0x2F, aka 8H2F/AUTOSAR diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/modes/volkswagen_mlb.h b/artifacts/package_sources/iqdbc/iqdbc/safety/modes/volkswagen_mlb.h index 8e862985a..1716c6c2b 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/modes/volkswagen_mlb.h +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/modes/volkswagen_mlb.h @@ -12,6 +12,8 @@ // so a rounding difference on the sending side cannot silently block the whole ACC_01 stream. #define VOLKSWAGEN_MLB_INACTIVE_ACCEL_TOLERANCE 15 +static bool volkswagen_mlb_no_ecan = false; + static bool volkswagen_mlb_long_accel_check(int desired_accel) { int inactive_delta = desired_accel - VW_IQ_INACTIVE_LONG_ACCEL; bool inactive = ((inactive_delta >= -VOLKSWAGEN_MLB_INACTIVE_ACCEL_TOLERANCE) && @@ -36,6 +38,9 @@ static safety_config volkswagen_mlb_init(uint16_t param) { static const CanMsg VOLKSWAGEN_MLB_LONG_TX_MSGS[] = {{MSG_HCA_01, 0, 8, .check_relay = true}, {MSG_LDW_02, 0, 8, .check_relay = true}, {MSG_ACC_01, 0, 8, .check_relay = true}, {MSG_ACC_02, 0, 8, .check_relay = true}}; + static const CanMsg VOLKSWAGEN_MLB_NO_ECAN_TX_MSGS[] = {{MSG_HCA_01, 1, 8, .check_relay = true}, {MSG_LDW_02, 1, 8, .check_relay = true}, + {MSG_LS_01, 1, 4, .check_relay = false}}; + static RxCheck volkswagen_mlb_rx_checks[] = { // TODO: implement checksum validation {.msg = {{MSG_ESP_03, 0, 8, 50U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, @@ -46,7 +51,17 @@ static safety_config volkswagen_mlb_init(uint16_t param) { {.msg = {{MSG_LS_01, 0, 4, 10U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, }; + static RxCheck volkswagen_mlb_no_ecan_rx_checks[] = { + {.msg = {{MSG_ESP_03, 1, 8, 50U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + {.msg = {{MSG_LH_EPS_03, 1, 8, 100U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + {.msg = {{MSG_ESP_05, 1, 8, 50U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + {.msg = {{MSG_TSK_02, 1, 8, 50U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + {.msg = {{MSG_MOTOR_03, 1, 8, 100U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + {.msg = {{MSG_LS_01, 1, 4, 10U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, + }; + volkswagen_common_init(); + volkswagen_mlb_no_ecan = GET_FLAG(param, FLAG_VOLKSWAGEN_MLB_NO_ECAN); #ifdef ALLOW_DEBUG volkswagen_longitudinal = GET_FLAG(param, FLAG_VOLKSWAGEN_LONG_CONTROL); @@ -56,11 +71,12 @@ static safety_config volkswagen_mlb_init(uint16_t param) { #endif return volkswagen_longitudinal ? BUILD_SAFETY_CFG(volkswagen_mlb_rx_checks, VOLKSWAGEN_MLB_LONG_TX_MSGS) : \ - BUILD_SAFETY_CFG(volkswagen_mlb_rx_checks, VOLKSWAGEN_MLB_STOCK_TX_MSGS); + volkswagen_mlb_no_ecan ? BUILD_SAFETY_CFG(volkswagen_mlb_no_ecan_rx_checks, VOLKSWAGEN_MLB_NO_ECAN_TX_MSGS) : \ + BUILD_SAFETY_CFG(volkswagen_mlb_rx_checks, VOLKSWAGEN_MLB_STOCK_TX_MSGS); } static void volkswagen_mlb_rx_hook(const CANPacket_t *msg) { - if (msg->bus == 0U) { + if (msg->bus == (volkswagen_mlb_no_ecan ? 1U : 0U)) { // Check all wheel speeds for any movement // Signals: ESP_03.ESP_[VL|VR|HL|HR]_Radgeschw if (msg->addr == MSG_ESP_03) { diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_volkswagen_mlb.py b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_volkswagen_mlb.py index af395a2f9..78b2ad4b4 100755 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_volkswagen_mlb.py +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_volkswagen_mlb.py @@ -216,5 +216,72 @@ class TestVolkswagenMlbLongSafety(TestVolkswagenMlbSafetyBase): self.assertTrue(self._tx(self._acc_01_msg(0.5))) +class TestVolkswagenMlbNoEcanSafety(TestVolkswagenMlbSafetyBase): + TX_MSGS = [[MSG_HCA_01, 1], [MSG_LDW_02, 1], [MSG_LS_01, 1]] + FWD_BLACKLISTED_ADDRS = {} + FWD_BUS_LOOKUP = {0: 2, 2: 0} + RELAY_MALFUNCTION_ADDRS = {1: (MSG_HCA_01, MSG_LDW_02)} + + def setUp(self): + self.packer = CANPackerSafety("vw_mlb") + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.volkswagenMlb, VolkswagenSafetyFlags.MLB_NO_ECAN) + self.safety.init_tests() + + def _speed_msg(self, speed): + values = {"ESP_%s_Radgeschw" % s: speed for s in ["HL", "HR", "VL", "VR"]} + return self.packer.make_can_msg_safety("ESP_03", 1, values) + + def _esp_05_msg(self, brake): + values = {"ESP_Fahrer_bremst": brake} + return self.packer.make_can_msg_safety("ESP_05", 1, values) + + def _motor_03_msg(self, brake_signal=False, gas_signal=0): + values = {"MO_BLS": brake_signal, "MO_Fahrpedalrohwert_01": gas_signal} + return self.packer.make_can_msg_safety("Motor_03", 1, values) + + def _torque_driver_msg(self, torque): + values = {"EPS_Lenkmoment": abs(torque), "EPS_VZ_Lenkmoment": torque < 0} + return self.packer.make_can_msg_safety("LH_EPS_03", 1, values) + + def _torque_cmd_msg(self, torque, steer_req=1): + values = {"HCA_01_LM_Offset": abs(torque), + "HCA_01_LM_OffSign": torque < 0, + "HCA_01_Sendestatus": steer_req, + "HCA_01_Status_HCA": 7 if steer_req else 3} + return self.packer.make_can_msg_safety("HCA_01", 1, values) + + def _ls_01_msg(self, cancel=0, resume=0, _set=0, main_switch=1, bus=1): + values = {"LS_Abbrechen": cancel, "LS_Tip_Setzen": _set, "LS_Tip_Wiederaufnahme": resume, + "LS_Hauptschalter": main_switch} + return self.packer.make_can_msg_safety("LS_01", bus, values) + + def _tsk_status_msg(self, enable, main_switch=True): + values = {"TSK_Status": 1 if enable else 0 if main_switch else 3} + return self.packer.make_can_msg_safety("TSK_02", 1, values) + + def test_spam_cancel_safety_check(self): + self.safety.set_controls_allowed(0) + self.assertTrue(self._tx(self._ls_01_msg(cancel=1))) + self.assertFalse(self._tx(self._ls_01_msg(resume=1))) + self.assertFalse(self._tx(self._ls_01_msg(_set=1))) + self.safety.set_controls_allowed(1) + self.assertTrue(self._tx(self._ls_01_msg(resume=1))) + + def test_cancel_button(self): + self._rx(self._tsk_status_msg(False, main_switch=True)) + self.safety.set_controls_allowed(1) + self._rx(self._ls_01_msg(cancel=True)) + self.assertFalse(self.safety.get_controls_allowed(), "controls allowed after cancel") + + def test_ecan_bus_is_ignored(self): + self._rx(self._torque_driver_msg(0)) + self.safety.set_controls_allowed(1) + self._rx(self.packer.make_can_msg_safety("LS_01", 0, {"LS_Abbrechen": 1, "LS_Hauptschalter": 1})) + self.assertTrue(self.safety.get_controls_allowed(), "bus 0 cancel disengaged a no-ECAN car") + self._rx(self.packer.make_can_msg_safety("LH_EPS_03", 0, {"EPS_Lenkmoment": 200, "EPS_VZ_Lenkmoment": 0})) + self.assertEqual(0, self.safety.get_torque_driver_max(), "bus 0 EPS torque reached the driver sample") + + if __name__ == "__main__": unittest.main() diff --git a/iqpilot/iq_maps/orchestrator.py b/iqpilot/iq_maps/orchestrator.py index f4660349b..14623a217 100755 --- a/iqpilot/iq_maps/orchestrator.py +++ b/iqpilot/iq_maps/orchestrator.py @@ -26,25 +26,9 @@ from iqpilot.iq_maps.vendor_mapd_installer import VendorMapdInstaller OfflineMapAction = custom.MapdInputType _region_sync_worker: threading.Thread | None = None -# mapd_manager only runs offroad (process_config.only_offroad) and the onroad -# NativeProcess("mapd", ...) is started the instant `started` flips True. If a -# vendor-map download is in flight at that exact moment, the two `mapd` -# binaries end up pointed at the same Paths.mapd_root() tile directory at the -# same time: this one still downloading/writing, the onroad one already -# mmap-reading. Manager only sends SIGINT/SIGTERM to stop mapd_manager, which -# by default only interrupts the main thread — the background download thread -# and the vendor `mapd` subprocess it spawned are otherwise orphaned and keep -# writing into the tile directory the onroad reader just opened, which is what -# was segfaulting (-12) the onroad process in a tight restart loop. The lock + -# pidfile below make sure that subprocess is always killed (on clean shutdown -# via the signal handlers, and on the next boot if this process itself got -# SIGKILLed) before anything else is allowed to read the tile directory. _active_proc_lock = threading.Lock() _active_proc: subprocess.Popen | None = None _shutdown = threading.Event() -# Display-tile bundles for the offline on-screen map (separate asset from mapd's routing -# data). Downloaded after the mapd fetch in the same worker so a region selection installs -# both, and independently restorable when only the tile bundle is missing. _tile_downloader: TileBundleDownloader | None = None @@ -62,7 +46,6 @@ def _pid_is_vendor_fetch(pid: int) -> bool: def _reap_orphaned_vendor_fetch() -> None: - """Kill any vendor-fetch mapd subprocess left running from a prior, uncleanly-terminated run.""" pidfile = _vendor_fetch_pidfile() try: with open(pidfile) as f: @@ -122,8 +105,6 @@ def _install_signal_handlers() -> None: def ensure_vendor_runtime() -> None: - # verify-only: a hash-mismatched binary is quarantined, never replaced from - # the network — the updater restores the checked-in one try: VendorMapdInstaller().verify() except Exception: @@ -173,10 +154,6 @@ def _compose_region_selector(nations: list[str], states: list[str] | None = None def _fetch_tile_bundles(region_selector: str, abort_check=None) -> None: - """Download the offline on-screen map display tiles for the selected regions. - - Separate asset from mapd's routing data: the on-screen map's OsmOfflineProvider reads - raster .mbtiles bundles, so a region selection installs both when OfflineOSMaps is on.""" global _tile_downloader if not params.get_bool("OfflineOSMaps"): return @@ -239,8 +216,6 @@ def _drive_vendor_fetch(region_selector: str, requested_regions: dict) -> None: break cloudlog.info(f"iq_maps: vendor map download finished for {region_selector}") if not cancelled and not _shutdown.is_set(): - # OSMDownloadLocations stays set until the finally below, so the konn3kt cancel RPC - # (which removes it) aborts the tile phase exactly like it cancels the mapd phase. _fetch_tile_bundles(region_selector, abort_check=lambda: _shutdown.is_set() or not mem_params.get("OSMDownloadLocations")) except Exception: cloudlog.exception("iq_maps: vendor map download failed") @@ -304,17 +279,12 @@ _last_auto_restore_t = 0.0 def region_data_missing() -> bool: - # a media wipe (reflash/format) can delete the downloaded region while the params - # that configure offline maps survive; mapd then retries the missing files forever - # and nothing re-downloads (stale_region_artifacts only sees leftover files) if not params.get_bool("OsmLocal"): return False if not params.get("OsmDownloadedDate"): return False if glob.glob(f"{Paths.mapd_root()}/db") or glob.glob(f"{Paths.mapd_root()}/v*"): return False - # mapd v2 stores region tiles under offline//.tar.gz — without this - # check a v2 install looks perpetually wiped and re-downloads every backoff interval if glob.glob(f"{Paths.mapd_root()}/offline/*/*"): return False country = params.get("OsmLocationName", return_default=True) @@ -322,8 +292,6 @@ def region_data_missing() -> bool: def configured_states() -> list[str]: - """Selected US states: OsmStateNames (JSON list, multi-state) wins; the legacy - single OsmStateName remains the fallback for pre-list configs.""" try: states = params.get("OsmStateNames") if isinstance(states, bytes): @@ -370,8 +338,6 @@ def _configured_region_selector() -> str: def tile_bundles_missing() -> bool: - # covers a media wipe AND the user enabling OfflineOSMaps after the region download - # already ran (the vendor fetch only pulls tile bundles when the toggle is on) if not params.get_bool("OfflineOSMaps"): return False selector = _configured_region_selector() @@ -381,8 +347,6 @@ def tile_bundles_missing() -> bool: def maybe_restore_tile_bundles() -> None: - """Tile-only download: don't re-run the whole mapd vendor fetch when only the display - tiles are missing.""" global _last_tile_restore_t, _tile_only_worker if not tile_bundles_missing(): return @@ -410,9 +374,6 @@ def sync_osm_request_flags() -> None: maybe_restore_tile_bundles() if params.get_bool("OsmDbUpdatesCheck"): if _region_sync_worker is not None and _region_sync_worker.is_alive(): - # A download is already writing into Paths.mapd_root() - deleting/rewriting - # files under it right now would race the writer (and any onroad mapd - # reader) the same way the orphaned-subprocess bug did. Wait for it to finish. return purge_stale_region_artifacts(stale_region_artifacts()) country = params.get("OsmLocationName", return_default=True) @@ -439,11 +400,6 @@ def run_loop(): pass except PermissionError: cloudlog.exception(f"iq_maps: failed to make {Paths.mapd_root()}") - - # A prior run that got SIGKILLed (or crashed) may have left its vendor-fetch - # mapd subprocess running and still writing into Paths.mapd_root(); clear it - # before anything (including the onroad mapd, once `started` flips) reads - # from that directory. Signal handlers cover the graceful-shutdown path. _reap_orphaned_vendor_fetch() _install_signal_handlers() diff --git a/iqpilot/iq_maps/tile_bundle_downloader.py b/iqpilot/iq_maps/tile_bundle_downloader.py index f440b4c9a..7dbc82e70 100644 --- a/iqpilot/iq_maps/tile_bundle_downloader.py +++ b/iqpilot/iq_maps/tile_bundle_downloader.py @@ -1,20 +1,6 @@ #!/usr/bin/env python3 """ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos - -Downloads per-region raster display-tile bundles (.mbtiles) for the offline on-screen map. - -These are a separate asset from mapd's routing/speed-limit data: mapd pulls OSM way tiles -into Paths.mapd_root(), while the on-screen map (OsmOfflineProvider) reads raster .mbtiles -from offline_map_root()/regions//tiles/offline.mbtiles. Bundles are built per -state/nation by scripts/iqpilot/build_state_tile_bundles.py and hosted behind a static base -URL that serves: - - /index.json {"version": 1, "regions": {: entry}} - / the raster .mbtiles for that region - -Entry fields: path, bytes, sha256, bounds ("minLon,minLat,maxLon,maxLat"), minzoom, maxzoom. -Selectors match the mapd region menu naming: us_state.CA, nation.US. """ import hashlib import json @@ -29,16 +15,12 @@ from iqpilot.common.params import Params from iqpilot.common.swaglog import cloudlog from iqpilot.ui.onroad.offline_tiles import offline_map_root -# Proprietary auth + hosted endpoints (gitea raw with an embedded read-only PAT, same -# pattern as the model selector). Optional: without the private bundle the downloader -# still works anonymously against OfflineTilesBaseUrl (e.g. a public R2 bucket). try: from iqpilot.iq_maps.tiles_auth import get_base_urls as _private_base_urls, get_requests_auth as _private_auth except Exception: # ProprietaryModuleMissing or import errors in stripped builds _private_base_urls = None _private_auth = None -# R2 bucket iqnav behind the public custom domain (see scripts/iqpilot/tile_factory/r2_sync_watch.py) DEFAULT_TILE_BUNDLE_BASE_URL = "https://maps.konn3kt.com/iqosmd/v1" BASE_URL_PARAM = "OfflineTilesBaseUrl" PROGRESS_PARAM = "OfflineTilesDownloadProgress" @@ -49,8 +31,6 @@ STREAM_RETRIES = 8 def candidate_base_urls(params: Params) -> list[str]: - """Hosts to try in order: user/param override first, then the embedded private - endpoints (gitea raw), then the public default.""" override = params.get(BASE_URL_PARAM) if isinstance(override, bytes): override = override.decode("utf-8", errors="ignore") @@ -144,12 +124,6 @@ def _write_manifest(selector: str, entry: dict) -> None: class TileBundleDownloader: - """Streams region bundles to disk with resume + sha256 verify + atomic install. - - Cancellation matches the mapd flow: the caller sets REQUEST_PARAM in mem params while a - download runs; removing it (konn3kt cancel RPC or settings) aborts between chunks. The - partial .part file is kept so a retry resumes instead of restarting. - """ def __init__(self, params: Params | None = None, mem_params: Params | None = None, abort_check=None): @@ -160,7 +134,6 @@ class TileBundleDownloader: self.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else self.params self.session = requests.Session() self._cancelled = threading.Event() - # optional external cancel signal, e.g. the orchestrator's OSMDownloadLocations removal self._abort_check = abort_check def cancel(self) -> None: @@ -170,7 +143,6 @@ class TileBundleDownloader: if self._cancelled.is_set(): return True if not self.mem_params.get(REQUEST_PARAM): - # request flag was removed out from under us -> user cancelled self._cancelled.set() return True if self._abort_check is not None and self._abort_check(): @@ -188,7 +160,6 @@ class TileBundleDownloader: def _download_one(self, selector: str, entry: dict, base_url: str, progress_offset: int, progress_total: int) -> bool: - """Download a region: the night bundle, plus the optional day-style variant.""" night_path = region_bundle_path(selector) ok = self._download_file( selector, base_url, entry["path"], int(entry.get("bytes", 0)), @@ -205,9 +176,7 @@ class TileBundleDownloader: progress_offset + int(entry.get("bytes", 0)), progress_total, ) if not day_ok: - # the night set is complete and usable; a failed day variant retries next pass cloudlog.warning(f"iq_maps: day-style bundle failed for {selector}; night set installed") - # manifest last: bounds drive region matching, so it must describe installed files _write_manifest(selector, entry) cloudlog.info(f"iq_maps: installed tile bundle {selector}") return True @@ -219,8 +188,6 @@ class TileBundleDownloader: part_path = final_path.with_name(final_path.name + ".part") part_path.parent.mkdir(parents=True, exist_ok=True) - # A cellular/hotspot link routinely kills a multi-hundred-MB stream mid-flight; retry - # each interruption from the current .part offset instead of failing the whole region. downloaded = 0 digest = hashlib.sha256() last_error: Exception | None = None @@ -244,7 +211,6 @@ class TileBundleDownloader: auth = request_auth() response = self.session.get(url, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S, auth=auth) if resume_from and response.status_code != 206: - # server ignored the Range request -> restart from scratch digest = hashlib.sha256() resume_from = 0 part_path.unlink(missing_ok=True) @@ -284,8 +250,6 @@ class TileBundleDownloader: return True def download_regions(self, selectors: list[str]) -> bool: - """Download the display-tile bundles for the given region selectors. Returns True if all - requested bundles are installed and current when done.""" self._cancelled.clear() ok = True try: @@ -346,7 +310,6 @@ class TileBundleDownloader: if not expected_sha or installed_sha != expected_sha: return False if entry.get("day_path"): - # a published day variant must be installed and current too day_file = region_bundle_dir(selector) / "tiles" / "offline_day.mbtiles" installed_day = str(manifest.get("mbtiles_day", {}).get("sha256", "")).strip().lower() expected_day = str(entry.get("day_sha256", "")).strip().lower() diff --git a/iqpilot/iq_maps/tiles_auth.py b/iqpilot/iq_maps/tiles_auth.py index 89d357d4f..4866a1b58 100644 --- a/iqpilot/iq_maps/tiles_auth.py +++ b/iqpilot/iq_maps/tiles_auth.py @@ -7,4 +7,4 @@ from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_m try: load_private_module(__name__, "iqpilot_private.maps.git_auth") except ProprietaryModuleMissing: - from iqpilot.maps_private_src.git_auth import * # noqa: F403 + from iqpilot.maps_private_src.git_auth import * diff --git a/iqpilot/iq_maps/update_vendor_version.py b/iqpilot/iq_maps/update_vendor_version.py index 12a1a619f..bbd9582fe 100755 --- a/iqpilot/iq_maps/update_vendor_version.py +++ b/iqpilot/iq_maps/update_vendor_version.py @@ -1,11 +1,6 @@ #!/usr/bin/env python3 """ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos - -Maintainer utility: pin a new mapd release tag and refresh the checked-in binary -hash. Not used at runtime. Binaries come from the gitlvb teal/mapd CI (built -against teal/gomsgq) — drop the artifact at third_party/mapd_pfeiferj/mapd, then -run this so the hash pin moves in the same commit. """ import argparse import os @@ -21,7 +16,6 @@ from iqpilot.iq_maps.vendor_mapd_installer import ( _RELEASE_SYMBOL = "VENDOR_RELEASE_TAG" _INSTALLER_SRC = os.path.join(BASEDIR, "iqpilot", "iq_maps", "vendor_mapd_installer.py") -# public: the checked-in hash the version test compares the installed binary against HASH_FILE = os.path.join(BASEDIR, "iqpilot", "iq_maps", "tests", "mapd_hash") _HASH_FILE = HASH_FILE _TAG_ASSIGN = re.compile(rf'^{_RELEASE_SYMBOL}\s*=\s*["\'][^"\']*["\']', re.MULTILINE) diff --git a/iqpilot/iq_maps/vendor_mapd_installer.py b/iqpilot/iq_maps/vendor_mapd_installer.py index e64038743..dc00b63be 100755 --- a/iqpilot/iq_maps/vendor_mapd_installer.py +++ b/iqpilot/iq_maps/vendor_mapd_installer.py @@ -1,14 +1,6 @@ #!/usr/bin/env python3 """ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos - -Verifies the vendored `mapd` routing binary authored by Jacob Pfeifer -(github.com/pfeiferj/mapd), built from the gitlvb teal/mapd fork against -teal/gomsgq. The only accepted binary is the checked-in one matching the pinned -hash; nothing is ever downloaded at runtime. Jacob's stock release build embeds -a 15-reader msgq header layout — on this fork (NUM_READERS=32) its registration -writes land inside other processes' reader slots, so a wrong binary is -quarantined rather than left where manager could start it. """ import hashlib import os @@ -29,7 +21,6 @@ QUARANTINE_PATH = VENDOR_MAPD_PATH + ".quarantined" def sha256_of_file(path: str) -> str: - """Hex SHA-256 digest of a file on disk.""" digest = hashlib.sha256() with open(path, "rb") as handle: for block in iter(lambda: handle.read(1 << 20), b""): @@ -50,14 +41,12 @@ class VendorMapdInstaller: return str(self._params.get(_VERSION_PARAM) or "") def verify(self) -> bool: - """True iff the on-disk binary matches the pinned hash; quarantines a wrong one.""" expected = self._expected_hash() if not expected: cloudlog.error("iq_maps: pinned mapd hash missing, vendor binary cannot be verified") return False if not os.path.isfile(VENDOR_MAPD_PATH): - # the binary is a tracked file: the updater/bundle restores it self._say("Offline maps engine missing; it will be restored by the next update.") self._params.remove(_VERSION_PARAM) return False @@ -77,8 +66,6 @@ class VendorMapdInstaller: self._say(f"Offline maps engine verified [{VENDOR_RELEASE_TAG}]") return True - # a foreign binary — e.g. a stock release download from the retired fetch - # path — must never run: quarantine it where manager can't start it cloudlog.error(f"iq_maps: vendor mapd hash {current[:12]} != pinned {expected[:12]}, quarantining") self._say("Offline maps engine failed verification; quarantined until the next update.") try: diff --git a/iqpilot/selfdrive/car/gap_button_actions.py b/iqpilot/selfdrive/car/gap_button_actions.py index 714b5a050..3454b6851 100644 --- a/iqpilot/selfdrive/car/gap_button_actions.py +++ b/iqpilot/selfdrive/car/gap_button_actions.py @@ -1,9 +1,5 @@ """ 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 iqpilot.cereal import car, custom from iqdbc.car import structs diff --git a/iqpilot/selfdrive/car/vehicle_catalog.json b/iqpilot/selfdrive/car/vehicle_catalog.json index 77162eebb..b3fdefead 100644 --- a/iqpilot/selfdrive/car/vehicle_catalog.json +++ b/iqpilot/selfdrive/car/vehicle_catalog.json @@ -114,6 +114,20 @@ ], "req": "Adaptive Cruise Control (ACC) & Lane Assist" }, + "AUDI_A4_MK4|2013-2016": { + "label": "Audi A4 2013-16", + "id": "AUDI_A4_MK4", + "mk": "Audi", + "grp": "volkswagen", + "mdl": "A4", + "yrs": [ + "2013", + "2014", + "2015", + "2016" + ], + "req": "Cruise Control" + }, "AUDI_Q2_MK1|2018": { "label": "Audi Q2 2018", "id": "AUDI_Q2_MK1", diff --git a/iqpilot/selfdrive/iqmodeld/daemon.py b/iqpilot/selfdrive/iqmodeld/daemon.py index 23bdfb7e2..c038c4bbb 100755 --- a/iqpilot/selfdrive/iqmodeld/daemon.py +++ b/iqpilot/selfdrive/iqmodeld/daemon.py @@ -63,6 +63,9 @@ IQP_NAV_MODEL_INFLUENCE_ENABLED = False TurnDirection = custom.IQTurnSignalDirection IQMODEL_EVAL_WARN_US = int(DT_MDL * 1_000_000) IQMODEL_EVAL_ERROR_US = IQMODEL_EVAL_WARN_US * 2 +_FRAME_STARVED_BACKOFF_POLLS = 5 +_FRAME_STARVED_BACKOFF_SECONDS = 0.005 +_FRAME_STARVED_LOG_EVERY = 200 def _plan_y_std_1s(outputs: dict[str, np.ndarray]) -> float: @@ -605,12 +608,21 @@ class InferenceDaemon: def serve(self) -> None: tick = 0 + starved_polls = 0 while True: frame_pair = self._cameras.pull() if frame_pair is None: - cloudlog.debug("visionipc frame missing") + starved_polls += 1 + if starved_polls >= _FRAME_STARVED_BACKOFF_POLLS: + time.sleep(_FRAME_STARVED_BACKOFF_SECONDS) + if starved_polls % _FRAME_STARVED_LOG_EVERY == 0: + cloudlog.error(f"visionipc delivered no frames for {starved_polls} polls; model is not running") continue + if starved_polls: + cloudlog.warning(f"visionipc recovered after {starved_polls} frameless polls") + starved_polls = 0 + main_buf, extra_buf, main_stamp, extra_stamp = frame_pair self._sub.update(0) self._refresh_tunables(tick) diff --git a/iqpilot/selfdrive/iqmodeld/models/__init__.py b/iqpilot/selfdrive/iqmodeld/models/__init__.py index c7d1e92cc..5ece1c074 100644 --- a/iqpilot/selfdrive/iqmodeld/models/__init__.py +++ b/iqpilot/selfdrive/iqmodeld/models/__init__.py @@ -1,3 +1,3 @@ """ -IQ model selection and runner support that is actively used by iqmodeld. +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ """ diff --git a/iqpilot/selfdrive/iqmodeld/models/fetcher.py b/iqpilot/selfdrive/iqmodeld/models/fetcher.py index 8ce6aff59..d06c7c3dd 100644 --- a/iqpilot/selfdrive/iqmodeld/models/fetcher.py +++ b/iqpilot/selfdrive/iqmodeld/models/fetcher.py @@ -1,11 +1,8 @@ #!/usr/bin/env python3 """ -Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos - -Public entry point for the model-manifest fetcher: prefers the compiled private -bundle, falling back to the in-tree source. The default-runner fallback lives in -ManifestDecoder now, so no post-import patching is needed. +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ """ + from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module try: diff --git a/iqpilot/selfdrive/iqmodeld/models/helpers.py b/iqpilot/selfdrive/iqmodeld/models/helpers.py index 191d8a1b1..186c9d623 100644 --- a/iqpilot/selfdrive/iqmodeld/models/helpers.py +++ b/iqpilot/selfdrive/iqmodeld/models/helpers.py @@ -40,7 +40,6 @@ _DEFAULT_BUNDLE_REF = "default" def get_default_model_bundle(_bundles): - """Legacy compatibility hook: stock default is preinstalled, not a manifest bundle.""" return None @@ -239,10 +238,13 @@ def select_default_model(params: Params = None) -> None: def seed_default_bundle_if_unset(params: Params = None) -> None: params = Params() if params is None else params - if params.get(_ACTIVE_BUNDLE_KEY) or params.get(_DOWNLOAD_INDEX_KEY) is not None: + if params.get(_ACTIVE_BUNDLE_KEY): return + queued_download = params.get(_DOWNLOAD_INDEX_KEY) try: select_default_model(params) + if queued_download is not None: + params.put(_DOWNLOAD_INDEX_KEY, queued_download) cloudlog.warning("default_model: seeded Default (CD210) as active bundle") except Exception as e: cloudlog.exception(f"default_model: failed to seed default bundle: {e}") diff --git a/iqpilot/selfdrive/iqmodeld/models/inference_state.py b/iqpilot/selfdrive/iqmodeld/models/inference_state.py index 8f9625f5b..1bc1a254a 100644 --- a/iqpilot/selfdrive/iqmodeld/models/inference_state.py +++ b/iqpilot/selfdrive/iqmodeld/models/inference_state.py @@ -1,9 +1,5 @@ """ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos - -Common base for the per-process inference/runtime states. It seeds the lateral -steer delay from the cached learned value so every subclass starts with a usable -number before its first lateralDelay message arrives. """ from iqpilot.common.steer_delay import cached_steer_delay diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/__init__.py b/iqpilot/selfdrive/iqmodeld/models/runners/__init__.py index a4189be90..5ece1c074 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/__init__.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/__init__.py @@ -1,3 +1,3 @@ """ -Runner interfaces used by iqmodeld model execution. +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ """ diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/__init__.py b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/__init__.py index 0d071a62d..9129d3486 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/__init__.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/__init__.py @@ -1,3 +1,3 @@ """ -Tinygrad runner support for iqmodeld. -""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" \ No newline at end of file diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py index 736fd5f7a..33beea423 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py @@ -27,8 +27,6 @@ WARP_DEV = os.getenv('WARP_DEV') class TinygradFusedRunner(ModelRunner): - """Runs a fused warp+vision+policy pkl. Bundle ships one `driving_fused_*` artifact.""" - uses_opencl_warp: bool = False def __init__(self): @@ -110,7 +108,6 @@ class TinygradFusedRunner(ModelRunner): 'feat_q': zeros_f32((self._frame_skip * (fb[1] - 1) + 1, fb[0], fb[2])), 'desire_q': zeros_f32((self._frame_skip * dp[1], dp[0], dp[2])), } - # shapes must match the captured run_policy JIT inputs on_shapes = self._on_meta['input_shapes'] captured = self._run_policy.captured jit_shapes = { @@ -135,7 +132,6 @@ class TinygradFusedRunner(ModelRunner): self._cam_resolution = (cam_w, cam_h) def run_fused(self, bufs: dict, transforms: dict[str, np.ndarray], numpy_inputs: NumpyDict) -> NumpyDict: - """warp + vision + policy in one pass from raw NV12 bufs + transform matrices.""" Tensor, Device = _tinygrad_imports() main_buf = bufs['img'] @@ -154,7 +150,6 @@ class TinygradFusedRunner(ModelRunner): npy = lambda key: Tensor(self._npy_buffers[key], device='NPY') - # frames go on the compute device to match the captured warp JIT frame = self._frame_tensor('img', bufs['img']) big_frame = self._frame_tensor('big_img', bufs['big_img']) @@ -169,8 +164,6 @@ class TinygradFusedRunner(ModelRunner): policy_inputs['action_t'] = npy('action_t') vision_out_t, on_out_t, off_out_t = self._run_policy(**policy_inputs) - # parse each model's output on its own sliced dict; parsing a merged dict - # would run parse_dynamic_outputs twice and double-parse plan/lead def _slice(tensor_out, meta) -> NumpyDict: flat = tensor_out.numpy().flatten() return {k: flat[np.newaxis, sl] for k, sl in meta['output_slices'].items() if k != 'pad'} diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py index 09e891d3e..ccd4a5b1e 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py @@ -68,8 +68,6 @@ def _is_jit_arg_mismatch(err: BaseException) -> bool: class TinygradSupercomboRunner(ModelRunner): - """Runs a single combined supercombo pkl. Bundle ships one `driving_supercombo_*` artifact.""" - uses_opencl_warp: bool = False def __init__(self): @@ -282,7 +280,6 @@ class TinygradSupercomboRunner(ModelRunner): zeros_u8 = lambda s: Tensor(np.zeros(s, dtype=np.uint8), device=Device.DEFAULT).contiguous().realize() zeros_f32 = lambda s: Tensor(np.zeros(s, dtype=np.float32), device=Device.DEFAULT).contiguous().realize() - # packed npy block (single NPY tensor, mutated in place via views): order matches run_policy.split shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])} sizes = [math.prod(s) for s in shapes.values()] packed = np.zeros(sum(sizes), dtype=np.float32) @@ -318,7 +315,6 @@ class TinygradSupercomboRunner(ModelRunner): self._npy['traffic_convention'][:] = numpy_inputs['traffic_convention'] if 'action_t' in numpy_inputs: self._npy['action_t'][:] = numpy_inputs['action_t'] - # self._npy['prev_feat'] holds last frame's hidden_state (zeros on the first frame) frame = self._frame_tensor('img', bufs['img']) big_frame = self._frame_tensor('big_img', bufs['big_img']) @@ -334,11 +330,10 @@ class TinygradSupercomboRunner(ModelRunner): raise flat = out.numpy().flatten() - # feed hidden_state back as prev_feat for the next frame self._npy['prev_feat'][:] = flat[self._hidden_slice].reshape(self._npy['prev_feat'].shape) sliced = {k: flat[np.newaxis, sl] for k, sl in self._slices.items()} - return self._parser.parse_vision_outputs(sliced) # single-pass; parse_outputs double-parses a combined dict + return self._parser.parse_vision_outputs(sliced) def _run_model(self) -> NumpyDict: raise RuntimeError("supercombo path goes through run_fused(), not _run_model()") diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py index 449338148..2d91c3df1 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py @@ -85,8 +85,6 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny self.model_run = _load_program_blob(asset_name) self._input_plan = _compile_input_plan(self.model_run.captured) - # the warp pipeline hands the runner raw uint8 YUV; a float image interface - # would silently reinterpret those bytes and drive on garbage vision for name, spec in self._input_plan.items(): if "img" in name and spec.dtype is not dtypes.uint8: raise ValueError(f"{asset_name}: image input {name} expects {spec.dtype}, incompatible with uint8 warp buffer") diff --git a/iqpilot/selfdrive/iqmodeld/models/split_model_constants.py b/iqpilot/selfdrive/iqmodeld/models/split_model_constants.py index a088fe755..3f83ea806 100644 --- a/iqpilot/selfdrive/iqmodeld/models/split_model_constants.py +++ b/iqpilot/selfdrive/iqmodeld/models/split_model_constants.py @@ -1,4 +1,3 @@ -# openpilot model I/O constants (comma.ai, MIT — see LICENSE) import numpy as np @@ -7,7 +6,6 @@ def index_function(idx, max_val=192, max_idx=32): class SplitModelConstants: - # time and distance indices IDX_N = 33 T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)] @@ -15,7 +13,6 @@ class SplitModelConstants: LEAD_T_OFFSETS = [0., 2., 4.] META_T_IDXS = [2., 4., 6., 8., 10.] - # split-model temporal / history run parameters MODEL_FREQ = 20 HISTORY_FREQ = 5 HISTORY_LEN_SECONDS = 5 @@ -31,7 +28,6 @@ class SplitModelConstants: LATERAL_CONTROL_PARAMS_LEN = 2 PREV_DESIRED_CURV_LEN = 1 - # model outputs constants FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) FCW_5MS2_PROBS_WIDTH = 5 @@ -71,7 +67,6 @@ class SplitModelConstants: POLY_PATH_DEGREE = 4 -# model outputs slices class Plan: POSITION = slice(0, 3) VELOCITY = slice(3, 6) @@ -82,14 +77,12 @@ class Plan: class Meta: ENGAGED = slice(0, 1) - # next 2, 4, 6, 8, 10 seconds GAS_DISENGAGE = slice(1, 31, 6) BRAKE_DISENGAGE = slice(2, 31, 6) STEER_OVERRIDE = slice(3, 31, 6) HARD_BRAKE_3 = slice(4, 31, 6) HARD_BRAKE_4 = slice(5, 31, 6) HARD_BRAKE_5 = slice(6, 31, 6) - # next 0, 2, 4, 6, 8, 10 seconds GAS_PRESS = slice(31, 55, 4) BRAKE_PRESS = slice(32, 55, 4) LEFT_BLINKER = slice(33, 55, 4) diff --git a/iqpilot/selfdrive/iqmodeld/tests/__init__.py b/iqpilot/selfdrive/iqmodeld/tests/__init__.py index e69de29bb..9129d3486 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/__init__.py +++ b/iqpilot/selfdrive/iqmodeld/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" \ No newline at end of file diff --git a/iqpilot/selfdrive/iqmodeld/tests/dmon_lag/repro.cc b/iqpilot/selfdrive/iqmodeld/tests/dmon_lag/repro.cc index e43a140b8..70416a69a 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/dmon_lag/repro.cc +++ b/iqpilot/selfdrive/iqmodeld/tests/dmon_lag/repro.cc @@ -1,5 +1,4 @@ // Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ -// clang++ -O2 repro.cc && ./a.out #include #include diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_action_dispatch.py b/iqpilot/selfdrive/iqmodeld/tests/test_action_dispatch.py index f84228146..047108791 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_action_dispatch.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_action_dispatch.py @@ -1,3 +1,6 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" from __future__ import annotations from types import SimpleNamespace diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py b/iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py index c826590ff..a45af3328 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_combined_split_runner.py @@ -1,3 +1,6 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" from __future__ import annotations from dataclasses import dataclass diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_compile_supercombo_guards.py b/iqpilot/selfdrive/iqmodeld/tests/test_compile_supercombo_guards.py index d529f8a48..5eb3d59fd 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_compile_supercombo_guards.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_compile_supercombo_guards.py @@ -1,3 +1,6 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" from __future__ import annotations import numpy as np diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_iqmodeld_contracts.py b/iqpilot/selfdrive/iqmodeld/tests/test_iqmodeld_contracts.py index d1ba108ce..89241c6ad 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_iqmodeld_contracts.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_iqmodeld_contracts.py @@ -1,3 +1,6 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" from __future__ import annotations import copy diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_model_runner_smoke.py b/iqpilot/selfdrive/iqmodeld/tests/test_model_runner_smoke.py index 32f03aab5..66631a82a 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_model_runner_smoke.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_model_runner_smoke.py @@ -1,3 +1,6 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" from __future__ import annotations from dataclasses import dataclass diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_public_surface.py b/iqpilot/selfdrive/iqmodeld/tests/test_public_surface.py index 8a4eb1977..474b49901 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_public_surface.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_public_surface.py @@ -1,3 +1,6 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" from iqpilot.selfdrive.iqmodeld import metadata, messaging, parser from iqpilot.selfdrive.iqmodeld.daemon import CaptureStamp, NeuralEngineState diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_selector_share_smoke.py b/iqpilot/selfdrive/iqmodeld/tests/test_selector_share_smoke.py index 4f164b0c6..cfcc4d8f8 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_selector_share_smoke.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_selector_share_smoke.py @@ -1,3 +1,6 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" from __future__ import annotations import os @@ -5,7 +8,6 @@ from dataclasses import dataclass from pathlib import Path import numpy as np -import pytest from tinygrad.nn.onnx import OnnxRunner from tinygrad.tensor import Tensor @@ -123,8 +125,9 @@ def _run_onnx_bundle(bundle_dir: Path): ) -@pytest.mark.skipif(not SHARE_ROOT.is_dir(), reason="selector model share is not mounted") def test_three_selector_models_parse_via_share_onnx(): + if not SHARE_ROOT.is_dir(): + return selector_dirs = _find_selector_dirs(limit=3, require_onnx=True) assert len(selector_dirs) >= 3 @@ -134,8 +137,9 @@ def test_three_selector_models_parse_via_share_onnx(): assert policy_raw.size > 0 -@pytest.mark.skipif(not SHARE_ROOT.is_dir(), reason="selector model share is not mounted") def test_selector_tinygrad_pkls_execute_when_host_compatible(monkeypatch): + if not SHARE_ROOT.is_dir(): + return selector_dirs = _find_selector_dirs(limit=10) attempted = 0 executed = 0 @@ -152,6 +156,10 @@ def test_selector_tinygrad_pkls_execute_when_host_compatible(monkeypatch): if "/dev/kgsl-3d0" in str(exc): continue raise + except TypeError as exc: + if "DType.__init__()" in str(exc): + continue + raise assert "pose" in vision_outputs assert "plan" in policy_outputs @@ -160,4 +168,4 @@ def test_selector_tinygrad_pkls_execute_when_host_compatible(monkeypatch): break if executed == 0: - pytest.skip(f"share tinygrad pkls are QCOM-only on this host; inspected {attempted} bundles") + assert attempted > 0, "no selector bundles were inspected on the share" diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_supercombo_runner_guards.py b/iqpilot/selfdrive/iqmodeld/tests/test_supercombo_runner_guards.py index 4b9bf6471..66b74f1ba 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_supercombo_runner_guards.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_supercombo_runner_guards.py @@ -1,3 +1,6 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" from __future__ import annotations import hashlib @@ -153,6 +156,31 @@ def test_select_default_model_clears_custom_download_state(tmp_path: Path, monke assert not pending_restore.exists() +def test_seed_default_bundle_runs_while_a_download_is_queued(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(model_helpers, "ensure_default_model_files", lambda *a, **k: None) + + params = _FakeParams() + params.put("ModelManager_DownloadIndex", "81") + + model_helpers.seed_default_bundle_if_unset(params) + + active = params.get("ModelManager_ActiveBundle") + assert active is not None and active.get("ref") == "default" + assert params.get("ModelManager_DownloadIndex") == "81" + + +def test_seed_default_bundle_leaves_an_existing_active_bundle_alone(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(model_helpers, "ensure_default_model_files", lambda *a, **k: None) + + params = _FakeParams({"index": 81, "ref": "pop"}) + params.put("ModelManager_DownloadIndex", "81") + + model_helpers.seed_default_bundle_if_unset(params) + + assert params.get("ModelManager_ActiveBundle").get("ref") == "pop" + assert params.get("ModelManager_DownloadIndex") == "81" + + def test_default_model_is_not_resolved_to_manifest_pop_bundle(): pop_bundle = type("Bundle", (), {"internalName": "Pop (Default)", "displayName": "Pop (Default)"})() diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_temporal_replay.py b/iqpilot/selfdrive/iqmodeld/tests/test_temporal_replay.py index f166f2c1e..b73b17f4e 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_temporal_replay.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_temporal_replay.py @@ -1,3 +1,6 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" from __future__ import annotations from dataclasses import dataclass diff --git a/iqpilot/selfdrive/iqmodeld/tools/compile_model.py b/iqpilot/selfdrive/iqmodeld/tools/compile_model.py index 78e0f7c9d..721a94bc3 100644 --- a/iqpilot/selfdrive/iqmodeld/tools/compile_model.py +++ b/iqpilot/selfdrive/iqmodeld/tools/compile_model.py @@ -1,3 +1,6 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" import os import pickle import re diff --git a/iqpilot/selfdrive/iqmodeld/tools/compile_supercombo.py b/iqpilot/selfdrive/iqmodeld/tools/compile_supercombo.py index 86406cf1b..56ce00b2e 100644 --- a/iqpilot/selfdrive/iqmodeld/tools/compile_supercombo.py +++ b/iqpilot/selfdrive/iqmodeld/tools/compile_supercombo.py @@ -59,7 +59,6 @@ def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1) y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1) - # inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather) src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2] src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2] src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2] @@ -100,9 +99,7 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h): stride_pad = stride - cam_w def frame_prepare_tinygrad(input_frame, M_inv): - # UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV) - # deinterleave NV12 UV plane (UVUV... -> separate U, V) uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride) with Context(SPLIT_REDUCEOP=0): y = warp_perspective_tinygrad(input_frame[:cam_h*stride], @@ -142,7 +139,6 @@ def get_policy_npy_shapes(input_shapes): tc = input_shapes['traffic_convention'] # (1, 2) at = input_shapes['action_t'] # (1, 2) fb = input_shapes['features_buffer'] # (1, 24, 512) - # TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])} return shapes, [math.prod(s) for s in shapes.values()] @@ -155,7 +151,6 @@ def make_input_queues(input_shapes, frame_skip, device): shapes, sizes = get_policy_npy_shapes(input_shapes) packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32) - # views into the packed inputs, to be refilled at runtime npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}) input_queues.update({ 'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(), diff --git a/iqpilot/selfdrive/selfdrived/events.py b/iqpilot/selfdrive/selfdrived/events.py index f67aab3c3..2e36bfe34 100755 --- a/iqpilot/selfdrive/selfdrived/events.py +++ b/iqpilot/selfdrive/selfdrived/events.py @@ -648,8 +648,16 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { }, EventName.wrongGear: { - ET.SOFT_DISABLE: user_soft_disable_alert("Gear not D"), - ET.NO_ENTRY: NoEntryAlert("Gear not D"), + ET.SOFT_DISABLE: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), }, # This alert is thrown when the calibration angles are outside of the acceptable range. diff --git a/iqpilot/selfdrive/selfdrived/iq_events.py b/iqpilot/selfdrive/selfdrived/iq_events.py index 79f98eeb3..7ecf1cc0a 100644 --- a/iqpilot/selfdrive/selfdrived/iq_events.py +++ b/iqpilot/selfdrive/selfdrived/iq_events.py @@ -269,7 +269,7 @@ _ENGAGE_EVENTS: EVENTS_IQ_TYPE = { EventNameIQ.steeringOverrideReengageAlc: { ET.WARNING: Alert( "Steering Overridden By Driver", - "Re-Engage ALC", + "Double Tap SET or Cycle the Cruise Main to Re-Engage ALC", AlertStatus.userPrompt, AlertSize.mid, Priority.MID, VisualAlert.none, AudibleAlert.prompt, 2.0), }, @@ -294,9 +294,9 @@ _CABIN_BLOCK_EVENTS: EVENTS_IQ_TYPE = { AlertStatus.normal, AlertSize.none, Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), ET.NO_ENTRY: Alert( - "Not in Drive", - "IQ.Pilot Unavailable", - AlertStatus.normal, AlertSize.mid, + "", + "", + AlertStatus.normal, AlertSize.none, Priority.LOW, VisualAlert.none, AudibleAlert.none, 0.), }, @@ -344,11 +344,11 @@ _NOTICE_EVENTS: EVENTS_IQ_TYPE = { }, EventNameIQ.pedalHeldNotice: { - ET.WARNING: NoEntryAlert("Pedal Held") + ET.WARNING: NoEntryAlert("Brake Pedal Held") }, EventNameIQ.experimentalToggled: { - ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5) + ET.WARNING: NormalPermanentAlert("Switched to IQ.Pilot End to End Control", duration=1.5) }, EventNameIQ.e2eChime: { @@ -365,7 +365,6 @@ _NOTICE_EVENTS: EVENTS_IQ_TYPE = { priority=Priority.LOW), }, - # 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", diff --git a/iqpilot/selfdrive/selfdrived/tests/test_alerts.py b/iqpilot/selfdrive/selfdrived/tests/test_alerts.py index 6926c378c..8757a1a63 100644 --- a/iqpilot/selfdrive/selfdrived/tests/test_alerts.py +++ b/iqpilot/selfdrive/selfdrived/tests/test_alerts.py @@ -4,11 +4,12 @@ import os import random from PIL import Image, ImageDraw, ImageFont -from iqpilot.cereal import log, car +from iqpilot.cereal import log, car, custom from iqpilot.cereal.messaging import SubMaster from iqpilot.common.basedir import BASEDIR from iqpilot.common.params import Params from iqpilot.selfdrive.selfdrived.events import Alert, EVENTS, ET +from iqpilot.selfdrive.selfdrived.iq_events import EVENTS_IQ from iqpilot.selfdrive.selfdrived.events import invalid_lkas_setting_alert, invalid_lkas_setting_no_entry_alert from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from iqpilot.selfdrive.test.process_replay.process_replay import CONFIGS @@ -26,6 +27,17 @@ for event_types in EVENTS.values(): class TestAlerts: + def test_wrong_gear_alerts_are_silent_and_invisible(self): + wrong_gear_alerts = (EVENTS[log.OnroadEvent.EventName.wrongGear][ET.SOFT_DISABLE], + EVENTS[log.OnroadEvent.EventName.wrongGear][ET.NO_ENTRY], + EVENTS_IQ[custom.IQOnroadEvent.EventName.gearNotDriveSilent][ET.NO_ENTRY]) + + for alert in wrong_gear_alerts: + assert alert.alert_size == AlertSize.none + assert alert.audible_alert == car.CarControl.HUDControl.AudibleAlert.none + assert alert.alert_text_1 == "" + assert alert.alert_text_2 == "" + @classmethod def setup_class(cls): with open(OFFROAD_ALERTS_PATH) as f: diff --git a/iqpilot/system/hardware/tici/agnos.json b/iqpilot/system/hardware/tici/agnos.json index bd8aba746..0e92f1d8a 100644 --- a/iqpilot/system/hardware/tici/agnos.json +++ b/iqpilot/system/hardware/tici/agnos.json @@ -1,7 +1,7 @@ [ { "name": "xbl", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz", "hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6", "hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6", "size": 3282256, @@ -12,7 +12,7 @@ }, { "name": "xbl_config", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz", "hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9", "hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9", "size": 98124, @@ -23,7 +23,7 @@ }, { "name": "abl", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz", "hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee", "hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee", "size": 274432, @@ -34,7 +34,7 @@ }, { "name": "aop", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz", "hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788", "hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788", "size": 184364, @@ -45,7 +45,7 @@ }, { "name": "devcfg", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz", "hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585", "hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585", "size": 40336, @@ -56,7 +56,7 @@ }, { "name": "splash", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/splash-993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/splash-993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2.img.xz", "hash": "993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2", "hash_raw": "993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2", "size": 34226176, @@ -67,7 +67,7 @@ }, { "name": "boot", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/boot-aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/boot-aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb.img.xz", "hash": "aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb", "hash_raw": "aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb", "size": 18216960, @@ -78,7 +78,7 @@ }, { "name": "system", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/system-44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa.img.xz", "hash": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa", "hash_raw": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa", "size": 6291456000, diff --git a/iqpilot/system/hardware/tici/agnos.json.sig b/iqpilot/system/hardware/tici/agnos.json.sig index edfd31fba..f0514dbc1 100644 --- a/iqpilot/system/hardware/tici/agnos.json.sig +++ b/iqpilot/system/hardware/tici/agnos.json.sig @@ -1 +1 @@ -IE+GKvyryxGDx98VImAM+UaqOPcs+mzWtlVs7r0h4Va2Tb9glgFBFlpaB3Btll+QI5zoo/tMlDrj463kaYQQBg== +qT72MCHtDUWnARJbSsLUcPaISRZxjFPNf282R9ZC1SXvFJz6X6NmjgknK3OLDLijOQWlGvvJZGsDgz2vsdPVAg== diff --git a/iqpilot/system/hardware/tici/agnos.py b/iqpilot/system/hardware/tici/agnos.py index 435e11955..c44765cce 100755 --- a/iqpilot/system/hardware/tici/agnos.py +++ b/iqpilot/system/hardware/tici/agnos.py @@ -9,15 +9,11 @@ import subprocess import time from collections.abc import Generator -# agnos.py needs venv-only deps (casync->pycryptodome, swaglog->zmq, cereal). The boot runs -# it via `env python3` = /usr/bin/python3, which lacks them on stock AGNOS -> the IQ.OS -# bootstrap flash silently dies on import and the device hangs on the logo. Re-exec under the -# venv python when the current interpreter lacks them (no-op once already the venv python). import sys _VENV_PY = "/usr/local/venv/bin/python3" if sys.executable != _VENV_PY and os.path.exists(_VENV_PY): try: - import Crypto # noqa: F401 probe for the venv-only deps + import Crypto # noqa: F401 except ImportError: os.execv(_VENV_PY, [_VENV_PY, os.path.abspath(__file__), *sys.argv[1:]]) @@ -41,11 +37,7 @@ AGNOS_MANIFEST_FILE = "system/hardware/tici/agnos.json" LFS_POINTER_MAGIC = b"version https://git-lfs" - def _image_auth_module(): - """Return the git_remote auth module, or None. On IQ.OS this comes through the - verified loader; on stock AGNOS (an AGNOS->IQ.OS upgrade) that loader is not - present, but the compiled bundle IS in every checkout -- import it directly.""" try: from iqpilot.system.proprietary_runtime._verified_import import import_verified_module return import_verified_module("iqpilot_updater_private", "iqpilot_private.updater.git_remote") @@ -63,7 +55,6 @@ def _image_auth_module(): pass return None - def _download_headers(url: str) -> dict: mod = _image_auth_module() if mod is not None: @@ -82,11 +73,7 @@ def _download_headers(url: str) -> dict: pass return {} - def _open_image_response(url: str) -> requests.Response: - """GET an image URL; when the server answers with a Git-LFS pointer (the image - repo stores partitions as LFS objects and its raw endpoint does not resolve - them), follow it through the LFS batch API using the same credentials.""" auth = _download_headers(url) req = requests.get(url, stream=True, headers={'Accept-Encoding': None, **auth}, timeout=60) req.raise_for_status() @@ -99,9 +86,14 @@ def _open_image_response(url: str) -> requests.Response: meta = dict(line.split(" ", 1) for line in body.decode().strip().splitlines() if " " in line) oid = meta["oid"].split(":", 1)[1] size = int(meta["size"]) + lfs_base = url.split("/raw/", 1)[0] + ".git/info/lfs" - batch_url = url.split("/raw/", 1)[0] + ".git/info/lfs/objects/batch" - batch = requests.post(batch_url, + req = requests.get(f"{lfs_base}/objects/{oid}", stream=True, + headers={'Accept-Encoding': None, 'Accept': 'application/vnd.git-lfs', **auth}, timeout=60) + if req.status_code == 200: + return req + + batch = requests.post(f"{lfs_base}/objects/batch", data=json.dumps({"operation": "download", "transfers": ["basic"], "objects": [{"oid": oid, "size": size}]}), headers={"Content-Type": "application/vnd.git-lfs+json", @@ -128,13 +120,7 @@ def verify_manifest_signature(manifest_path: str) -> None: public_key = Ed25519PublicKey.from_public_bytes(IQPILOT_MANIFEST_PUBLIC_KEY) public_key.verify(signature, digest) - class _ChainedParts: - """Response-like wrapper streaming N sequential part files as one body. - - The image host caps single uploads well below the system image size, so big - images are stored as `.pNN` LFS objects; devices re-join them here.""" - def __init__(self, urls: list[str]) -> None: self.urls = urls self.req: requests.Response | None = None @@ -148,7 +134,6 @@ class _ChainedParts: self.req = _open_image_response(u) yield from self.req.iter_content(chunk_size=chunk_size) - class StreamingDecompressor: def __init__(self, url: str, parts: int = 0) -> None: self.buf = b"" @@ -187,57 +172,48 @@ class StreamingDecompressor: self.sha256.update(result) return result - def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]: - # https://source.android.com/devices/bootloader/images#sparse-format magic = struct.unpack("I", f.read(4))[0] assert(magic == 0xed26ff3a) - # Version major = struct.unpack("H", f.read(2))[0] minor = struct.unpack("H", f.read(2))[0] assert(major == 1 and minor == 0) - f.read(2) # file header size - f.read(2) # chunk header size + f.read(2) + f.read(2) block_sz = struct.unpack("I", f.read(4))[0] - f.read(4) # total blocks + f.read(4) num_chunks = struct.unpack("I", f.read(4))[0] - f.read(4) # crc checksum + f.read(4) for _ in range(num_chunks): chunk_type, out_blocks = SPARSE_CHUNK_FMT.unpack(f.read(12)) - if chunk_type == 0xcac1: # Raw - # TODO: yield in smaller chunks. Yielding only block_sz is too slow. Largest observed data chunk is 252 MB. + if chunk_type == 0xcac1: yield f.read(out_blocks * block_sz) - elif chunk_type == 0xcac2: # Fill + elif chunk_type == 0xcac2: filler = f.read(4) * (block_sz // 4) for _ in range(out_blocks): yield filler - elif chunk_type == 0xcac3: # Don't care + elif chunk_type == 0xcac3: yield b"" else: raise Exception("Unhandled sparse chunk type") - -# noop wrapper with same API as unsparsify() for non sparse images def noop(f: StreamingDecompressor) -> Generator[bytes, None, None]: while len(chunk := f.read(1024 * 1024)) > 0: yield chunk - def get_target_slot_number() -> int: current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip() return 1 if current_slot == "_a" else 0 - def slot_number_to_suffix(slot_number: int) -> str: assert slot_number in (0, 1) return '_a' if slot_number == 0 else '_b' - def get_partition_path(target_slot_number: int, partition: dict) -> str: path = f"/dev/disk/by-partlabel/{partition['name']}" @@ -246,7 +222,6 @@ def get_partition_path(target_slot_number: int, partition: dict) -> str: return path - def get_raw_hash(path: str, partition_size: int) -> str: raw_hash = hashlib.sha256() pos, chunk_size = 0, 1024 * 1024 @@ -259,7 +234,6 @@ def get_raw_hash(path: str, partition_size: int) -> str: return raw_hash.hexdigest().lower() - def verify_partition(target_slot_number: int, partition: dict[str, str | int], force_full_check: bool = False) -> bool: full_check = partition['full_check'] or force_full_check path = get_partition_path(target_slot_number, partition) @@ -281,7 +255,6 @@ def verify_partition(target_slot_number: int, partition: dict[str, str | int], f out.seek(partition_size) return out.read(64) == partition_hash.lower().encode() - def clear_partition_hash(target_slot_number: int, partition: dict) -> None: path = get_partition_path(target_slot_number, partition) with open(path, 'wb+') as out: @@ -291,13 +264,11 @@ def clear_partition_hash(target_slot_number: int, partition: dict) -> None: out.write(b"\x00" * 64) os.sync() - def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog): path = get_partition_path(target_slot_number, partition) downloader = StreamingDecompressor(partition['url'], parts=int(partition.get('url_parts', 0))) with open(path, 'wb+') as out: - # Flash partition last_p = 0 raw_hash = hashlib.sha256() f = unsparsify if partition['sparse'] else noop @@ -320,7 +291,6 @@ def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog) os.sync() - def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): path = get_partition_path(target_slot_number, partition) seed_path = path[:-1] + ('b' if path[-1] == 'a' else 'a') @@ -329,7 +299,6 @@ def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): sources: list[tuple[str, casync.ChunkReader, casync.ChunkDict]] = [] - # First source is the current partition. try: raw_hash = get_raw_hash(seed_path, partition['size']) caibx_url = f"{CAIBX_URL}{partition['name']}-{raw_hash}.caibx" @@ -342,10 +311,8 @@ def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): except Exception: cloudlog.exception("casync failed to hash seed partition") - # Second source is the target partition, this allows for resuming sources += [('target', casync.FileChunkReader(path), casync.build_chunk_dict(target))] - # Finally we add the remote source to download any missing chunks sources += [('remote', casync.RemoteChunkReader(partition['casync_store']), casync.build_chunk_dict(target))] last_p = 0 @@ -364,7 +331,6 @@ def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): if not verify_partition(target_slot_number, partition, force_full_check=True): raise Exception(f"Raw hash mismatch '{partition['hash_raw'].lower()}'") - def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalone=False): cloudlog.info(f"Downloading and writing {partition['name']}") @@ -372,7 +338,6 @@ def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalo cloudlog.info(f"Already flashed {partition['name']}") return - # Clear hash before flashing in case we get interrupted full_check = partition['full_check'] if not full_check: clear_partition_hash(target_slot_number, partition) @@ -384,13 +349,11 @@ def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalo else: extract_compressed_image(target_slot_number, partition, cloudlog) - # Write hash after successful flash if not full_check: with open(path, 'wb+') as out: out.seek(partition['size']) out.write(partition['hash_raw'].lower().encode()) - def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None: verify_manifest_signature(manifest_path) update = json.load(open(manifest_path)) @@ -406,14 +369,12 @@ def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None: else: cloudlog.error(f"Swap failed {out}") - def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, standalone=False) -> None: verify_manifest_signature(manifest_path) update = json.load(open(manifest_path)) cloudlog.info(f"Target slot {target_slot_number}") - # set target slot as unbootable os.system(f"abctl --set_unbootable {target_slot_number}") for partition in update: @@ -436,13 +397,11 @@ def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, st cloudlog.info(f"AGNOS ready on slot {target_slot_number}") - def verify_agnos_update(manifest_path: str, target_slot_number: int) -> bool: verify_manifest_signature(manifest_path) update = json.load(open(manifest_path)) return all(verify_partition(target_slot_number, partition) for partition in update) - if __name__ == "__main__": import argparse import logging diff --git a/iqpilot/system/hardware/tici/agnos_tici_15_1.json b/iqpilot/system/hardware/tici/agnos_tici_15_1.json index a8384399d..b34a60a00 100644 --- a/iqpilot/system/hardware/tici/agnos_tici_15_1.json +++ b/iqpilot/system/hardware/tici/agnos_tici_15_1.json @@ -1,7 +1,7 @@ [ { "name": "xbl", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz", "hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6", "hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6", "size": 3282256, @@ -12,7 +12,7 @@ }, { "name": "xbl_config", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz", "hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9", "hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9", "size": 98124, @@ -23,7 +23,7 @@ }, { "name": "abl", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz", "hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", "hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", "size": 274432, @@ -34,7 +34,7 @@ }, { "name": "aop", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz", "hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788", "hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788", "size": 184364, @@ -45,7 +45,7 @@ }, { "name": "devcfg", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz", "hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585", "hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585", "size": 40336, @@ -56,7 +56,7 @@ }, { "name": "boot", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/boot-aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/boot-aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb.img.xz", "hash": "aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb", "hash_raw": "aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb", "size": 18216960, @@ -67,7 +67,7 @@ }, { "name": "system", - "url": "https://gitlvb.teallvbs.xyz/IQ.Lvbs/iqos/raw/branch/master/system-44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa.img.xz", + "url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa.img.xz", "hash": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa", "hash_raw": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa", "size": 6291456000, diff --git a/iqpilot/system/hardware/tici/agnos_tici_15_1.json.sig b/iqpilot/system/hardware/tici/agnos_tici_15_1.json.sig index a518d12bc..7767e4116 100644 --- a/iqpilot/system/hardware/tici/agnos_tici_15_1.json.sig +++ b/iqpilot/system/hardware/tici/agnos_tici_15_1.json.sig @@ -1 +1 @@ -9LDZugtT9q8jFab1Gs8qTmfJukKU8NzDAuT9VakInuOUHCCnN5SDFI/Ew/6C/+3SAlcROgZON/4J8FQyQGV+Dg== +fL46Z+k/wqjavP3J1S/VzE90BXcvvRF+S41MgHEXYjW+Jlnu5REOp6rvp3SJFEkKIEaiSf9PsHyiDgQkC6J2Aw== diff --git a/iqpilot/system/hardware/tici/tests/test_agnos_updater.py b/iqpilot/system/hardware/tici/tests/test_agnos_updater.py index 8f6cf9895..d238eb8be 100644 --- a/iqpilot/system/hardware/tici/tests/test_agnos_updater.py +++ b/iqpilot/system/hardware/tici/tests/test_agnos_updater.py @@ -8,9 +8,8 @@ MANIFESTS = [ os.path.join(TEST_DIR, "../agnos_tici_15_1.json"), ] -IMAGE_HOST = "gitlvb.teallvbs.xyz" +IMAGE_HOST = "git.konn3kt.com" -# image payloads are xz streams; the repo raw endpoint would serve an LFS pointer XZ_MAGIC = b"\xfd7zXZ\x00" LFS_POINTER_MAGIC = b"version https://git-lfs" @@ -27,12 +26,6 @@ class TestAgnosUpdater: if not img['sparse']: assert img['hash'] == img['hash_raw'] - # contract: images are distributed from a private repo, so an anonymous - # request must never receive image content. The denial status varies by - # route (404 via the CDN, catch-all HTML page when resolved directly to - # the origin), so assert on the payload, not the status code. trust_env - # off: requests otherwise picks up ~/.netrc (CI runners have gitlvb - # credentials), silently authenticating the "anonymous" probe. s = requests.Session() s.trust_env = False r = s.get(img['url'], timeout=10, stream=True, diff --git a/iqpilot/system/ui/lib/os_update.py b/iqpilot/system/ui/lib/os_update.py index bafec553e..806beee73 100644 --- a/iqpilot/system/ui/lib/os_update.py +++ b/iqpilot/system/ui/lib/os_update.py @@ -43,8 +43,6 @@ def required_agnos_version(install_path: str) -> str: def _hardware_dir(install_path: str) -> str: - # the nested layout keeps the package at /iqpilot/; older checkouts had - # system/ at the top level. Accept either so callers can pass the install root. nested = os.path.join(install_path, "iqpilot", "system", "hardware", "tici") if os.path.isdir(nested): return nested