IQ.Pilot Release Commit @ bec7652

This commit is contained in:
IQ.Lvbs CI [bot]
2026-08-22 21:29:55 -05:00
parent 11cefcb266
commit 7786d94c33
58 changed files with 578 additions and 267 deletions

View File

@@ -16,17 +16,17 @@
}, },
"python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "94664d1b9abd1c0cb124707bf10f746f23adf0847a890d507df86f1e4a7e8ca9", "sha256": "50f00d356371a60a50b2395966b6b1646fd3b8306e9ad6fe0229ad0e24186683",
"size": 67616 "size": 67616
}, },
"python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/updater/_pat_blob.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "481235a5685e14f5b8e9216908be23be9743fe6f487f2b38fb21c33dd84e22cf", "sha256": "57b15e47bcc1084d33bda3908fb50347c2403cde0c9c951dad1b107ef56b06f1",
"size": 67616 "size": 67616
}, },
"python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "75ffbf8212f805b054c97ca4723a56071db822e889cda9d6bec233b2d2c8592a", "sha256": "1d3611c753cc94d1e22802900a2487a13c445def98fa998385fa72935a0dd110",
"size": 136488 "size": 136488
}, },
"runtime": { "runtime": {
@@ -38,8 +38,8 @@
} }
}, },
"signatures": { "signatures": {
"python/iqpilot_private/updater/_iqos_pat_blob.cpython-312-aarch64-linux-gnu.so": "B45oDHpHNmslzjTyeuEVbE4fOytUsC/0po+CiHiLI+T01yWJIb6Dt4yFJm+qVTEqCLJfajedd1FnIQHCyUddDw==", "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": "a+YcE2ik7Oo3btunKGoA+FHN+uSDoric2V6nu5SjtKSM/Nwpg1M9BX0BM55sA56sz6FgPRn93u+yPL7eryLYDw==", "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": "NcdigDJOQ7d9A+HXCewPowezSML1i4cHW6uRyZ1N6vmtkXDorRk9YC4lxlM8uvoS/tB5/jQBkfc/DhkwPXAoAA==" "python/iqpilot_private/updater/git_remote.cpython-312-aarch64-linux-gnu.so": "2aLUgznDiiBF3sYwIxZKMjMW2A1S9q+tHZUnyAiEB48QbxAFjDAS6tMHUp5ej6/JAmejOzxQePYPL1jJnxKsBQ=="
} }
} }

View File

@@ -1 +1 @@
54xdruMwhqg4ouUav9CwbOhcsytYbn/uy/OL1pT6Sd/JRvgsLckfe8+jsG527nyhZ7sBpxPx5dFLX3h/eOaSDg== D9FZfSC3DUspPq4ewwPjNoCS1N8ai3jcgutVQo94CnOI6Wcx3L1H30hTZ6SS8jw9MfMsVaggFXfMo2QT4yJlBQ==

View File

@@ -41,6 +41,9 @@ if not _os.path.exists("/usr/libexec/iqpilot/runtime_integrity.json"):
"update_mqb_carstate_alc_state": _noop, "update_mqb_carstate_alc_state": _noop,
"update_mlb_carstate_alc_state": _noop, "update_mlb_carstate_alc_state": _noop,
"update_pq_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", { _stub_module("iqpilot_private.konn3kt.iqlvbs.iqlvbs_commander", {
"update_turn_signals": _noop, "update_turn_signals": _noop,
@@ -50,13 +53,20 @@ if not _os.path.exists("/usr/libexec/iqpilot/runtime_integrity.json"):
try: try:
from iqpilot.system.proprietary_runtime import _verified_import as _vi from iqpilot.system.proprietary_runtime import _verified_import as _vi
_real_import_verified_module = _vi.import_verified_module _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: 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 --------------------------------------------------- # --- end host test harness ---------------------------------------------------
_ABSTRACT_BASES = frozenset(("CarSafetyTest", "AolSafetyTestBase", "SafetyTest", "SafetyTestBase")) _ABSTRACT_BASES = frozenset(("CarSafetyTest", "AolSafetyTestBase", "SafetyTest", "SafetyTestBase"))

View File

@@ -353,6 +353,7 @@ routes = [
CarTestRoute("79b0a26c081d6d6c/00000083--8407f96a1e", VOLKSWAGEN.VOLKSWAGEN_PASSAT_MK7), CarTestRoute("79b0a26c081d6d6c/00000083--8407f96a1e", VOLKSWAGEN.VOLKSWAGEN_PASSAT_MK7),
CarTestRoute("cc7e1fc0ce4ec686/00000004--dddc5bf4b6", VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS_PLUS), CarTestRoute("cc7e1fc0ce4ec686/00000004--dddc5bf4b6", VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS_PLUS),
CarTestRoute("07667b885add75fd/2021-01-23--19-48-42", VOLKSWAGEN.AUDI_A3_MK3), 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("5432d2499e17e646/000000e8--9ce8cf6d58", VOLKSWAGEN.AUDI_Q5_MK1),
CarTestRoute("6c6b466346192818/2021-06-06--14-17-47", VOLKSWAGEN.AUDI_Q2_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), CarTestRoute("0cd0b7f7e31a3853/2021-12-03--03-12-05", VOLKSWAGEN.AUDI_Q3_MK2),

View File

@@ -124,6 +124,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
"VOLKSWAGEN_ID3_MK2" = "VOLKSWAGEN_ID4_MK2" "VOLKSWAGEN_ID3_MK2" = "VOLKSWAGEN_ID4_MK2"
"SKODA_ENYAQ_MK1" = "VOLKSWAGEN_ID4_MK1" "SKODA_ENYAQ_MK1" = "VOLKSWAGEN_ID4_MK1"
"SKODA_ENYAQ_MK2" = "VOLKSWAGEN_ID4_MK2" "SKODA_ENYAQ_MK2" = "VOLKSWAGEN_ID4_MK2"
"AUDI_A4_MK4" = "AUDI_Q5_MK1"
"AUDI_Q4_MK1" = "VOLKSWAGEN_ID4_MK1" "AUDI_Q4_MK1" = "VOLKSWAGEN_ID4_MK1"
"AUDI_Q4_MK2" = "VOLKSWAGEN_ID4_MK2" "AUDI_Q4_MK2" = "VOLKSWAGEN_ID4_MK2"
"VOLKSWAGEN_GOLF_MK8" = "VOLKSWAGEN_GOLF_MK7" "VOLKSWAGEN_GOLF_MK8" = "VOLKSWAGEN_GOLF_MK7"

View File

@@ -205,6 +205,8 @@ class CarController(CarControllerBase):
self._pt_tx_bus = self.CAN.aux self._pt_tx_bus = self.CAN.aux
elif CP.flags & VolkswagenFlags.MLB: elif CP.flags & VolkswagenFlags.MLB:
self.CCS = mlbcan 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): elif CP.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO):
self.CCS = mebcan self.CCS = mebcan
else: else:
@@ -648,7 +650,8 @@ class CarController(CarControllerBase):
cancel_cmd = stock_cancel_pressed or self._tap_gra_cancel(CC.cruiseControl.cancel, gra_send_ready) 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) 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): 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, can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, bus_send, CS.gra_stock_values,
cancel=cancel_cmd, resume=resume_cmd)) cancel=cancel_cmd, resume=resume_cmd))

View File

