IQ.Pilot Release Commit @ e46d557
This commit is contained in:
@@ -208,6 +208,7 @@ struct CarState {
|
||||
blockPcmEnable @60 :Bool; # whether to allow PCM to enable this frame
|
||||
lateralAvailable @61 :Bool; # lateral control is available even if cruise is faulted
|
||||
cruiseFaultLateralMode @62 :Bool; # cruise is faulted but lateral control is still active
|
||||
carNotReady @95 :Bool; # car is transiently refusing engagement, not a fault
|
||||
radarDisableFailed @66 :Bool;
|
||||
# Physical vehicle odometer in kilometers. Zero means unavailable on this platform.
|
||||
odometer @67 :Float64;
|
||||
|
||||
@@ -53,6 +53,7 @@ class IQCarParams:
|
||||
enableGasInterceptor: bool = auto_field()
|
||||
longitudinalStoppingSpeedOverride: float = auto_field()
|
||||
stoppingDecelRateOverride: float = auto_field()
|
||||
longActiveWithGasOverride: bool = auto_field()
|
||||
|
||||
iqLateralNet: 'IQCarParams.LateralNet' = field(default_factory=lambda: IQCarParams.LateralNet())
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ class CarState(CarStateBase, IQCarState):
|
||||
|
||||
self.hands_on_level = 0
|
||||
self.acc_state_last = 0
|
||||
self.das_accCancel = False
|
||||
self.das_cancel_last = True
|
||||
self.das_control = None
|
||||
self.das_body_controls_dat = b""
|
||||
self._odometer_store = vehicle_state.VehicleOdometerStore(CP, Params())
|
||||
@@ -93,15 +95,24 @@ class CarState(CarStateBase, IQCarState):
|
||||
cruise_state = self.can_define.dv["DI_state"]["DI_cruiseState"].get(int(cp_party.vl["DI_state"]["DI_cruiseState"]), None)
|
||||
speed_units = self.can_define.dv["DI_state"]["DI_speedUnits"].get(int(cp_party.vl["DI_state"]["DI_speedUnits"]), None)
|
||||
acc_state = cp_ap_party.vl["DAS_control"]["DAS_accState"]
|
||||
# Respect all stock DAS cancel states, not just ACC_CANCEL_GENERIC_SILENT(13).
|
||||
# ELDA/ELK triggers ACC_CANCEL_GENERIC(0) which must also be forwarded.
|
||||
self.das_accCancel = acc_state in (0, 1, 2, 12, 13, 14, 15)
|
||||
|
||||
summon_state = self.can_define.dv["DI_state"]["DI_autoparkState"].get(int(cp_party.vl["DI_state"]["DI_autoparkState"]), None)
|
||||
cruise_enabled = cruise_state in ("ENABLED", "STANDSTILL", "OVERRIDE", "PRE_FAULT", "PRE_CANCEL")
|
||||
self.cruise_override = cruise_state in ("OVERRIDE")
|
||||
self.update_summon_state(summon_state, cruise_enabled)
|
||||
|
||||
# Respect all stock DAS cancel states, not just ACC_CANCEL_GENERIC_SILENT(13).
|
||||
# ELDA/ELK triggers ACC_CANCEL_GENERIC(0) which must also be forwarded.
|
||||
# The stock AP is isolated from the party bus while the relay is closed, so its accState
|
||||
# free-runs between ACC_ON and ACC_CANCEL_GENERIC. Only a rising edge while ACC is engaged
|
||||
# is a real cancel; level-forwarding it pins DI_cruiseState to UNAVAILABLE and blocks engaging.
|
||||
das_cancel = acc_state in (0, 1, 2, 12, 13, 14, 15)
|
||||
if not cruise_enabled:
|
||||
self.das_accCancel = False
|
||||
elif das_cancel and not self.das_cancel_last:
|
||||
self.das_accCancel = True
|
||||
self.das_cancel_last = das_cancel
|
||||
|
||||
# Match panda safety cruise engaged logic
|
||||
ret.cruiseState.enabled = cruise_enabled and not self.summon
|
||||
if speed_units == "KPH":
|
||||
|
||||
@@ -44,6 +44,7 @@ FW_VERSIONS = {
|
||||
b'TeMYG4_Legacy3Y_0.0.0 (6),Y4003.04.0',
|
||||
b'TeMYG4_Main_0.0.0 (77),Y4003.05.4',
|
||||
b'TeMYG4_Main_0.0.0 (78),Y4003.06.0',
|
||||
b'TeMYG4_Main_0.0.0 (87),Y4003.09.3',
|
||||
],
|
||||
},
|
||||
CAR.TESLA_MODEL_X: {
|
||||
|
||||
@@ -2,7 +2,7 @@ from iqdbc.car import Bus, get_safety_config, structs
|
||||
from iqdbc.car.interfaces import CarInterfaceBase
|
||||
from iqdbc.car.tesla.carcontroller import CarController
|
||||
from iqdbc.car.tesla.carstate import CarState
|
||||
from iqdbc.car.tesla.values import TeslaSafetyFlags, TeslaFlags, CANBUS, CAR, DBC, LEGACY_DAS_STEERING_FW, Ecu
|
||||
from iqdbc.car.tesla.values import TeslaSafetyFlags, TeslaFlags, CANBUS, CAR, DBC, Ecu, is_legacy_das_steering
|
||||
from iqdbc.car.tesla.radar_interface import RadarInterface, RADAR_START_ADDR
|
||||
|
||||
from iqdbc.lvbs.car.tesla.values import TeslaFlagsIQ, TeslaSafetyFlagsIQ
|
||||
@@ -41,7 +41,7 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.openpilotLongitudinalControl = True
|
||||
ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.LONG_CONTROL.value
|
||||
|
||||
legacy_das = any(fw.ecu == Ecu.eps and fw.fwVersion in LEGACY_DAS_STEERING_FW.get(candidate, []) for fw in car_fw)
|
||||
legacy_das = any(fw.ecu == Ecu.eps and is_legacy_das_steering(candidate, fw.fwVersion) for fw in car_fw)
|
||||
if legacy_das:
|
||||
ret.flags |= TeslaFlags.LEGACY_DAS_STEERING.value
|
||||
ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.LEGACY_DAS_STEERING.value
|
||||
|
||||
@@ -1,11 +1,110 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import pytest
|
||||
|
||||
from iqdbc.car import gen_empty_fingerprint, structs
|
||||
from iqdbc.car.structs import CarParams
|
||||
from iqdbc.car.fw_versions import match_fw_to_car
|
||||
from iqdbc.car.tesla.fingerprints import FW_VERSIONS
|
||||
from iqdbc.car.tesla.interface import CarInterface
|
||||
from iqdbc.car.tesla.teslacan import TeslaCAN
|
||||
from iqdbc.car.tesla.radar_interface import RADAR_START_ADDR
|
||||
from iqdbc.car.tesla.carcontroller import CarController
|
||||
from iqdbc.car.tesla.values import CAR
|
||||
from iqdbc.car.tesla.values import (CAR, FW_PATTERN, LEGACY_DAS_STEERING_FW, TeslaFlags, TeslaSafetyFlags,
|
||||
get_platform_codes, is_legacy_das_steering)
|
||||
from iqdbc.can import CANPacker, CANParser
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
EPS_ADDR = 0x730
|
||||
|
||||
|
||||
def fw_match(fw: bytes):
|
||||
car_fw = [CarParams.CarFw(ecu=Ecu.eps, fwVersion=fw, address=EPS_ADDR, subAddress=0, brand='tesla')]
|
||||
exact, matches = match_fw_to_car(car_fw, '0' * 17, log=False)
|
||||
return exact, matches
|
||||
|
||||
|
||||
class TestTeslaFwPattern:
|
||||
def test_all_known_fw_parses(self):
|
||||
for car, ecus in FW_VERSIONS.items():
|
||||
for fws in ecus.values():
|
||||
for fw in fws:
|
||||
assert FW_PATTERN.match(fw) is not None, f'{car}: unparsed FW version: {fw}'
|
||||
|
||||
def test_model_code_identifies_one_platform(self):
|
||||
# a new platform reusing an existing model code would silently misfingerprint
|
||||
platforms = defaultdict(set)
|
||||
for car, ecus in FW_VERSIONS.items():
|
||||
for fws in ecus.values():
|
||||
for model, _, _ in get_platform_codes(fws):
|
||||
platforms[model].add(car)
|
||||
|
||||
for model, cars in platforms.items():
|
||||
assert len(cars) == 1, f'model code {model} maps to multiple platforms: {cars}'
|
||||
|
||||
def test_exact_match_still_wins(self):
|
||||
for car, ecus in FW_VERSIONS.items():
|
||||
for fws in ecus.values():
|
||||
for fw in fws:
|
||||
exact, matches = fw_match(fw)
|
||||
assert exact, f'{fw} fell back to fuzzy matching'
|
||||
assert matches == {car}, f'{fw} matched {matches}, expected {car}'
|
||||
|
||||
@pytest.mark.parametrize("fw, expected", [
|
||||
# a firmware bump within a known series, the case that used to fingerprint as MOCK
|
||||
(b'TeMYG4_Main_0.0.0 (99),Y4003.14.0', CAR.TESLA_MODEL_Y),
|
||||
(b'TeMYG4_Main_0.0.0 (99),E4H015.09.0', CAR.TESLA_MODEL_3),
|
||||
(b'TeM3_SP_XP002p2_0.0.0 (40),XPR003.12.0', CAR.TESLA_MODEL_X),
|
||||
# Tesla bumps the series within a platform (E4014 -> E4015, Y4002 -> Y4003)
|
||||
(b'TeMYG4_Main_0.0.0 (12),Y4004.01.0', CAR.TESLA_MODEL_Y),
|
||||
# an unknown model code is a car we don't support
|
||||
(b'TeCT_Main_0.0.0 (1),CT001.01.0', None),
|
||||
(b'garbage', None),
|
||||
])
|
||||
def test_unknown_fw_fuzzy_match(self, fw, expected):
|
||||
exact, matches = fw_match(fw)
|
||||
if expected is None:
|
||||
assert matches == set(), f'{fw} unexpectedly matched {matches}'
|
||||
else:
|
||||
assert not exact
|
||||
assert matches == {expected}
|
||||
|
||||
|
||||
class TestTeslaLegacyDasSteering:
|
||||
def test_reproduces_known_table(self):
|
||||
for car, ecus in FW_VERSIONS.items():
|
||||
for fws in ecus.values():
|
||||
for fw in fws:
|
||||
expected = fw in LEGACY_DAS_STEERING_FW.get(car, [])
|
||||
assert is_legacy_das_steering(car, fw) == expected, f'{car}: wrong legacy DAS verdict for {fw}'
|
||||
|
||||
@pytest.mark.parametrize("car, fw, expected", [
|
||||
# below a family's known modern cutoff (Y4/003 splits at 003.04.0)
|
||||
(CAR.TESLA_MODEL_Y, b'TeMYG4_Legacy3Y_0.0.0 (5),Y4003.03.9', True),
|
||||
(CAR.TESLA_MODEL_Y, b'TeMYG4_Main_0.0.0 (99),Y4003.14.0', False),
|
||||
# E4/015 splits at 015.04.5
|
||||
(CAR.TESLA_MODEL_3, b'TeMYG4_Main_0.0.0 (68),E4H015.03.9', True),
|
||||
(CAR.TESLA_MODEL_3, b'TeMYG4_Main_0.0.0 (99),E4H015.09.0', False),
|
||||
# families with no known modern FW: interpolate legacy, extrapolate modern
|
||||
(CAR.TESLA_MODEL_3, b'TeM3_E014p10_0.0.0 (16),E014.18.00', True),
|
||||
(CAR.TESLA_MODEL_3, b'TeM3_E014p10_0.0.0 (30),E014.22.0', False),
|
||||
# numeric, not lexical, version compare (XPR003.10.0 > XPR003.6.0)
|
||||
(CAR.TESLA_MODEL_X, b'TeM3_SP_XP002p2_0.0.0 (30),XPR003.9.0', True),
|
||||
# an unknown series has no history to compare against
|
||||
(CAR.TESLA_MODEL_Y, b'TeMYG4_Main_0.0.0 (12),Y4004.01.0', False),
|
||||
(CAR.TESLA_MODEL_Y, b'garbage', False),
|
||||
])
|
||||
def test_unknown_fw(self, car, fw, expected):
|
||||
assert is_legacy_das_steering(car, fw) == expected
|
||||
|
||||
def test_flag_set_from_fuzzy_match(self):
|
||||
for fw, legacy in ((b'TeMYG4_Legacy3Y_0.0.0 (5),Y4003.03.9', True),
|
||||
(b'TeMYG4_Main_0.0.0 (99),Y4003.14.0', False)):
|
||||
car_fw = [CarParams.CarFw(ecu=Ecu.eps, fwVersion=fw, address=EPS_ADDR, subAddress=0, brand='tesla')]
|
||||
CP = CarInterface.get_params(CAR.TESLA_MODEL_Y, gen_empty_fingerprint(), car_fw, False, False, False)
|
||||
assert bool(CP.flags & TeslaFlags.LEGACY_DAS_STEERING) == legacy
|
||||
assert bool(CP.safetyConfigs[0].safetyParam & TeslaSafetyFlags.LEGACY_DAS_STEERING) == legacy
|
||||
|
||||
|
||||
class TestTeslaFingerprint:
|
||||
def test_radar_detection(self):
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, IntFlag
|
||||
from functools import cache
|
||||
from iqdbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, CarSpecs, DbcDict, PlatformConfig, Platforms
|
||||
from iqdbc.car.lateral import AngleSteeringLimits, ISO_LATERAL_ACCEL
|
||||
from iqdbc.car.structs import CarParams, CarState
|
||||
from iqdbc.car.docs_definitions import CarDocs, CarFootnote, CarHarness, CarParts, Column, SupportType
|
||||
from iqdbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
|
||||
from iqdbc.car.fw_query_definitions import FwQueryConfig, LiveFwVersions, OfflineFwVersions, Request, StdQueries
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
|
||||
@@ -69,16 +72,6 @@ class CAR(Platforms):
|
||||
)
|
||||
|
||||
|
||||
FW_QUERY_CONFIG = FwQueryConfig(
|
||||
requests=[
|
||||
Request(
|
||||
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.SUPPLIER_SOFTWARE_VERSION_REQUEST],
|
||||
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.SUPPLIER_SOFTWARE_VERSION_RESPONSE],
|
||||
bus=0,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Cars with this EPS FW have a 2-bit DAS_steeringControlType and use TeslaFlags.LEGACY_DAS_STEERING
|
||||
LEGACY_DAS_STEERING_FW = {
|
||||
CAR.TESLA_MODEL_3: [
|
||||
@@ -116,6 +109,99 @@ LEGACY_DAS_STEERING_FW = {
|
||||
],
|
||||
}
|
||||
|
||||
# e.g. TeMYG4_Main_0.0.0 (87),Y4003.09.3
|
||||
# 11111_22222_______33____45__666666
|
||||
# 1 = EPS firmware program, 2 = build lineage, 3 = build number, 4 = model code,
|
||||
# 5 = trim/hardware variant, 6 = series and software version
|
||||
#
|
||||
# Only the model code identifies the vehicle: 1 and 2 are shared across models (Model 3 and
|
||||
# Model Y both ship TeM3_ and TeMYG4_ firmware) and 3 is only monotone within one lineage.
|
||||
FW_PATTERN = re.compile(rb'^Te[A-Z0-9]+_[A-Za-z0-9_]+_0\.0\.0 \(\d+\),' +
|
||||
rb'(?P<model>E4|E|Y4|Y|XP)[A-Z]{0,2}(?P<series>\d{3})\.(?P<version>\d+(?:\.\d+)*)$')
|
||||
|
||||
|
||||
def get_platform_codes(fw_versions: list[bytes] | set[bytes]) -> set[tuple[bytes, bytes, tuple[int, ...]]]:
|
||||
codes = set()
|
||||
for fw in fw_versions:
|
||||
match = FW_PATTERN.match(fw)
|
||||
if match is not None:
|
||||
codes.add((match.group('model'), match.group('series'),
|
||||
tuple(int(v) for v in match.group('version').split(b'.'))))
|
||||
|
||||
return codes
|
||||
|
||||
|
||||
@cache
|
||||
def _das_steering_cutoffs() -> dict[tuple[str, bytes, bytes], tuple[tuple[int, ...] | None, tuple[int, ...] | None]]:
|
||||
"""Per (platform, model code, series) family, the oldest known modern version and the newest
|
||||
known legacy version. Tesla only ever moves a family forward, so these bound the split."""
|
||||
# imported here because fingerprints.py imports this module
|
||||
from iqdbc.car.tesla.fingerprints import FW_VERSIONS
|
||||
|
||||
legacy: defaultdict[tuple, set] = defaultdict(set)
|
||||
modern: defaultdict[tuple, set] = defaultdict(set)
|
||||
for platform, ecus in FW_VERSIONS.items():
|
||||
known_legacy = LEGACY_DAS_STEERING_FW.get(platform, [])
|
||||
for fws in ecus.values():
|
||||
for fw in fws:
|
||||
for model, series, version in get_platform_codes([fw]):
|
||||
(legacy if fw in known_legacy else modern)[(platform, model, series)].add(version)
|
||||
|
||||
return {k: (min(modern[k]) if k in modern else None, max(legacy[k]) if k in legacy else None)
|
||||
for k in set(legacy) | set(modern)}
|
||||
|
||||
|
||||
def is_legacy_das_steering(candidate: str, fw: bytes) -> bool:
|
||||
"""Whether an EPS FW uses the 2-bit DAS_steeringControlType. Unknown firmware newer than
|
||||
anything in a family is treated as modern: cars only move forward, and someone left behind
|
||||
on legacy software can force the platform with CarPlatformBundle."""
|
||||
if fw in LEGACY_DAS_STEERING_FW.get(candidate, []):
|
||||
return True
|
||||
|
||||
codes = get_platform_codes([fw])
|
||||
if not len(codes):
|
||||
return False
|
||||
|
||||
model, series, version = next(iter(codes))
|
||||
first_modern, last_legacy = _das_steering_cutoffs().get((candidate, model, series), (None, None))
|
||||
if first_modern is not None:
|
||||
return version < first_modern
|
||||
|
||||
return last_legacy is not None and version <= last_legacy
|
||||
|
||||
|
||||
def match_fw_to_car_fuzzy(live_fw_versions: LiveFwVersions, vin: str, offline_fw_versions: OfflineFwVersions) -> set[str]:
|
||||
# Tesla fingerprints on the EPS alone and Ecu.eps is in FUZZY_EXCLUDE_ECUS, so the generic fuzzy
|
||||
# matcher can never match a Tesla. Match on the model code, which survives the EPS version bumps
|
||||
# that ship with Tesla software updates. The series is deliberately not required to be known:
|
||||
# Tesla bumps it within a platform (E4014 -> E4015, Y4002 -> Y4003).
|
||||
offline_codes: defaultdict[bytes, set[str]] = defaultdict(set)
|
||||
for candidate, ecus in offline_fw_versions.items():
|
||||
for fws in ecus.values():
|
||||
for model, _, _ in get_platform_codes(fws):
|
||||
offline_codes[model].add(candidate)
|
||||
|
||||
candidates: set[str] = set()
|
||||
for ecu, addr, sub_addr in {e for ecus in offline_fw_versions.values() for e in ecus}:
|
||||
if ecu != Ecu.eps:
|
||||
continue
|
||||
for model, _, _ in get_platform_codes(live_fw_versions.get((addr, sub_addr), set())):
|
||||
candidates |= offline_codes[model]
|
||||
|
||||
return candidates if len(candidates) == 1 else set()
|
||||
|
||||
|
||||
FW_QUERY_CONFIG = FwQueryConfig(
|
||||
requests=[
|
||||
Request(
|
||||
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.SUPPLIER_SOFTWARE_VERSION_REQUEST],
|
||||
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.SUPPLIER_SOFTWARE_VERSION_RESPONSE],
|
||||
bus=0,
|
||||
)
|
||||
],
|
||||
match_fw_to_car_fuzzy=match_fw_to_car_fuzzy,
|
||||
)
|
||||
|
||||
|
||||
class CANBUS:
|
||||
party = 0
|
||||
|
||||
@@ -381,6 +381,9 @@ class CarState(CarStateBase):
|
||||
|
||||
ret.cruiseState.available = pt_cp.vl["Motor_51"]["TSK_Status"] in (2, 3, 4, 5)
|
||||
ret.cruiseState.enabled = pt_cp.vl["Motor_51"]["TSK_Status"] in (3, 4, 5)
|
||||
# TSK winds its braking down through brake_only after a driver brake. Requesting drive-off in this
|
||||
# state can fault TSK, and stock refuses to engage here as well, so block entry until it clears.
|
||||
ret.carNotReady = pt_cp.vl["Motor_51"]["TSK_Status"] == 5 # brake_only
|
||||
acc_values = ext_cp.vl.get("MEB_ACC_01", ext_cp.vl.get("ACC_19", {}))
|
||||
ret.cruiseState.nonAdaptive = bool(acc_values.get("ACC_Limiter_Mode", 0)) if self.CP.pcmCruise else bool(pt_cp.vl["Motor_51"]["TSK_Limiter_ausgewaehlt"])
|
||||
|
||||
@@ -585,6 +588,7 @@ class CarState(CarStateBase):
|
||||
ret.cruiseFaultLateralMode = allow_lat_only and cruise_faulted and cruise_main_available
|
||||
ret.lateralAvailable = ret.cruiseState.available or ret.cruiseFaultLateralMode
|
||||
ret.blockPcmEnable = ret.cruiseFaultLateralMode
|
||||
ret.carNotReady = bool(pt_cp.vl["Bremse_8"]["BR8_Sta_VerzReg"])
|
||||
|
||||
# Update ACC setpoint. When the setpoint reads as 255, the driver has not
|
||||
# yet established an ACC setpoint, so treat it as zero.
|
||||
|
||||
@@ -4,7 +4,9 @@ from pathlib import Path
|
||||
env = Environment(ENV=os.environ)
|
||||
|
||||
# short colored build output, if the top-level pretty tool is present (main-repo build)
|
||||
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
|
||||
_pretty = Dir('#tools/scons/site_tools').File('pretty.py')
|
||||
if not _pretty.exists():
|
||||
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
|
||||
if _pretty.exists():
|
||||
env.Tool('pretty', toolpath=[_pretty.dir.abspath])
|
||||
if not hasattr(env, 'PrettyAction'):
|
||||
|
||||
@@ -26,7 +26,9 @@ env = Environment(
|
||||
)
|
||||
|
||||
# short colored build output, if the top-level pretty tool is present (main-repo build)
|
||||
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
|
||||
_pretty = Dir('#tools/scons/site_tools').File('pretty.py')
|
||||
if not _pretty.exists():
|
||||
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
|
||||
if _pretty.exists():
|
||||
env.Tool('pretty', toolpath=[_pretty.dir.abspath])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user