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()
|
||||
|
||||
Reference in New Issue
Block a user