@@ -672,9 +672,13 @@ class CarState(CarStateBase):
ret.gasPressed = pt_cp.vl["Motor_03"]["MO_Fahrpedalrohwert_01"] > 0 ret.gasPressed = pt_cp.vl["Motor_03"]["MO_Fahrpedalrohwert_01"] > 0
if self.CP.carFingerprint == CAR.PORSCHE_MACAN_MK1: 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)) 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: else:
ret.gearShifter = GearShifter.drive 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"]) cruise_main_switch = bool(pt_cp.vl["LS_01"]["LS_Hauptschalter"])
if not self.CP.pcmCruise: if not self.CP.pcmCruise:
ret.cruiseState.available = cruise_main_switch ret.cruiseState.available = cruise_main_switch
@@ -687,7 +691,8 @@ class CarState(CarStateBase):
else: else:
ret.cruiseState.available = pt_cp.vl["TSK_02"]["TSK_Status"] in (0, 1, 2) 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.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.accFaulted = pt_cp.vl["TSK_02"]["TSK_Status"] in (3,)
ret.cruiseState.nonAdaptive = bool(pt_cp.vl["LS_01"]["LS_Limiter"]) 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): 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.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"])] 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.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 ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE
@@ -826,13 +838,17 @@ class CarState(CarStateBase):
] ]
else: else:
pt_messages += [("Kombi_02", math.nan)] # Auxiliary-bus cluster odometer 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: if CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT:
cam_messages += [ cam_messages += [
("HCA_01", 1), # From R242 Driver assistance camera, 50Hz if steering/1Hz if not ("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 { 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.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), Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, CanBus(CP).cam),
} }

View File

