This commit is contained in:
firestar5683
2026-08-18 17:28:05 -05:00
parent 80c97d0e80
commit 4078e6fcfa
13 changed files with 141 additions and 65 deletions
Binary file not shown.
+5 -5
View File
@@ -507,7 +507,7 @@ struct DeviceState @0xa4d8b5af2aa492eb {
pmicTempC @39 :List(Float32);
intakeTempC @46 :Float32;
exhaustTempC @47 :Float32;
caseTempC @48 :Float32;
gnssTempC @48 :Float32;
bottomSocTempC @50 :Float32;
maxTempC @44 :Float32; # max of other temps, used to control fan
thermalZones @38 :List(ThermalZone);
@@ -522,10 +522,10 @@ struct DeviceState @0xa4d8b5af2aa492eb {
}
enum ThermalStatus {
green @0;
yellow @1;
red @2;
danger @3;
ok @0;
warmDEPRECATED @1;
overheated @2;
critical @3;
}
enum NetworkType {
@@ -28,6 +28,11 @@ IONIQ_6_BLINDSPOT_LEFT_MASK = 0x10
CANFD_CAMERA_LEAD_MIN_DISTANCE = 0.1
ALT_BUS_LDA_BUTTON_BURST_DEBOUNCE_NS = int(1.3e9)
CLASSIC_MEDIA_BUTTON_CARS = frozenset({
CAR.HYUNDAI_ELANTRA_2024,
CAR.HYUNDAI_ELANTRA_HEV_2024,
})
def get_non_scc_cruise_signals(CP) -> tuple[str, str, str, str, str, str]:
if CP.flags & HyundaiFlags.EV:
@@ -437,6 +442,9 @@ class CarState(CarStateBase):
ret.lowSpeedAlert = self.low_speed_alert
fp_ret = custom.StarPilotCarState.new_message()
if self.CP.carFingerprint in CLASSIC_MEDIA_BUTTON_CARS:
fp_ret.modePressed = bool(cp.vl["GW_SWRC_PE"]["C_ModeSW"])
fp_ret.customPressed = bool(cp.vl["GW_SWRC_PE"]["C_MTSSW"])
return ret, fp_ret
@@ -686,6 +694,9 @@ class CarState(CarStateBase):
("BCM_PO_11", 0),
("CLU13", 0),
]
if CP.carFingerprint in CLASSIC_MEDIA_BUTTON_CARS:
# Steering-wheel media switches are event-driven on the refresh Elantra.
msgs.append(("GW_SWRC_PE", 0))
if CP.flags & HyundaiFlags.NON_SCC and not (CP.flags & HyundaiFlags.NON_SCC_NO_FCA):
msgs.append(("FCA11", 0)) # Non-SCC trims can stop publishing FCA11; don't let it poison canValid
@@ -654,6 +654,22 @@ class TestHyundaiFingerprint:
assert DBC[CP.carFingerprint][Bus.pt] == "hyundai_can_refresh_generated"
assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CAN_REFRESH_MSGS
def test_elantra_refresh_decodes_classic_media_buttons(self):
toggles = get_test_toggles()
CP = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_HEV_2024, gen_empty_fingerprint(), [], True, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(CAR.HYUNDAI_ELANTRA_HEV_2024, gen_empty_fingerprint(), [], CP, toggles)
car_state = CarState(CP, FPCP)
can_parsers = car_state.get_can_parsers(CP)
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
media_msg = packer.make_can_msg("GW_SWRC_PE", 0, {"C_ModeSW": 1, "C_MTSSW": 1})
can_parsers[Bus.pt].update([(1_000_000_000, [media_msg])])
_, fp_ret = car_state.update(can_parsers, toggles)
assert fp_ret.modePressed
assert fp_ret.customPressed
def test_hyundai_lkas_button_sets_starpilot_safety_flag(self):
fingerprint = gen_empty_fingerprint()
fingerprint[0][0x391] = 8
+41 -24
View File
@@ -17,15 +17,12 @@ LEAF_ADAS_COMMAND_ADDRS = frozenset((0x1C3, 0x2B0))
LEAF_2025_SV_PLUS_CAMERA_FW = b'6WK2CDB\x04\x18\x00\x00\x00\x00\x00R=1\x18\x99\x10\x00\x00\x00\x80'
# This Leaf camera uses KWP2000 rather than UDS for session management.
LEAF_KWP_AFTER_SALES_REQUEST = b"\x10\xC0"
LEAF_KWP_AFTER_SALES_RESPONSE = b"\x50\xC0"
LEAF_KWP_DATA_MONITOR_REQUEST = b"\x10\xF0"
LEAF_KWP_DATA_MONITOR_RESPONSE = b"\x50\xF0"
LEAF_KWP_DISABLE_NORMAL_TX = b"\x28\x01"
LEAF_KWP_ENABLE_NORMAL_TX = b"\x29\x01"
LEAF_KWP_ENABLE_NORMAL_TX_RESPONSE = b"\x69"
LEAF_KWP_SESSIONS = (
(LEAF_KWP_AFTER_SALES_REQUEST, LEAF_KWP_AFTER_SALES_RESPONSE),
(NISSAN_DIAGNOSTIC_REQUEST_KWP, NISSAN_DIAGNOSTIC_RESPONSE_KWP),
LEAF_KWP_TAKEOVER_SESSIONS = (
(LEAF_KWP_DATA_MONITOR_REQUEST, LEAF_KWP_DATA_MONITOR_RESPONSE),
)
@@ -68,25 +65,45 @@ def leaf_adas_commands_silent(can_recv, settle_time=0.05, observe_time=0.15):
return saw_adas_bus_traffic
def leaf_adas_commands_present(can_recv, settle_time=0.05, observe_time=0.15):
"""Confirm the stock ADAS command sender resumed on bus 1."""
if can_recv is None:
return False
try:
time.sleep(settle_time)
can_recv()
deadline = time.monotonic() + observe_time
while time.monotonic() < deadline:
for packet in can_recv(wait_for_one=True):
if any(msg.src == LEAF_ADAS_COMMAND_BUS and msg.address in LEAF_ADAS_COMMAND_ADDRS for msg in packet):
return True
except Exception as e:
ecu_log(f"Nissan Leaf ADAS TX recovery verification exception: {e}")
return False
ecu_log("Nissan Leaf ADAS normal TX recovery could not be verified")
return False
def restore_leaf_adas_tx(can_recv, can_send):
"""Re-enable normal KWP message transmission, trying both safe diagnostic sessions."""
"""Return to the confirmed KWP default session and verify normal TX resumes."""
if can_recv is None or can_send is None:
return False
for diag_request, diag_response in LEAF_KWP_SESSIONS:
try:
ecu_log(f"Nissan Leaf ADAS TX restore using KWP session {diag_request.hex()}")
query = IsoTpParallelQuery(
can_send, can_recv, LEAF_ADAS_ECU_BUS, [LEAF_ADAS_ECU_ADDR],
[diag_request, LEAF_KWP_ENABLE_NORMAL_TX],
[diag_response, LEAF_KWP_ENABLE_NORMAL_TX_RESPONSE],
response_offset=NISSAN_RX_OFFSET,
)
if query.get_data(0.2):
ecu_log("Nissan Leaf ADAS normal TX restored")
return True
except Exception as e:
ecu_log(f"Nissan Leaf ADAS TX restore exception: {e}")
try:
ecu_log("Nissan Leaf ADAS TX restore using KWP default session 1081")
query = IsoTpParallelQuery(
can_send, can_recv, LEAF_ADAS_ECU_BUS, [LEAF_ADAS_ECU_ADDR],
[NISSAN_DIAGNOSTIC_REQUEST_KWP], [NISSAN_DIAGNOSTIC_RESPONSE_KWP],
response_offset=NISSAN_RX_OFFSET,
)
if query.get_data(0.2) and leaf_adas_commands_present(can_recv):
ecu_log("Nissan Leaf ADAS normal TX restored and command traffic confirmed")
return True
except Exception as e:
ecu_log(f"Nissan Leaf ADAS TX restore exception: {e}")
ecu_log("Nissan Leaf ADAS normal TX restore was not confirmed")
return False
@@ -141,10 +158,10 @@ class CarInterface(CarInterfaceBase):
from openpilot.common.params import Params
params = Params()
ecu_disabled = False
for diag_request, diag_response in LEAF_KWP_SESSIONS:
for diag_request, diag_response in LEAF_KWP_TAKEOVER_SESSIONS:
ecu_log(f"Nissan Leaf ADAS takeover using KWP session {diag_request.hex()}")
ecu_disabled = disable_ecu(can_recv, can_send, bus=LEAF_ADAS_ECU_BUS, addr=LEAF_ADAS_ECU_ADDR,
com_cont_req=LEAF_KWP_DISABLE_NORMAL_TX, require_response=True, retry=3,
com_cont_req=LEAF_KWP_DISABLE_NORMAL_TX, require_response=True, retry=1,
diag_request=diag_request, diag_response=diag_response, response_offset=NISSAN_RX_OFFSET)
if ecu_disabled:
break
@@ -5,7 +5,8 @@ import pytest
from opendbc.car import Bus, ButtonType, gen_empty_fingerprint, structs
from opendbc.car.can_definitions import CanData
from opendbc.car.nissan.carstate import CarState
from opendbc.car.nissan.interface import CarInterface, LEAF_2025_SV_PLUS_CAMERA_FW, leaf_adas_commands_silent, restore_leaf_adas_tx
from opendbc.car.nissan.interface import CarInterface, LEAF_2025_SV_PLUS_CAMERA_FW, leaf_adas_commands_present, \
leaf_adas_commands_silent, restore_leaf_adas_tx
from opendbc.car.nissan.values import CAR, CarControllerParams, NissanSafetyFlags
@@ -143,28 +144,24 @@ def test_leaf_ecu_disable_is_strict_and_falls_back(monkeypatch, ecu_disabled):
monkeypatch.setattr("opendbc.car.nissan.interface.ecu_log", lambda *_: None)
CarInterface.init(CP, None, None)
assert len(calls) == (1 if ecu_disabled else 2)
assert len(calls) == 1
assert calls[0]["addr"] == 0x707
assert calls[0]["bus"] == 0
assert calls[0]["response_offset"] == 0x20
assert calls[0]["require_response"] is True
assert calls[0]["diag_request"] == b"\x10\xc0"
assert calls[0]["diag_response"] == b"\x50\xc0"
assert calls[0]["diag_request"] == b"\x10\xf0"
assert calls[0]["diag_response"] == b"\x50\xf0"
assert calls[0]["com_cont_req"] == b"\x28\x01"
assert calls[0]["retry"] == 3
if not ecu_disabled:
assert calls[1]["diag_request"] == b"\x10\x81"
assert calls[1]["diag_response"] == b"\x50\x81"
assert calls[0]["retry"] == 1
assert CP.openpilotLongitudinalControl is ecu_disabled
assert CP.pcmCruise is not ecu_disabled
assert bool(CP.safetyConfigs[-1].safetyParam & NissanSafetyFlags.LONG_CONTROL) is ecu_disabled
def test_leaf_kwp_session_can_confirm_ecu_disable(monkeypatch):
def test_leaf_kwp_data_monitor_session_can_confirm_ecu_disable(monkeypatch):
CP = CarInterface.get_params(CAR.NISSAN_LEAF, gen_empty_fingerprint(), SUPPORTED_LEAF_FW, True, False, False, None)
results = iter((False, True))
monkeypatch.setattr("opendbc.car.nissan.interface.disable_ecu", lambda *args, **kwargs: next(results))
monkeypatch.setattr("opendbc.car.nissan.interface.disable_ecu", lambda *args, **kwargs: True)
monkeypatch.setattr("opendbc.car.nissan.interface.leaf_adas_commands_silent", lambda *_: True)
monkeypatch.setattr("opendbc.car.nissan.interface.ecu_log", lambda *_: None)
CarInterface.init(CP, None, None)
@@ -205,7 +202,17 @@ def test_leaf_adas_command_silence_requires_live_bus_without_stock_commands(monk
assert not leaf_adas_commands_silent(lambda wait_for_one=False: [], settle_time=0, observe_time=0.001)
def test_leaf_adas_restore_uses_kwp_enable_normal_transmission(monkeypatch):
def test_leaf_adas_command_recovery_requires_stock_command(monkeypatch):
monkeypatch.setattr("opendbc.car.nissan.interface.ecu_log", lambda *_: None)
def stock_command_traffic(wait_for_one=False):
return [] if not wait_for_one else [[CanData(0x1C3, b"\x00" * 8, 1)]]
assert leaf_adas_commands_present(stock_command_traffic, settle_time=0, observe_time=0.001)
assert not leaf_adas_commands_present(lambda wait_for_one=False: [], settle_time=0, observe_time=0.001)
def test_leaf_adas_restore_returns_to_kwp_default_session(monkeypatch):
queries = []
class FakeQuery:
@@ -216,7 +223,8 @@ def test_leaf_adas_restore_uses_kwp_enable_normal_transmission(monkeypatch):
return {(0x707, None): b""}
monkeypatch.setattr("opendbc.car.nissan.interface.IsoTpParallelQuery", FakeQuery)
monkeypatch.setattr("opendbc.car.nissan.interface.leaf_adas_commands_present", lambda *_: True)
monkeypatch.setattr("opendbc.car.nissan.interface.ecu_log", lambda *_: None)
assert restore_leaf_adas_tx(lambda **kwargs: [], lambda msgs: None)
assert queries == [(0, [0x707], [b"\x10\xc0", b"\x29\x01"], [b"\x50\xc0", b"\x69"], 0x20)]
assert queries == [(0, [0x707], [b"\x10\x81"], [b"\x50\x81"], 0x20)]
@@ -709,7 +709,7 @@ IONIQ_5_LOW_SPEED_CENTER_LAT = 0.40
IONIQ_5_LOW_SPEED_CENTER_LAT_WIDTH = 0.10
IONIQ_5_LOW_SPEED_CENTER_JERK = 0.40
IONIQ_5_LOW_SPEED_CENTER_JERK_WIDTH = 0.12
IONIQ_5_FRICTION_JERK_DEADZONE_MAX = 0.30
IONIQ_5_FRICTION_JERK_DEADZONE_MAX = 0.36
IONIQ_5_FRICTION_JERK_DEADZONE_LAT = 1.25
IONIQ_5_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.35
IONIQ_5_FRICTION_JERK_DEADZONE_SPEED = 18.0
+2 -2
View File
@@ -56,7 +56,7 @@ MonitoringPolicy = log.DriverMonitoringState.MonitoringPolicy
StarPilotEventName = custom.StarPilotOnroadEvent.EventName
IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
VALID_ONLY_COMM_ISSUE_GRACE_FRAMES = max(1, round(0.25 / DT_CTRL))
VALID_ONLY_COMM_ISSUE_GRACE_FRAMES = max(1, round(0.5 / DT_CTRL))
def evaluate_comm_issue(all_checks: bool, all_alive: bool, all_freq_ok: bool,
@@ -515,7 +515,7 @@ class SelfdriveD:
self.events.add(EventName.pedalPressed)
# Create events for temperature, disk space, and memory
if self.sm['deviceState'].thermalStatus >= ThermalStatus.red:
if self.sm['deviceState'].thermalStatus >= ThermalStatus.overheated:
self.events.add(EventName.overheat)
if self.sm['deviceState'].freeSpacePercent < 7 and not SIMULATION:
self.events.add(EventName.outOfSpace)
@@ -3,6 +3,7 @@ import cereal.messaging as messaging
from cereal import car, custom, log
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR
from opendbc.car.nissan.values import CAR as NISSAN_CAR
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.selfdrived.selfdrived import (
VALID_ONLY_COMM_ISSUE_GRACE_FRAMES,
@@ -27,6 +28,13 @@ def test_valid_only_comm_issue_is_debounced():
assert frames == 0
def test_route_length_validity_cascade_stays_silent():
frames = 0
for _ in range(round(0.4 / DT_CTRL)):
should_alert, frames = evaluate_comm_issue(False, True, True, frames)
assert not should_alert
def test_dead_or_slow_comm_issue_is_immediate():
assert evaluate_comm_issue(False, False, True, 0) == (True, 0)
assert evaluate_comm_issue(False, True, False, 0) == (True, 0)
+2 -2
View File
@@ -122,9 +122,9 @@ class Sidebar(Widget):
thermal_status = device_state.thermalStatus
temperature = f"{int(device_state.maxTempC)}°C"
if thermal_status == ThermalStatus.green:
if thermal_status == ThermalStatus.ok:
self._temp_status.update(tr_noop("TEMP"), temperature, Colors.GOOD)
elif thermal_status == ThermalStatus.yellow:
elif thermal_status == ThermalStatus.warmDEPRECATED:
self._temp_status.update(tr_noop("TEMP"), temperature, Colors.WARNING)
else:
self._temp_status.update(tr_noop("TEMP"), temperature, Colors.DANGER)
+2 -1
View File
@@ -51,7 +51,8 @@ class ThermalConfig:
memory: ThermalZone | None = None
intake: ThermalZone | None = None
exhaust: ThermalZone | None = None
case: ThermalZone | None = None
gnss: ThermalZone | None = None
bottomSoc: ThermalZone | None = None
def get_msg(self):
ret = {}
+24 -14
View File
@@ -133,15 +133,21 @@ HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'ne
# List of thermal bands. We will stay within this region as long as we are within the bounds.
# When exiting the bounds, we'll jump to the lower or higher band. Bands are ordered in the dict.
THERMAL_BANDS = OrderedDict({
ThermalStatus.green: ThermalBand(None, 80.0),
ThermalStatus.yellow: ThermalBand(75.0, 96.0),
ThermalStatus.red: ThermalBand(88.0, 107.),
ThermalStatus.danger: ThermalBand(94.0, None),
})
if HARDWARE.get_device_type() == "mici":
THERMAL_BANDS = OrderedDict({
ThermalStatus.ok: ThermalBand(None, 100.0),
ThermalStatus.overheated: ThermalBand(92.0, 107.),
ThermalStatus.critical: ThermalBand(98.0, None),
})
else:
THERMAL_BANDS = OrderedDict({
ThermalStatus.ok: ThermalBand(None, 96.0),
ThermalStatus.overheated: ThermalBand(88.0, 107.),
ThermalStatus.critical: ThermalBand(94.0, None),
})
# Override to highest thermal band when offroad and above this temp
OFFROAD_DANGER_TEMP = 75
OFFROAD_DANGER_TEMP = 85 if HARDWARE.get_device_type() == "mici" else 75
prev_offroad_states: dict[str, tuple[bool, str | None]] = {}
@@ -279,7 +285,7 @@ def hardware_thread(end_event, hw_queue) -> None:
started_ts: float | None = None
started_seen = False
startup_blocked_ts: float | None = None
thermal_status = ThermalStatus.yellow
thermal_status = ThermalStatus.ok
last_hw_state = HardwareState(
network_type=NetworkType.none,
@@ -405,13 +411,13 @@ def hardware_thread(end_event, hw_queue) -> None:
# StarPilot variables
if starpilot_toggles.increase_thermal_limits:
all_comp_temp -= (THERMAL_BANDS[ThermalStatus.danger].min_temp - THERMAL_BANDS[ThermalStatus.red].min_temp)
all_comp_temp -= (THERMAL_BANDS[ThermalStatus.critical].min_temp - THERMAL_BANDS[ThermalStatus.overheated].min_temp)
is_offroad_for_5_min = (started_ts is None) and ((not started_seen) or (off_ts is None) or (time.monotonic() - off_ts > 60 * 5))
if is_offroad_for_5_min and offroad_comp_temp > OFFROAD_DANGER_TEMP:
# if device is offroad and already hot without the extra onroad load,
# we want to cool down first before increasing load
thermal_status = ThermalStatus.danger
thermal_status = ThermalStatus.critical
else:
current_band = THERMAL_BANDS[thermal_status]
band_idx = list(THERMAL_BANDS.keys()).index(thermal_status)
@@ -434,17 +440,20 @@ def hardware_thread(end_event, hw_queue) -> None:
startup_conditions["not_taking_snapshot"] = not params.get_bool("IsTakingSnapshot")
# must be at an engageable thermal band to go onroad
startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.red
startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.overheated
# ensure device is fully booted
startup_conditions["device_booted"] = startup_conditions.get("device_booted", False) or HARDWARE.booted()
# if the temperature enters the danger zone, go offroad to cool down
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.critical
extra_text = f"{offroad_comp_temp:.1f}C"
show_alert = (not onroad_conditions["device_temp_good"] or not startup_conditions["device_temp_engageable"]) and onroad_conditions["ignition"]
set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", show_alert, extra_text=extra_text)
if show_alert:
msg.deviceState.fanSpeedPercentDesired = 100
# *** registration check ***
if not PC:
# we enforce this for our software, but you are welcome
@@ -563,9 +572,10 @@ def hardware_thread(end_event, hw_queue) -> None:
statlog.gauge("fan_speed_percent_desired", msg.deviceState.fanSpeedPercentDesired)
statlog.gauge("screen_brightness_percent", msg.deviceState.screenBrightnessPercent)
# report to server once every 10 minutes
# report to server once every 10 minutes, or every 1s when thermally blocked
rising_edge_started = should_start and not should_start_prev
if rising_edge_started or (count % int(600. / DT_HW)) == 0:
status_packet_interval = 1. if show_alert else 600.
if rising_edge_started or (count % int(status_packet_interval / DT_HW)) == 0:
dat = {
'count': count,
'pandaStates': [strip_deprecated_keys(p.to_dict()) for p in pandaStates],
+8 -3
View File
@@ -321,11 +321,12 @@ class Tici(HardwareBase):
os.system("sudo poweroff")
def get_thermal_config(self):
intake, exhaust, case = None, None, None
intake, exhaust, gnss, bottomSoc = None, None, None, None
if self.get_device_type() == "mici":
case = ThermalZone("case")
gnss = ThermalZone("gnss")
intake = ThermalZone("intake")
exhaust = ThermalZone("exhaust")
bottomSoc = ThermalZone("bottom_soc")
return ThermalConfig(cpu=[ThermalZone(f"cpu{i}-silver-usr") for i in range(4)] +
[ThermalZone(f"cpu{i}-gold-usr") for i in range(4)],
gpu=[ThermalZone("gpu0-usr"), ThermalZone("gpu1-usr")],
@@ -334,7 +335,8 @@ class Tici(HardwareBase):
pmic=[ThermalZone("pm8998_tz"), ThermalZone("pm8005_tz")],
intake=intake,
exhaust=exhaust,
case=case)
gnss=gnss,
bottomSoc=bottomSoc)
def set_display_power(self, on):
try:
@@ -383,6 +385,9 @@ class Tici(HardwareBase):
continue
gov = 'ondemand' if powersave_enabled else 'performance'
sudo_write(gov, f'/sys/devices/system/cpu/cpufreq/policy{n}/scaling_governor')
if not powersave_enabled:
# cap max core freq to 1689 Mhz
sudo_write('1689600', f'/sys/devices/system/cpu/cpufreq/policy{n}/scaling_max_freq')
# *** IRQ config ***