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