@@ -1046,6 +1046,14 @@ FW_VERSIONS = {
b'\xf1\x875Q0907572P \xf1\x890682', 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: { CAR.AUDI_Q2_MK1: {
(Ecu.engine, 0x7e0, None): [ (Ecu.engine, 0x7e0, None): [
b'\xf1\x8704E906027JT\xf1\x894145', b'\xf1\x8704E906027JT\xf1\x894145',

View File

@@ -7,7 +7,8 @@ from iqdbc.car.isotp_parallel_query import IsoTpParallelQuery
from iqdbc.car.volkswagen.carcontroller import CarController from iqdbc.car.volkswagen.carcontroller import CarController
from iqdbc.car.volkswagen.carstate import CarState from iqdbc.car.volkswagen.carstate import CarState
from iqdbc.car.volkswagen.values import ( 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, VolkswagenFlags, VolkswagenSafetyFlags, VolkswagenFlagsIQ, get_longitudinal_stopping_speed_override,
) )
from iqdbc.car.volkswagen.radar_interface import RadarInterface from iqdbc.car.volkswagen.radar_interface import RadarInterface
@@ -94,6 +95,29 @@ class CarInterface(CarInterfaceBase):
ret.transmissionType = TransmissionType.automatic ret.transmissionType = TransmissionType.automatic
ret.dashcamOnly = False 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): elif ret.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO):
if ret.flags & VolkswagenFlags.MEB: if ret.flags & VolkswagenFlags.MEB:
safety_configs = [get_safety_config(structs.CarParams.SafetyModel.volkswagenMeb)] safety_configs = [get_safety_config(structs.CarParams.SafetyModel.volkswagenMeb)]
@@ -209,6 +233,11 @@ class CarInterface(CarInterfaceBase):
ret.longitudinalTuning.kiV = [0.4, 0.] ret.longitudinalTuning.kiV = [0.4, 0.]
ret.alphaLongitudinalAvailable = ret.networkLocation == NetworkLocation.gateway or docs or bool(ret.flags & VolkswagenFlags.DISABLE_RADAR) 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: if alpha_long:
# Proof-of-concept, prep for E2E only. No radar points available. Panda ALLOW_DEBUG firmware required. # Proof-of-concept, prep for E2E only. No radar points available. Panda ALLOW_DEBUG firmware required.
ret.openpilotLongitudinalControl = True ret.openpilotLongitudinalControl = True

View File

@@ -4,11 +4,14 @@ from types import SimpleNamespace
import pytest 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.volkswagen.carstate import CarState
from iqdbc.car.structs import CarParams from iqdbc.car.structs import CarParams
from iqdbc.car.volkswagen.interface import CarInterface 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 from iqdbc.car.volkswagen.fingerprints import FW_VERSIONS
Ecu = CarParams.Ecu 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 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", ( @pytest.mark.parametrize("button_type", (
structs.CarState.ButtonEvent.Type.setCruise, structs.CarState.ButtonEvent.Type.setCruise,
structs.CarState.ButtonEvent.Type.resumeCruise, structs.CarState.ButtonEvent.Type.resumeCruise,

View File

@@ -300,6 +300,7 @@ class VolkswagenSafetyFlags(IntFlag):
PQ_NO_CAM_BUS = 128 PQ_NO_CAM_BUS = 128
PQ_ACC_FTS_EPB = 256 PQ_ACC_FTS_EPB = 256
PQ_SNG_ECD = 512 PQ_SNG_ECD = 512
MLB_NO_ECAN = 1024
class VolkswagenFlags(IntFlag): 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_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_SNG_ECD = 1 << 19
IQ_PQ_TIMEBOMB = 1 << 20 IQ_PQ_TIMEBOMB = 1 << 20
IQ_MLB_NO_ECAN = 1 << 21
IQ_MLB_NO_HCA_EPS = 1 << 22
RADAR_DISABLE_STATE = {"error": False} 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 @dataclass
class VolkswagenMLBPlatformConfig(PlatformConfig): class VolkswagenMLBPlatformConfig(PlatformConfig):
@@ -671,6 +687,12 @@ class CAR(Platforms):
chassis_codes={"8V", "FF"}, chassis_codes={"8V", "FF"},
wmis={WMI.AUDI_GERMANY_CAR, WMI.AUDI_SPORT}, 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( AUDI_Q2_MK1 = VolkswagenMQBPlatformConfig(
[VWCarDocs("Audi Q2 2018")], [VWCarDocs("Audi Q2 2018")],
VolkswagenCarSpecs(mass=1205, wheelbase=2.61), VolkswagenCarSpecs(mass=1205, wheelbase=2.61),

View File

@@ -18,6 +18,8 @@ extern const uint16_t FLAG_VOLKSWAGEN_PQ_ACC_FTS_EPB;
const uint16_t FLAG_VOLKSWAGEN_PQ_ACC_FTS_EPB = 256; const uint16_t FLAG_VOLKSWAGEN_PQ_ACC_FTS_EPB = 256;
extern const uint16_t FLAG_VOLKSWAGEN_PQ_SNG_ECD; extern const uint16_t FLAG_VOLKSWAGEN_PQ_SNG_ECD;
const uint16_t FLAG_VOLKSWAGEN_PQ_SNG_ECD = 512; 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 static uint8_t volkswagen_crc8_lut_8h2f[256]; // Static lookup table for CRC8 poly 0x2F, aka 8H2F/AUTOSAR

View File

@@ -12,6 +12,8 @@
// so a rounding difference on the sending side cannot silently block the whole ACC_01 stream. // so a rounding difference on the sending side cannot silently block the whole ACC_01 stream.
#define VOLKSWAGEN_MLB_INACTIVE_ACCEL_TOLERANCE 15 #define VOLKSWAGEN_MLB_INACTIVE_ACCEL_TOLERANCE 15
static bool volkswagen_mlb_no_ecan = false;
static bool volkswagen_mlb_long_accel_check(int desired_accel) { static bool volkswagen_mlb_long_accel_check(int desired_accel) {
int inactive_delta = desired_accel - VW_IQ_INACTIVE_LONG_ACCEL; int inactive_delta = desired_accel - VW_IQ_INACTIVE_LONG_ACCEL;
bool inactive = ((inactive_delta >= -VOLKSWAGEN_MLB_INACTIVE_ACCEL_TOLERANCE) && 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}, 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}}; {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[] = { static RxCheck volkswagen_mlb_rx_checks[] = {
// TODO: implement checksum validation // TODO: implement checksum validation
{.msg = {{MSG_ESP_03, 0, 8, 50U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, {.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 }}}, {.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_common_init();
volkswagen_mlb_no_ecan = GET_FLAG(param, FLAG_VOLKSWAGEN_MLB_NO_ECAN);
#ifdef ALLOW_DEBUG #ifdef ALLOW_DEBUG
volkswagen_longitudinal = GET_FLAG(param, FLAG_VOLKSWAGEN_LONG_CONTROL); volkswagen_longitudinal = GET_FLAG(param, FLAG_VOLKSWAGEN_LONG_CONTROL);
@@ -56,11 +71,12 @@ static safety_config volkswagen_mlb_init(uint16_t param) {
#endif #endif
return volkswagen_longitudinal ? BUILD_SAFETY_CFG(volkswagen_mlb_rx_checks, VOLKSWAGEN_MLB_LONG_TX_MSGS) : \ 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) { 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 // Check all wheel speeds for any movement
// Signals: ESP_03.ESP_[VL|VR|HL|HR]_Radgeschw // Signals: ESP_03.ESP_[VL|VR|HL|HR]_Radgeschw
if (msg->addr == MSG_ESP_03) { if (msg->addr == MSG_ESP_03) {

View File

@@ -216,5 +216,72 @@ class TestVolkswagenMlbLongSafety(TestVolkswagenMlbSafetyBase):
self.assertTrue(self._tx(self._acc_01_msg(0.5))) 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -26,25 +26,9 @@ from iqpilot.iq_maps.vendor_mapd_installer import VendorMapdInstaller
OfflineMapAction = custom.MapdInputType OfflineMapAction = custom.MapdInputType
_region_sync_worker: threading.Thread | None = None _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_lock = threading.Lock()
_active_proc: subprocess.Popen | None = None _active_proc: subprocess.Popen | None = None
_shutdown = threading.Event() _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 _tile_downloader: TileBundleDownloader | None = None
@@ -62,7 +46,6 @@ def _pid_is_vendor_fetch(pid: int) -> bool:
def _reap_orphaned_vendor_fetch() -> None: def _reap_orphaned_vendor_fetch() -> None:
"""Kill any vendor-fetch mapd subprocess left running from a prior, uncleanly-terminated run."""
pidfile = _vendor_fetch_pidfile() pidfile = _vendor_fetch_pidfile()
try: try:
with open(pidfile) as f: with open(pidfile) as f:
@@ -122,8 +105,6 @@ def _install_signal_handlers() -> None:
def ensure_vendor_runtime() -> 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: try:
VendorMapdInstaller().verify() VendorMapdInstaller().verify()
except Exception: 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: 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 global _tile_downloader
if not params.get_bool("OfflineOSMaps"): if not params.get_bool("OfflineOSMaps"):
return return
@@ -239,8 +216,6 @@ def _drive_vendor_fetch(region_selector: str, requested_regions: dict) -> None:
break break
cloudlog.info(f"iq_maps: vendor map download finished for {region_selector}") cloudlog.info(f"iq_maps: vendor map download finished for {region_selector}")
if not cancelled and not _shutdown.is_set(): 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")) _fetch_tile_bundles(region_selector, abort_check=lambda: _shutdown.is_set() or not mem_params.get("OSMDownloadLocations"))
except Exception: except Exception:
cloudlog.exception("iq_maps: vendor map download failed") cloudlog.exception("iq_maps: vendor map download failed")
@@ -304,17 +279,12 @@ _last_auto_restore_t = 0.0
def region_data_missing() -> bool: 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"): if not params.get_bool("OsmLocal"):
return False return False
if not params.get("OsmDownloadedDate"): if not params.get("OsmDownloadedDate"):
return False return False
if glob.glob(f"{Paths.mapd_root()}/db") or glob.glob(f"{Paths.mapd_root()}/v*"): if glob.glob(f"{Paths.mapd_root()}/db") or glob.glob(f"{Paths.mapd_root()}/v*"):
return False return False
# mapd v2 stores region tiles under offline/<evenLat>/<evenLon>.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/*/*"): if glob.glob(f"{Paths.mapd_root()}/offline/*/*"):
return False return False
country = params.get("OsmLocationName", return_default=True) country = params.get("OsmLocationName", return_default=True)
@@ -322,8 +292,6 @@ def region_data_missing() -> bool:
def configured_states() -> list[str]: 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: try:
states = params.get("OsmStateNames") states = params.get("OsmStateNames")
if isinstance(states, bytes): if isinstance(states, bytes):
@@ -370,8 +338,6 @@ def _configured_region_selector() -> str:
def tile_bundles_missing() -> bool: 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"): if not params.get_bool("OfflineOSMaps"):
return False return False
selector = _configured_region_selector() selector = _configured_region_selector()
@@ -381,8 +347,6 @@ def tile_bundles_missing() -> bool:
def maybe_restore_tile_bundles() -> None: 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 global _last_tile_restore_t, _tile_only_worker
if not tile_bundles_missing(): if not tile_bundles_missing():
return return
@@ -410,9 +374,6 @@ def sync_osm_request_flags() -> None:
maybe_restore_tile_bundles() maybe_restore_tile_bundles()
if params.get_bool("OsmDbUpdatesCheck"): if params.get_bool("OsmDbUpdatesCheck"):
if _region_sync_worker is not None and _region_sync_worker.is_alive(): 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 return
purge_stale_region_artifacts(stale_region_artifacts()) purge_stale_region_artifacts(stale_region_artifacts())
country = params.get("OsmLocationName", return_default=True) country = params.get("OsmLocationName", return_default=True)
@@ -439,11 +400,6 @@ def run_loop():
pass pass
except PermissionError: except PermissionError:
cloudlog.exception(f"iq_maps: failed to make {Paths.mapd_root()}") 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() _reap_orphaned_vendor_fetch()
_install_signal_handlers() _install_signal_handlers()

View File

@@ -1,20 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos 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/<selector>/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:
<base>/index.json {"version": 1, "regions": {<selector>: entry}}
<base>/<entry["path"]> 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 hashlib
import json import json
@@ -29,16 +15,12 @@ from iqpilot.common.params import Params
from iqpilot.common.swaglog import cloudlog from iqpilot.common.swaglog import cloudlog
from iqpilot.ui.onroad.offline_tiles import offline_map_root 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: try:
from iqpilot.iq_maps.tiles_auth import get_base_urls as _private_base_urls, get_requests_auth as _private_auth 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 except Exception: # ProprietaryModuleMissing or import errors in stripped builds
_private_base_urls = None _private_base_urls = None
_private_auth = 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" DEFAULT_TILE_BUNDLE_BASE_URL = "https://maps.konn3kt.com/iqosmd/v1"
BASE_URL_PARAM = "OfflineTilesBaseUrl" BASE_URL_PARAM = "OfflineTilesBaseUrl"
PROGRESS_PARAM = "OfflineTilesDownloadProgress" PROGRESS_PARAM = "OfflineTilesDownloadProgress"
@@ -49,8 +31,6 @@ STREAM_RETRIES = 8
def candidate_base_urls(params: Params) -> list[str]: 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) override = params.get(BASE_URL_PARAM)
if isinstance(override, bytes): if isinstance(override, bytes):
override = override.decode("utf-8", errors="ignore") override = override.decode("utf-8", errors="ignore")
@@ -144,12 +124,6 @@ def _write_manifest(selector: str, entry: dict) -> None:
class TileBundleDownloader: 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, def __init__(self, params: Params | None = None, mem_params: Params | None = None,
abort_check=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.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else self.params
self.session = requests.Session() self.session = requests.Session()
self._cancelled = threading.Event() self._cancelled = threading.Event()
# optional external cancel signal, e.g. the orchestrator's OSMDownloadLocations removal
self._abort_check = abort_check self._abort_check = abort_check
def cancel(self) -> None: def cancel(self) -> None:
@@ -170,7 +143,6 @@ class TileBundleDownloader:
if self._cancelled.is_set(): if self._cancelled.is_set():
return True return True
if not self.mem_params.get(REQUEST_PARAM): if not self.mem_params.get(REQUEST_PARAM):
# request flag was removed out from under us -> user cancelled
self._cancelled.set() self._cancelled.set()
return True return True
if self._abort_check is not None and self._abort_check(): 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, def _download_one(self, selector: str, entry: dict, base_url: str,
progress_offset: int, progress_total: int) -> bool: 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) night_path = region_bundle_path(selector)
ok = self._download_file( ok = self._download_file(
selector, base_url, entry["path"], int(entry.get("bytes", 0)), 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, progress_offset + int(entry.get("bytes", 0)), progress_total,
) )
if not day_ok: 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") 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) _write_manifest(selector, entry)
cloudlog.info(f"iq_maps: installed tile bundle {selector}") cloudlog.info(f"iq_maps: installed tile bundle {selector}")
return True return True
@@ -219,8 +188,6 @@ class TileBundleDownloader:
part_path = final_path.with_name(final_path.name + ".part") part_path = final_path.with_name(final_path.name + ".part")
part_path.parent.mkdir(parents=True, exist_ok=True) 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 downloaded = 0
digest = hashlib.sha256() digest = hashlib.sha256()
last_error: Exception | None = None last_error: Exception | None = None
@@ -244,7 +211,6 @@ class TileBundleDownloader:
auth = request_auth() auth = request_auth()
response = self.session.get(url, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S, auth=auth) response = self.session.get(url, headers=headers, stream=True, timeout=HTTP_TIMEOUT_S, auth=auth)
if resume_from and response.status_code != 206: if resume_from and response.status_code != 206:
# server ignored the Range request -> restart from scratch
digest = hashlib.sha256() digest = hashlib.sha256()
resume_from = 0 resume_from = 0
part_path.unlink(missing_ok=True) part_path.unlink(missing_ok=True)
@@ -284,8 +250,6 @@ class TileBundleDownloader:
return True return True
def download_regions(self, selectors: list[str]) -> bool: 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() self._cancelled.clear()
ok = True ok = True
try: try:
@@ -346,7 +310,6 @@ class TileBundleDownloader:
if not expected_sha or installed_sha != expected_sha: if not expected_sha or installed_sha != expected_sha:
return False return False
if entry.get("day_path"): 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" day_file = region_bundle_dir(selector) / "tiles" / "offline_day.mbtiles"
installed_day = str(manifest.get("mbtiles_day", {}).get("sha256", "")).strip().lower() installed_day = str(manifest.get("mbtiles_day", {}).get("sha256", "")).strip().lower()
expected_day = str(entry.get("day_sha256", "")).strip().lower() expected_day = str(entry.get("day_sha256", "")).strip().lower()

View File

@@ -7,4 +7,4 @@ from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_m
try: try:
load_private_module(__name__, "iqpilot_private.maps.git_auth") load_private_module(__name__, "iqpilot_private.maps.git_auth")
except ProprietaryModuleMissing: except ProprietaryModuleMissing:
from iqpilot.maps_private_src.git_auth import * # noqa: F403 from iqpilot.maps_private_src.git_auth import *

View File

@@ -1,11 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos 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 argparse
import os import os
@@ -21,7 +16,6 @@ from iqpilot.iq_maps.vendor_mapd_installer import (
_RELEASE_SYMBOL = "VENDOR_RELEASE_TAG" _RELEASE_SYMBOL = "VENDOR_RELEASE_TAG"
_INSTALLER_SRC = os.path.join(BASEDIR, "iqpilot", "iq_maps", "vendor_mapd_installer.py") _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 = os.path.join(BASEDIR, "iqpilot", "iq_maps", "tests", "mapd_hash")
_HASH_FILE = HASH_FILE _HASH_FILE = HASH_FILE
_TAG_ASSIGN = re.compile(rf'^{_RELEASE_SYMBOL}\s*=\s*["\'][^"\']*["\']', re.MULTILINE) _TAG_ASSIGN = re.compile(rf'^{_RELEASE_SYMBOL}\s*=\s*["\'][^"\']*["\']', re.MULTILINE)

View File

@@ -1,14 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos 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 hashlib
import os import os
@@ -29,7 +21,6 @@ QUARANTINE_PATH = VENDOR_MAPD_PATH + ".quarantined"
def sha256_of_file(path: str) -> str: def sha256_of_file(path: str) -> str:
"""Hex SHA-256 digest of a file on disk."""
digest = hashlib.sha256() digest = hashlib.sha256()
with open(path, "rb") as handle: with open(path, "rb") as handle:
for block in iter(lambda: handle.read(1 << 20), b""): for block in iter(lambda: handle.read(1 << 20), b""):
@@ -50,14 +41,12 @@ class VendorMapdInstaller:
return str(self._params.get(_VERSION_PARAM) or "") return str(self._params.get(_VERSION_PARAM) or "")
def verify(self) -> bool: def verify(self) -> bool:
"""True iff the on-disk binary matches the pinned hash; quarantines a wrong one."""
expected = self._expected_hash() expected = self._expected_hash()
if not expected: if not expected:
cloudlog.error("iq_maps: pinned mapd hash missing, vendor binary cannot be verified") cloudlog.error("iq_maps: pinned mapd hash missing, vendor binary cannot be verified")
return False return False
if not os.path.isfile(VENDOR_MAPD_PATH): 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._say("Offline maps engine missing; it will be restored by the next update.")
self._params.remove(_VERSION_PARAM) self._params.remove(_VERSION_PARAM)
return False return False
@@ -77,8 +66,6 @@ class VendorMapdInstaller:
self._say(f"Offline maps engine verified [{VENDOR_RELEASE_TAG}]") self._say(f"Offline maps engine verified [{VENDOR_RELEASE_TAG}]")
return True 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") 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.") self._say("Offline maps engine failed verification; quarantined until the next update.")
try: try:

View File

@@ -1,9 +1,5 @@
""" """
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos 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 iqpilot.cereal import car, custom
from iqdbc.car import structs from iqdbc.car import structs

View File

@@ -114,6 +114,20 @@
], ],
"req": "Adaptive Cruise Control (ACC) & Lane Assist" "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": { "AUDI_Q2_MK1|2018": {
"label": "Audi Q2 2018", "label": "Audi Q2 2018",
"id": "AUDI_Q2_MK1", "id": "AUDI_Q2_MK1",

View File

@@ -63,6 +63,9 @@ IQP_NAV_MODEL_INFLUENCE_ENABLED = False
TurnDirection = custom.IQTurnSignalDirection TurnDirection = custom.IQTurnSignalDirection
IQMODEL_EVAL_WARN_US = int(DT_MDL * 1_000_000) IQMODEL_EVAL_WARN_US = int(DT_MDL * 1_000_000)
IQMODEL_EVAL_ERROR_US = IQMODEL_EVAL_WARN_US * 2 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: def _plan_y_std_1s(outputs: dict[str, np.ndarray]) -> float:
@@ -605,12 +608,21 @@ class InferenceDaemon:
def serve(self) -> None: def serve(self) -> None:
tick = 0 tick = 0
starved_polls = 0
while True: while True:
frame_pair = self._cameras.pull() frame_pair = self._cameras.pull()
if frame_pair is None: 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 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 main_buf, extra_buf, main_stamp, extra_stamp = frame_pair
self._sub.update(0) self._sub.update(0)
self._refresh_tunables(tick) self._refresh_tunables(tick)

View File

@@ -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/
""" """

View File

@@ -1,11 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos Copyright © 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.
""" """
from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
try: try:

View File

@@ -40,7 +40,6 @@ _DEFAULT_BUNDLE_REF = "default"
def get_default_model_bundle(_bundles): def get_default_model_bundle(_bundles):
"""Legacy compatibility hook: stock default is preinstalled, not a manifest bundle."""
return None return None
@@ -239,10 +238,13 @@ def select_default_model(params: Params = None) -> None:
def seed_default_bundle_if_unset(params: Params = None) -> None: def seed_default_bundle_if_unset(params: Params = None) -> None:
params = Params() if params is None else params 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 return
queued_download = params.get(_DOWNLOAD_INDEX_KEY)
try: try:
select_default_model(params) 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") cloudlog.warning("default_model: seeded Default (CD210) as active bundle")
except Exception as e: except Exception as e:
cloudlog.exception(f"default_model: failed to seed default bundle: {e}") cloudlog.exception(f"default_model: failed to seed default bundle: {e}")

View File

@@ -1,9 +1,5 @@
""" """
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos 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 from iqpilot.common.steer_delay import cached_steer_delay

View File

@@ -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/
""" """

View File

@@ -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/
""" """

View File

@@ -27,8 +27,6 @@ WARP_DEV = os.getenv('WARP_DEV')
class TinygradFusedRunner(ModelRunner): class TinygradFusedRunner(ModelRunner):
"""Runs a fused warp+vision+policy pkl. Bundle ships one `driving_fused_*` artifact."""
uses_opencl_warp: bool = False uses_opencl_warp: bool = False
def __init__(self): 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])), '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])), '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'] on_shapes = self._on_meta['input_shapes']
captured = self._run_policy.captured captured = self._run_policy.captured
jit_shapes = { jit_shapes = {
@@ -135,7 +132,6 @@ class TinygradFusedRunner(ModelRunner):
self._cam_resolution = (cam_w, cam_h) self._cam_resolution = (cam_w, cam_h)
def run_fused(self, bufs: dict, transforms: dict[str, np.ndarray], numpy_inputs: NumpyDict) -> NumpyDict: 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() Tensor, Device = _tinygrad_imports()
main_buf = bufs['img'] main_buf = bufs['img']
@@ -154,7 +150,6 @@ class TinygradFusedRunner(ModelRunner):
npy = lambda key: Tensor(self._npy_buffers[key], device='NPY') 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']) frame = self._frame_tensor('img', bufs['img'])
big_frame = self._frame_tensor('big_img', bufs['big_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') policy_inputs['action_t'] = npy('action_t')
vision_out_t, on_out_t, off_out_t = self._run_policy(**policy_inputs) 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: def _slice(tensor_out, meta) -> NumpyDict:
flat = tensor_out.numpy().flatten() flat = tensor_out.numpy().flatten()
return {k: flat[np.newaxis, sl] for k, sl in meta['output_slices'].items() if k != 'pad'} return {k: flat[np.newaxis, sl] for k, sl in meta['output_slices'].items() if k != 'pad'}

View File

@@ -68,8 +68,6 @@ def _is_jit_arg_mismatch(err: BaseException) -> bool:
class TinygradSupercomboRunner(ModelRunner): class TinygradSupercomboRunner(ModelRunner):
"""Runs a single combined supercombo pkl. Bundle ships one `driving_supercombo_*` artifact."""
uses_opencl_warp: bool = False uses_opencl_warp: bool = False
def __init__(self): 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_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() 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])} 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()] sizes = [math.prod(s) for s in shapes.values()]
packed = np.zeros(sum(sizes), dtype=np.float32) packed = np.zeros(sum(sizes), dtype=np.float32)
@@ -318,7 +315,6 @@ class TinygradSupercomboRunner(ModelRunner):
self._npy['traffic_convention'][:] = numpy_inputs['traffic_convention'] self._npy['traffic_convention'][:] = numpy_inputs['traffic_convention']
if 'action_t' in numpy_inputs: if 'action_t' in numpy_inputs:
self._npy['action_t'][:] = numpy_inputs['action_t'] 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']) frame = self._frame_tensor('img', bufs['img'])
big_frame = self._frame_tensor('big_img', bufs['big_img']) big_frame = self._frame_tensor('big_img', bufs['big_img'])
@@ -334,11 +330,10 @@ class TinygradSupercomboRunner(ModelRunner):
raise raise
flat = out.numpy().flatten() 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) 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()} 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: def _run_model(self) -> NumpyDict:
raise RuntimeError("supercombo path goes through run_fused(), not _run_model()") raise RuntimeError("supercombo path goes through run_fused(), not _run_model()")

View File

@@ -85,8 +85,6 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
self.model_run = _load_program_blob(asset_name) self.model_run = _load_program_blob(asset_name)
self._input_plan = _compile_input_plan(self.model_run.captured) 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(): for name, spec in self._input_plan.items():
if "img" in name and spec.dtype is not dtypes.uint8: 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") raise ValueError(f"{asset_name}: image input {name} expects {spec.dtype}, incompatible with uint8 warp buffer")

View File

@@ -1,4 +1,3 @@
# openpilot model I/O constants (comma.ai, MIT — see LICENSE)
import numpy as np import numpy as np
@@ -7,7 +6,6 @@ def index_function(idx, max_val=192, max_idx=32):
class SplitModelConstants: class SplitModelConstants:
# time and distance indices
IDX_N = 33 IDX_N = 33
T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] 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)] 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.] LEAD_T_OFFSETS = [0., 2., 4.]
META_T_IDXS = [2., 4., 6., 8., 10.] META_T_IDXS = [2., 4., 6., 8., 10.]
# split-model temporal / history run parameters
MODEL_FREQ = 20 MODEL_FREQ = 20
HISTORY_FREQ = 5 HISTORY_FREQ = 5
HISTORY_LEN_SECONDS = 5 HISTORY_LEN_SECONDS = 5
@@ -31,7 +28,6 @@ class SplitModelConstants:
LATERAL_CONTROL_PARAMS_LEN = 2 LATERAL_CONTROL_PARAMS_LEN = 2
PREV_DESIRED_CURV_LEN = 1 PREV_DESIRED_CURV_LEN = 1
# model outputs constants
FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32)
FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32)
FCW_5MS2_PROBS_WIDTH = 5 FCW_5MS2_PROBS_WIDTH = 5
@@ -71,7 +67,6 @@ class SplitModelConstants:
POLY_PATH_DEGREE = 4 POLY_PATH_DEGREE = 4
# model outputs slices
class Plan: class Plan:
POSITION = slice(0, 3) POSITION = slice(0, 3)
VELOCITY = slice(3, 6) VELOCITY = slice(3, 6)
@@ -82,14 +77,12 @@ class Plan:
class Meta: class Meta:
ENGAGED = slice(0, 1) ENGAGED = slice(0, 1)
# next 2, 4, 6, 8, 10 seconds
GAS_DISENGAGE = slice(1, 31, 6) GAS_DISENGAGE = slice(1, 31, 6)
BRAKE_DISENGAGE = slice(2, 31, 6) BRAKE_DISENGAGE = slice(2, 31, 6)
STEER_OVERRIDE = slice(3, 31, 6) STEER_OVERRIDE = slice(3, 31, 6)
HARD_BRAKE_3 = slice(4, 31, 6) HARD_BRAKE_3 = slice(4, 31, 6)
HARD_BRAKE_4 = slice(5, 31, 6) HARD_BRAKE_4 = slice(5, 31, 6)
HARD_BRAKE_5 = slice(6, 31, 6) HARD_BRAKE_5 = slice(6, 31, 6)
# next 0, 2, 4, 6, 8, 10 seconds
GAS_PRESS = slice(31, 55, 4) GAS_PRESS = slice(31, 55, 4)
BRAKE_PRESS = slice(32, 55, 4) BRAKE_PRESS = slice(32, 55, 4)
LEFT_BLINKER = slice(33, 55, 4) LEFT_BLINKER = slice(33, 55, 4)

