forked from IQ.Lvbs/IQ.Pilot
134 lines
5.3 KiB
Python
134 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Real-CAN replay invariant tests for VW torque platforms (PQ / MQB / MLB).
|
|
|
|
Replays real konn3kt routes through the car interface with openpilot lateral
|
|
INACTIVE (latActive=False) and asserts openpilot never transmits an active-steering
|
|
HCA command (active status or non-zero torque). Re-transmitting the stock camera's
|
|
active HCA while not in control (stock-LKAS forwarding) leaves the EPS faulted for
|
|
the whole drive (LH2_Sta_HCA=FAULT) - the regression that bricked steering on a PQ
|
|
Passat NMS with a factory LKAS camera. Panda accepts these frames, so only a replay
|
|
invariant like this catches it.
|
|
|
|
Self-contained within iqdbc: routes are resolved through konn3kt's public
|
|
/v1/route/<id>/files endpoint (URLs are signed server-side, no auth/token needed).
|
|
"""
|
|
import json
|
|
import os
|
|
import urllib.parse
|
|
import urllib.request
|
|
from collections import Counter
|
|
|
|
import pytest
|
|
|
|
from iqdbc.can.parser import CANParser
|
|
from iqdbc.car import Bus, structs
|
|
from iqdbc.car.can_definitions import CanData
|
|
from iqdbc.car.car_helpers import can_fingerprint, interfaces
|
|
from iqdbc.car.logreader import LogReader
|
|
from iqdbc.car.volkswagen.values import CAR, DBC, VolkswagenFlags
|
|
|
|
API_HOST = os.environ.get("API_HOST", "https://api-iqlabs.konn3kt.com")
|
|
|
|
# konn3kt-hosted VW routes. (route_id, segment, platform, label)
|
|
# Add MQB routes here as konn3kt-hosted MQB logs become available.
|
|
VW_ROUTES = [
|
|
("b29ee8c5a0a735d1|000000dc--a384e9083e", 0, CAR.VOLKSWAGEN_PASSAT_NMS, "PQ with stock LKAS camera"),
|
|
("0f53129ed44f6920|00000287--3efbddeb96", 0, CAR.VOLKSWAGEN_PASSAT_NMS, "PQ without stock LKAS camera"),
|
|
]
|
|
|
|
# Per-platform HCA message: (address, msg, status signal, torque signal, active-status values)
|
|
HCA_INFO = {
|
|
"pq": (0xD2, "HCA_1", "HCA_Status", "LM_Offset", (5, 7)),
|
|
"mqb": (0x126, "HCA_01", "HCA_01_Status_HCA", "HCA_01_LM_Offset", (5, 6, 7)),
|
|
}
|
|
|
|
|
|
def _request_headers() -> dict[str, str]:
|
|
# konn3kt's edge rejects the default urllib User-Agent with 403. The routes are public
|
|
# (access returns early for public routes), but IQ.Pilot/Cabana tooling conventionally
|
|
# sends a Konn3kt user JWT, so include one when available (env or ~/.comma/auth.json).
|
|
headers = {"User-Agent": "iqdbc"}
|
|
token = os.environ.get("KONN3KT_ACCESS_TOKEN")
|
|
if not token:
|
|
try:
|
|
with open(os.path.expanduser("~/.comma/auth.json")) as f:
|
|
token = json.load(f).get("access_token")
|
|
except (OSError, ValueError):
|
|
token = None
|
|
if token:
|
|
headers["Authorization"] = f"JWT {token}"
|
|
return headers
|
|
|
|
|
|
def _rlog_url(route_id: str, segment: int) -> str:
|
|
req = urllib.request.Request(f"{API_HOST}/v1/route/{urllib.parse.quote(route_id, safe='|')}/files",
|
|
headers=_request_headers())
|
|
with urllib.request.urlopen(req, timeout=30) as f:
|
|
files = json.load(f)
|
|
for url in files.get("logs", []):
|
|
# path looks like /connectdata/<dongle>/<log>/<seg>/rlog.zst
|
|
parts = urllib.parse.urlparse(url).path.rstrip("/").split("/")
|
|
if len(parts) >= 2 and parts[-2] == str(segment):
|
|
return url
|
|
raise RuntimeError(f"no rlog for {route_id} segment {segment} (uploaded & public?)")
|
|
|
|
|
|
def _load_can(route_id: str, segment: int):
|
|
lr = LogReader(_rlog_url(route_id, segment), only_union_types=True, sort_by_time=True)
|
|
return [(m.logMonoTime, [CanData(c.address, c.dat, c.src) for c in m.can]) for m in lr if m.which() == "can"]
|
|
|
|
|
|
@pytest.mark.parametrize("route_id,segment,platform,label", VW_ROUTES)
|
|
def test_vw_inactive_steering_invariant(route_id, segment, platform, label):
|
|
can_msgs = _load_can(route_id, segment)
|
|
assert len(can_msgs) > 1000, f"insufficient CAN data for {label}: {len(can_msgs)} frames"
|
|
|
|
# fingerprint from a fresh iterator over the (unmutated) frame list
|
|
frame_iter = (frames for _, frames in can_msgs)
|
|
def can_recv(wait_for_one: bool = False):
|
|
return [next(frame_iter, [])]
|
|
_, fingerprint = can_fingerprint(can_recv)
|
|
|
|
CarInterface = interfaces[platform]
|
|
CP = CarInterface.get_params(platform, fingerprint, [], False, False, False)
|
|
CP_IQ = CarInterface.get_params_iq(CP, platform, fingerprint, [], False, False, False)
|
|
|
|
if CP.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO):
|
|
pytest.skip("invariant covers torque-based VW platforms (PQ/MQB/MLB)")
|
|
|
|
key = "pq" if CP.flags & VolkswagenFlags.PQ else "mqb"
|
|
hca_addr, hca_msg, status_sig, torque_sig, active_status = HCA_INFO[key]
|
|
cp = CANParser(DBC[platform][Bus.pt], [(hca_msg, 0)], 0)
|
|
|
|
CI = CarInterface(CP, CP_IQ)
|
|
CC = structs.CarControl().as_reader() # latActive defaults to False
|
|
CC_IQ = structs.IQCarControl()
|
|
|
|
hca_seen = 0
|
|
violations = Counter()
|
|
for i, (mono, frames) in enumerate(can_msgs):
|
|
CI.update([(mono, frames)])
|
|
_, sendcan = CI.apply(CC, CC_IQ, mono)
|
|
|
|
if i < 300: # CarController / CANParser warmup
|
|
continue
|
|
|
|
for addr, dat, bus in sendcan:
|
|
if addr != hca_addr or bus != 0:
|
|
continue
|
|
hca_seen += 1
|
|
cp.update([(mono, [(addr, bytes(dat), 0)])])
|
|
if int(cp.vl[hca_msg][status_sig]) in active_status:
|
|
violations["active_status"] += 1
|
|
if abs(cp.vl[hca_msg][torque_sig]) > 0:
|
|
violations["nonzero_torque"] += 1
|
|
|
|
assert hca_seen > 50, f"{label}: no HCA steering messages transmitted to inspect"
|
|
assert not len(violations), \
|
|
f"{label}: openpilot TX'd active HCA while latActive=False: {dict(violations)}"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
sys.exit(pytest.main([__file__, "-v"]))
|