View File

@@ -0,0 +1,3 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""

View File

@@ -1,5 +1,4 @@
// Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ // Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
// clang++ -O2 repro.cc && ./a.out
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>

View File

@@ -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 __future__ import annotations
from types import SimpleNamespace from types import SimpleNamespace

View File

@@ -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 __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass

View File

@@ -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 __future__ import annotations
import numpy as np import numpy as np

View File

@@ -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 __future__ import annotations
import copy import copy

View File

@@ -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 __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass

View File

@@ -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 import metadata, messaging, parser
from iqpilot.selfdrive.iqmodeld.daemon import CaptureStamp, NeuralEngineState from iqpilot.selfdrive.iqmodeld.daemon import CaptureStamp, NeuralEngineState

View File

@@ -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 __future__ import annotations
import os import os
@@ -5,7 +8,6 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
import pytest
from tinygrad.nn.onnx import OnnxRunner from tinygrad.nn.onnx import OnnxRunner
from tinygrad.tensor import Tensor 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(): 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) selector_dirs = _find_selector_dirs(limit=3, require_onnx=True)
assert len(selector_dirs) >= 3 assert len(selector_dirs) >= 3
@@ -134,8 +137,9 @@ def test_three_selector_models_parse_via_share_onnx():
assert policy_raw.size > 0 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): def test_selector_tinygrad_pkls_execute_when_host_compatible(monkeypatch):
if not SHARE_ROOT.is_dir():
return
selector_dirs = _find_selector_dirs(limit=10) selector_dirs = _find_selector_dirs(limit=10)
attempted = 0 attempted = 0
executed = 0 executed = 0
@@ -152,6 +156,10 @@ def test_selector_tinygrad_pkls_execute_when_host_compatible(monkeypatch):
if "/dev/kgsl-3d0" in str(exc): if "/dev/kgsl-3d0" in str(exc):
continue continue
raise raise
except TypeError as exc:
if "DType.__init__()" in str(exc):
continue
raise
assert "pose" in vision_outputs assert "pose" in vision_outputs
assert "plan" in policy_outputs assert "plan" in policy_outputs
@@ -160,4 +168,4 @@ def test_selector_tinygrad_pkls_execute_when_host_compatible(monkeypatch):
break break
if executed == 0: 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"

View File

@@ -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 __future__ import annotations
import hashlib import hashlib
@@ -153,6 +156,31 @@ def test_select_default_model_clears_custom_download_state(tmp_path: Path, monke
assert not pending_restore.exists() 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(): def test_default_model_is_not_resolved_to_manifest_pop_bundle():
pop_bundle = type("Bundle", (), {"internalName": "Pop (Default)", "displayName": "Pop (Default)"})() pop_bundle = type("Bundle", (), {"internalName": "Pop (Default)", "displayName": "Pop (Default)"})()

View File

@@ -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 __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass

View File

@@ -1,3 +1,6 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
import os import os
import pickle import pickle
import re import re

View File

@@ -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) 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) 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_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_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] 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 stride_pad = stride - cam_w
def frame_prepare_tinygrad(input_frame, M_inv): 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) 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) uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
with Context(SPLIT_REDUCEOP=0): with Context(SPLIT_REDUCEOP=0):
y = warp_perspective_tinygrad(input_frame[:cam_h*stride], 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) tc = input_shapes['traffic_convention'] # (1, 2)
at = input_shapes['action_t'] # (1, 2) at = input_shapes['action_t'] # (1, 2)
fb = input_shapes['features_buffer'] # (1, 24, 512) 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])} 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()] 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) shapes, sizes = get_policy_npy_shapes(input_shapes)
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32) 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)}) 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({ input_queues.update({
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(), 'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(),

View File

@@ -648,8 +648,16 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
}, },
EventName.wrongGear: { EventName.wrongGear: {
ET.SOFT_DISABLE: user_soft_disable_alert("Gear not D"), ET.SOFT_DISABLE: Alert(
ET.NO_ENTRY: NoEntryAlert("Gear not D"), "",
"",
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. # This alert is thrown when the calibration angles are outside of the acceptable range.

View File

@@ -269,7 +269,7 @@ _ENGAGE_EVENTS: EVENTS_IQ_TYPE = {
EventNameIQ.steeringOverrideReengageAlc: { EventNameIQ.steeringOverrideReengageAlc: {
ET.WARNING: Alert( ET.WARNING: Alert(
"Steering Overridden By Driver", "Steering Overridden By Driver",
"Re-Engage ALC", "Double Tap SET or Cycle the Cruise Main to Re-Engage ALC",
AlertStatus.userPrompt, AlertSize.mid, AlertStatus.userPrompt, AlertSize.mid,
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 2.0), Priority.MID, VisualAlert.none, AudibleAlert.prompt, 2.0),
}, },
@@ -294,9 +294,9 @@ _CABIN_BLOCK_EVENTS: EVENTS_IQ_TYPE = {
AlertStatus.normal, AlertSize.none, AlertStatus.normal, AlertSize.none,
Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.),
ET.NO_ENTRY: Alert( 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.), Priority.LOW, VisualAlert.none, AudibleAlert.none, 0.),
}, },
@@ -344,11 +344,11 @@ _NOTICE_EVENTS: EVENTS_IQ_TYPE = {
}, },
EventNameIQ.pedalHeldNotice: { EventNameIQ.pedalHeldNotice: {
ET.WARNING: NoEntryAlert("Pedal Held") ET.WARNING: NoEntryAlert("Brake Pedal Held")
}, },
EventNameIQ.experimentalToggled: { 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: { EventNameIQ.e2eChime: {
@@ -365,7 +365,6 @@ _NOTICE_EVENTS: EVENTS_IQ_TYPE = {
priority=Priority.LOW), priority=Priority.LOW),
}, },
# outranks the generic processNotRunning alert so the driver sees why engagement is blocked
EventNameIQ.modelUpdating: { EventNameIQ.modelUpdating: {
ET.NO_ENTRY: NoEntryAlert("Update finishes while parked with internet", ET.NO_ENTRY: NoEntryAlert("Update finishes while parked with internet",
alert_text_1="Driving Model Updating", alert_text_1="Driving Model Updating",

View File

@@ -4,11 +4,12 @@ import os
import random import random
from PIL import Image, ImageDraw, ImageFont 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.cereal.messaging import SubMaster
from iqpilot.common.basedir import BASEDIR from iqpilot.common.basedir import BASEDIR
from iqpilot.common.params import Params from iqpilot.common.params import Params
from iqpilot.selfdrive.selfdrived.events import Alert, EVENTS, ET 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.events import invalid_lkas_setting_alert, invalid_lkas_setting_no_entry_alert
from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from iqpilot.selfdrive.test.process_replay.process_replay import CONFIGS from iqpilot.selfdrive.test.process_replay.process_replay import CONFIGS
@@ -26,6 +27,17 @@ for event_types in EVENTS.values():
class TestAlerts: 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 @classmethod
def setup_class(cls): def setup_class(cls):
with open(OFFROAD_ALERTS_PATH) as f: with open(OFFROAD_ALERTS_PATH) as f:

View File

@@ -1,7 +1,7 @@
[ [
{ {
"name": "xbl", "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": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
"hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6", "hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
"size": 3282256, "size": 3282256,
@@ -12,7 +12,7 @@
}, },
{ {
"name": "xbl_config", "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": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
"hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9", "hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
"size": 98124, "size": 98124,
@@ -23,7 +23,7 @@
}, },
{ {
"name": "abl", "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": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
"hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee", "hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
"size": 274432, "size": 274432,
@@ -34,7 +34,7 @@
}, },
{ {
"name": "aop", "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": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
"hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788", "hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
"size": 184364, "size": 184364,
@@ -45,7 +45,7 @@
}, },
{ {
"name": "devcfg", "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": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
"hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585", "hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
"size": 40336, "size": 40336,
@@ -56,7 +56,7 @@
}, },
{ {
"name": "splash", "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": "993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2",
"hash_raw": "993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2", "hash_raw": "993d7fb8ddfa552bd7f60e8a78b8735efbc716a0978682ed3c92fa0d694528d2",
"size": 34226176, "size": 34226176,
@@ -67,7 +67,7 @@
}, },
{ {
"name": "boot", "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": "aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb",
"hash_raw": "aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb", "hash_raw": "aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb",
"size": 18216960, "size": 18216960,
@@ -78,7 +78,7 @@
}, },
{ {
"name": "system", "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": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa",
"hash_raw": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa", "hash_raw": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa",
"size": 6291456000, "size": 6291456000,

View File

@@ -1 +1 @@
IE+GKvyryxGDx98VImAM+UaqOPcs+mzWtlVs7r0h4Va2Tb9glgFBFlpaB3Btll+QI5zoo/tMlDrj463kaYQQBg== qT72MCHtDUWnARJbSsLUcPaISRZxjFPNf282R9ZC1SXvFJz6X6NmjgknK3OLDLijOQWlGvvJZGsDgz2vsdPVAg==

View File

@@ -9,15 +9,11 @@ import subprocess
import time import time
from collections.abc import Generator 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 import sys
_VENV_PY = "/usr/local/venv/bin/python3" _VENV_PY = "/usr/local/venv/bin/python3"
if sys.executable != _VENV_PY and os.path.exists(_VENV_PY): if sys.executable != _VENV_PY and os.path.exists(_VENV_PY):
try: try:
import Crypto # noqa: F401 probe for the venv-only deps import Crypto # noqa: F401
except ImportError: except ImportError:
os.execv(_VENV_PY, [_VENV_PY, os.path.abspath(__file__), *sys.argv[1:]]) 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" LFS_POINTER_MAGIC = b"version https://git-lfs"
def _image_auth_module(): 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: try:
from iqpilot.system.proprietary_runtime._verified_import import import_verified_module from iqpilot.system.proprietary_runtime._verified_import import import_verified_module
return import_verified_module("iqpilot_updater_private", "iqpilot_private.updater.git_remote") return import_verified_module("iqpilot_updater_private", "iqpilot_private.updater.git_remote")
@@ -63,7 +55,6 @@ def _image_auth_module():
pass pass
return None return None
def _download_headers(url: str) -> dict: def _download_headers(url: str) -> dict:
mod = _image_auth_module() mod = _image_auth_module()
if mod is not None: if mod is not None:
@@ -82,11 +73,7 @@ def _download_headers(url: str) -> dict:
pass pass
return {} return {}
def _open_image_response(url: str) -> requests.Response: 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) auth = _download_headers(url)
req = requests.get(url, stream=True, headers={'Accept-Encoding': None, **auth}, timeout=60) req = requests.get(url, stream=True, headers={'Accept-Encoding': None, **auth}, timeout=60)
req.raise_for_status() 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) meta = dict(line.split(" ", 1) for line in body.decode().strip().splitlines() if " " in line)
oid = meta["oid"].split(":", 1)[1] oid = meta["oid"].split(":", 1)[1]
size = int(meta["size"]) 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" req = requests.get(f"{lfs_base}/objects/{oid}", stream=True,
batch = requests.post(batch_url, 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"], data=json.dumps({"operation": "download", "transfers": ["basic"],
"objects": [{"oid": oid, "size": size}]}), "objects": [{"oid": oid, "size": size}]}),
headers={"Content-Type": "application/vnd.git-lfs+json", 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 = Ed25519PublicKey.from_public_bytes(IQPILOT_MANIFEST_PUBLIC_KEY)
public_key.verify(signature, digest) public_key.verify(signature, digest)
class _ChainedParts: 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 `<name>.pNN` LFS objects; devices re-join them here."""
def __init__(self, urls: list[str]) -> None: def __init__(self, urls: list[str]) -> None:
self.urls = urls self.urls = urls
self.req: requests.Response | None = None self.req: requests.Response | None = None
@@ -148,7 +134,6 @@ class _ChainedParts:
self.req = _open_image_response(u) self.req = _open_image_response(u)
yield from self.req.iter_content(chunk_size=chunk_size) yield from self.req.iter_content(chunk_size=chunk_size)
class StreamingDecompressor: class StreamingDecompressor:
def __init__(self, url: str, parts: int = 0) -> None: def __init__(self, url: str, parts: int = 0) -> None:
self.buf = b"" self.buf = b""
@@ -187,57 +172,48 @@ class StreamingDecompressor:
self.sha256.update(result) self.sha256.update(result)
return result return result
def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]: 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] magic = struct.unpack("I", f.read(4))[0]
assert(magic == 0xed26ff3a) assert(magic == 0xed26ff3a)
# Version
major = struct.unpack("H", f.read(2))[0] major = struct.unpack("H", f.read(2))[0]
minor = struct.unpack("H", f.read(2))[0] minor = struct.unpack("H", f.read(2))[0]
assert(major == 1 and minor == 0) assert(major == 1 and minor == 0)
f.read(2) # file header size f.read(2)
f.read(2) # chunk header size f.read(2)
block_sz = struct.unpack("I", f.read(4))[0] 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] num_chunks = struct.unpack("I", f.read(4))[0]
f.read(4) # crc checksum f.read(4)
for _ in range(num_chunks): for _ in range(num_chunks):
chunk_type, out_blocks = SPARSE_CHUNK_FMT.unpack(f.read(12)) chunk_type, out_blocks = SPARSE_CHUNK_FMT.unpack(f.read(12))
if chunk_type == 0xcac1: # Raw if chunk_type == 0xcac1:
# TODO: yield in smaller chunks. Yielding only block_sz is too slow. Largest observed data chunk is 252 MB.
yield f.read(out_blocks * block_sz) yield f.read(out_blocks * block_sz)
elif chunk_type == 0xcac2: # Fill elif chunk_type == 0xcac2:
filler = f.read(4) * (block_sz // 4) filler = f.read(4) * (block_sz // 4)
for _ in range(out_blocks): for _ in range(out_blocks):
yield filler yield filler
elif chunk_type == 0xcac3: # Don't care elif chunk_type == 0xcac3:
yield b"" yield b""
else: else:
raise Exception("Unhandled sparse chunk type") 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]: def noop(f: StreamingDecompressor) -> Generator[bytes, None, None]:
while len(chunk := f.read(1024 * 1024)) > 0: while len(chunk := f.read(1024 * 1024)) > 0:
yield chunk yield chunk
def get_target_slot_number() -> int: def get_target_slot_number() -> int:
current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip() current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip()
return 1 if current_slot == "_a" else 0 return 1 if current_slot == "_a" else 0
def slot_number_to_suffix(slot_number: int) -> str: def slot_number_to_suffix(slot_number: int) -> str:
assert slot_number in (0, 1) assert slot_number in (0, 1)
return '_a' if slot_number == 0 else '_b' return '_a' if slot_number == 0 else '_b'
def get_partition_path(target_slot_number: int, partition: dict) -> str: def get_partition_path(target_slot_number: int, partition: dict) -> str:
path = f"/dev/disk/by-partlabel/{partition['name']}" 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 return path
def get_raw_hash(path: str, partition_size: int) -> str: def get_raw_hash(path: str, partition_size: int) -> str:
raw_hash = hashlib.sha256() raw_hash = hashlib.sha256()
pos, chunk_size = 0, 1024 * 1024 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() return raw_hash.hexdigest().lower()
def verify_partition(target_slot_number: int, partition: dict[str, str | int], force_full_check: bool = False) -> bool: 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 full_check = partition['full_check'] or force_full_check
path = get_partition_path(target_slot_number, partition) 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) out.seek(partition_size)
return out.read(64) == partition_hash.lower().encode() return out.read(64) == partition_hash.lower().encode()
def clear_partition_hash(target_slot_number: int, partition: dict) -> None: def clear_partition_hash(target_slot_number: int, partition: dict) -> None:
path = get_partition_path(target_slot_number, partition) path = get_partition_path(target_slot_number, partition)
with open(path, 'wb+') as out: 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) out.write(b"\x00" * 64)
os.sync() os.sync()
def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog): def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog):
path = get_partition_path(target_slot_number, partition) path = get_partition_path(target_slot_number, partition)
downloader = StreamingDecompressor(partition['url'], parts=int(partition.get('url_parts', 0))) downloader = StreamingDecompressor(partition['url'], parts=int(partition.get('url_parts', 0)))
with open(path, 'wb+') as out: with open(path, 'wb+') as out:
# Flash partition
last_p = 0 last_p = 0
raw_hash = hashlib.sha256() raw_hash = hashlib.sha256()
f = unsparsify if partition['sparse'] else noop f = unsparsify if partition['sparse'] else noop
@@ -320,7 +291,6 @@ def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog)
os.sync() os.sync()
def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): def extract_casync_image(target_slot_number: int, partition: dict, cloudlog):
path = get_partition_path(target_slot_number, partition) path = get_partition_path(target_slot_number, partition)
seed_path = path[:-1] + ('b' if path[-1] == 'a' else 'a') 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]] = [] sources: list[tuple[str, casync.ChunkReader, casync.ChunkDict]] = []
# First source is the current partition.
try: try:
raw_hash = get_raw_hash(seed_path, partition['size']) raw_hash = get_raw_hash(seed_path, partition['size'])
caibx_url = f"{CAIBX_URL}{partition['name']}-{raw_hash}.caibx" 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: except Exception:
cloudlog.exception("casync failed to hash seed partition") 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))] 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))] sources += [('remote', casync.RemoteChunkReader(partition['casync_store']), casync.build_chunk_dict(target))]
last_p = 0 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): if not verify_partition(target_slot_number, partition, force_full_check=True):
raise Exception(f"Raw hash mismatch '{partition['hash_raw'].lower()}'") raise Exception(f"Raw hash mismatch '{partition['hash_raw'].lower()}'")
def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalone=False): def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalone=False):
cloudlog.info(f"Downloading and writing {partition['name']}") 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']}") cloudlog.info(f"Already flashed {partition['name']}")
return return
# Clear hash before flashing in case we get interrupted
full_check = partition['full_check'] full_check = partition['full_check']
if not full_check: if not full_check:
clear_partition_hash(target_slot_number, partition) clear_partition_hash(target_slot_number, partition)
@@ -384,13 +349,11 @@ def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalo
else: else:
extract_compressed_image(target_slot_number, partition, cloudlog) extract_compressed_image(target_slot_number, partition, cloudlog)
# Write hash after successful flash
if not full_check: if not full_check:
with open(path, 'wb+') as out: with open(path, 'wb+') as out:
out.seek(partition['size']) out.seek(partition['size'])
out.write(partition['hash_raw'].lower().encode()) out.write(partition['hash_raw'].lower().encode())
def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None: def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None:
verify_manifest_signature(manifest_path) verify_manifest_signature(manifest_path)
update = json.load(open(manifest_path)) update = json.load(open(manifest_path))
@@ -406,14 +369,12 @@ def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None:
else: else:
cloudlog.error(f"Swap failed {out}") cloudlog.error(f"Swap failed {out}")
def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, standalone=False) -> None: def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, standalone=False) -> None:
verify_manifest_signature(manifest_path) verify_manifest_signature(manifest_path)
update = json.load(open(manifest_path)) update = json.load(open(manifest_path))
cloudlog.info(f"Target slot {target_slot_number}") cloudlog.info(f"Target slot {target_slot_number}")
# set target slot as unbootable
os.system(f"abctl --set_unbootable {target_slot_number}") os.system(f"abctl --set_unbootable {target_slot_number}")
for partition in update: 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}") cloudlog.info(f"AGNOS ready on slot {target_slot_number}")
def verify_agnos_update(manifest_path: str, target_slot_number: int) -> bool: def verify_agnos_update(manifest_path: str, target_slot_number: int) -> bool:
verify_manifest_signature(manifest_path) verify_manifest_signature(manifest_path)
update = json.load(open(manifest_path)) update = json.load(open(manifest_path))
return all(verify_partition(target_slot_number, partition) for partition in update) return all(verify_partition(target_slot_number, partition) for partition in update)
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
import logging import logging

View File

@@ -1,7 +1,7 @@
[ [
{ {
"name": "xbl", "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": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
"hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6", "hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
"size": 3282256, "size": 3282256,
@@ -12,7 +12,7 @@
}, },
{ {
"name": "xbl_config", "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": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
"hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9", "hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
"size": 98124, "size": 98124,
@@ -23,7 +23,7 @@
}, },
{ {
"name": "abl", "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": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
"hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", "hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
"size": 274432, "size": 274432,
@@ -34,7 +34,7 @@
}, },
{ {
"name": "aop", "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": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
"hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788", "hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
"size": 184364, "size": 184364,
@@ -45,7 +45,7 @@
}, },
{ {
"name": "devcfg", "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": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
"hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585", "hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
"size": 40336, "size": 40336,
@@ -56,7 +56,7 @@
}, },
{ {
"name": "boot", "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": "aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb",
"hash_raw": "aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb", "hash_raw": "aea4aecefd188d9c95726b699902378676c6266a7ae37008b5c2aa0e1e1190fb",
"size": 18216960, "size": 18216960,
@@ -67,7 +67,7 @@
}, },
{ {
"name": "system", "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": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa",
"hash_raw": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa", "hash_raw": "44b251e1b3d8cd9243d5c79287d2a0e74b5063d3e21b24192b1293430a3471aa",
"size": 6291456000, "size": 6291456000,

View File

@@ -1 +1 @@
9LDZugtT9q8jFab1Gs8qTmfJukKU8NzDAuT9VakInuOUHCCnN5SDFI/Ew/6C/+3SAlcROgZON/4J8FQyQGV+Dg== fL46Z+k/wqjavP3J1S/VzE90BXcvvRF+S41MgHEXYjW+Jlnu5REOp6rvp3SJFEkKIEaiSf9PsHyiDgQkC6J2Aw==

View File

@@ -8,9 +8,8 @@ MANIFESTS = [
os.path.join(TEST_DIR, "../agnos_tici_15_1.json"), 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" XZ_MAGIC = b"\xfd7zXZ\x00"
LFS_POINTER_MAGIC = b"version https://git-lfs" LFS_POINTER_MAGIC = b"version https://git-lfs"
@@ -27,12 +26,6 @@ class TestAgnosUpdater:
if not img['sparse']: if not img['sparse']:
assert img['hash'] == img['hash_raw'] 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 = requests.Session()
s.trust_env = False s.trust_env = False
r = s.get(img['url'], timeout=10, stream=True, r = s.get(img['url'], timeout=10, stream=True,

View File

@@ -43,8 +43,6 @@ def required_agnos_version(install_path: str) -> str:
def _hardware_dir(install_path: str) -> str: def _hardware_dir(install_path: str) -> str:
# the nested layout keeps the package at <install>/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") nested = os.path.join(install_path, "iqpilot", "system", "hardware", "tici")
if os.path.isdir(nested): if os.path.isdir(nested):
return nested return nested