IQ.Pilot Release Commit @ 9fe0487

This commit is contained in:
IQ.Lvbs CI [bot]
2026-07-27 01:04:28 -05:00
parent 6edfe55a1e
commit 541e76a873
210 changed files with 992 additions and 504 deletions

View File

@@ -200,6 +200,9 @@ class HardwareBase(ABC):
def reboot_modem(self):
pass
def recover_sim_detection(self) -> bool:
return False
def get_networks(self):
return None

View File

@@ -204,6 +204,7 @@ def hw_state_thread(end_event, hw_queue):
modem_configured = False
modem_missing_count = 0
modem_restart_count = 0
sim_detection_recovered = False
while not end_event.is_set():
# these are expensive calls. update every 10s
@@ -254,6 +255,10 @@ def hw_state_thread(end_event, hw_queue):
HARDWARE.configure_modem()
modem_configured = True
if modem_configured and not sim_detection_recovered and HARDWARE.recover_sim_detection():
cloudlog.event("sim missing with hot-swap detect armed, rebooting modem with detect disabled", error=True)
sim_detection_recovered = True
prev_hw_state = hw_state
except Exception:
cloudlog.exception("Error getting hardware state")
@@ -549,15 +554,17 @@ def hardware_thread(end_event, hw_queue) -> None:
som_power_draw = HARDWARE.get_som_power_draw()
msg.deviceState.somPowerDrawW = som_power_draw
# FastSleep deep standby: shed heavy processes at low battery instead of shutting down,
# recover on ignition or once the alternator is charging
# FastSleep deep standby: shed heavy processes once parked with the screen idled off
# (or at low battery) instead of shutting down, recover on ignition or once the
# alternator is charging
fast_sleep = params.get_bool("FastSleep")
if fast_sleep and not tesla_no_sleep:
if low_power:
if onroad_conditions["ignition"] or power_monitor.car_voltage_mV >= (VBATT_LOW_POWER_EXIT * 1e3):
low_power = False
else:
low_power = power_monitor.should_enter_low_power(onroad_conditions["ignition"], in_car, off_ts)
screen_off = msg.deviceState.screenBrightnessPercent < 1e-3
low_power = power_monitor.should_enter_low_power(onroad_conditions["ignition"], in_car, off_ts, screen_off)
else:
low_power = False

View File

@@ -13,11 +13,13 @@ CAR_CHARGING_RATE_W = 45
VBATT_PAUSE_CHARGING = 11.8 # Lower limit on the LPF car battery voltage
# FastSleep (deep standby): enter low power at the normal shutdown voltage, shut down
# at a lower floor, exit once the alternator is charging
# FastSleep (deep standby): enter low power once parked with the screen idled off, or
# immediately at the normal shutdown voltage; shut down at a lower floor, exit once the
# alternator is charging
VBATT_LOW_POWER_ENTRY = 11.8
VBATT_LOW_POWER_EXIT = 12.8
VBATT_HARD_SHUTDOWN = 11.5
LOW_POWER_ENTRY_TIME_S = 300
MAX_TIME_OFFROAD_S = 30*3600
MIN_ON_TIME_S = 3600
DELAY_SHUTDOWN_TIME_S = 300 # Wait at least DELAY_SHUTDOWN_TIME_S seconds after offroad_time to shutdown.
@@ -124,14 +126,18 @@ class PowerMonitoring:
return 0 < iq_max_time_val_s <= offroad_time
# FastSleep: see if we should enter low power mode instead of shutting down
def should_enter_low_power(self, ignition: bool, in_car: bool, offroad_timestamp: float | None) -> bool:
def should_enter_low_power(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, screen_off: bool) -> bool:
if offroad_timestamp is None or ignition or not in_car:
return False
if not self.params.get_bool("FastSleep"):
return False
offroad_time = time.monotonic() - offroad_timestamp
return (self.car_voltage_mV < (VBATT_LOW_POWER_ENTRY * 1e3) and
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
# a healthy battery rests above VBATT_LOW_POWER_ENTRY, so parked entry must be
# time-based; the voltage trigger stays as the sagging-battery fast path
low_voltage = (self.car_voltage_mV < (VBATT_LOW_POWER_ENTRY * 1e3) and
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
parked_idle = screen_off and offroad_time > LOW_POWER_ENTRY_TIME_S
return low_voltage or parked_idle
# See if we need to shutdown
def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool):
@@ -141,12 +147,15 @@ class PowerMonitoring:
now = time.monotonic()
should_shutdown = False
offroad_time = (now - offroad_timestamp)
vbatt_min = VBATT_HARD_SHUTDOWN if self.params.get_bool("FastSleep") else VBATT_PAUSE_CHARGING
fast_sleep = self.params.get_bool("FastSleep")
vbatt_min = VBATT_HARD_SHUTDOWN if fast_sleep else VBATT_PAUSE_CHARGING
low_voltage_shutdown = (self.car_voltage_mV < (vbatt_min * 1e3) and
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
should_shutdown |= self.max_time_offroad_exceeded(offroad_time)
should_shutdown |= low_voltage_shutdown
should_shutdown |= (self.car_battery_capacity_uWh <= 0)
# the 30 Wh bookkeeping model empties within hours at offroad draw regardless of the
# real battery state; under FastSleep the measured voltage floors govern instead
should_shutdown |= (self.car_battery_capacity_uWh <= 0) and not fast_sleep
should_shutdown &= not ignition
should_shutdown &= (not self.params.get_bool("DisablePowerDown"))
should_shutdown &= in_car

View File

@@ -2,7 +2,8 @@ import pytest
from openpilot.common.params import Params
from openpilot.system.hardware.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \
CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S, MAX_TIME_OFFROAD_S
CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S, MAX_TIME_OFFROAD_S, \
VBATT_HARD_SHUTDOWN, LOW_POWER_ENTRY_TIME_S
# Create fake time
ssb = 0.
@@ -232,3 +233,59 @@ class TestPowerMonitoring:
result = pm.max_time_offroad_exceeded(offroad_time_s)
assert result == expected_result
# FastSleep must not shut down on the empty bookkeeping model while voltage is healthy
def test_fast_sleep_ignores_battery_capacity_model(self, mocker):
self.params.put_bool("FastSleep", True)
self.params.put("MaxTimeOffroad", 0)
try:
pm_patch(mocker, "HARDWARE.get_current_power_draw", 0)
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = 0
start_time = ssb
for _ in range(DELAY_SHUTDOWN_TIME_S + 100):
pm.calculate(GOOD_VOLTAGE, False)
assert not pm.should_shutdown(False, True, start_time, True)
finally:
self.params.put_bool("FastSleep", False)
# FastSleep still shuts down below the hard voltage floor
def test_fast_sleep_hard_voltage_floor(self, mocker):
self.params.put_bool("FastSleep", True)
try:
pm_patch(mocker, "HARDWARE.get_current_power_draw", 0)
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
start_time = ssb
for _ in range(DELAY_SHUTDOWN_TIME_S + 100):
pm.calculate((VBATT_HARD_SHUTDOWN - 0.5) * 1e3, False)
assert pm.should_shutdown(False, True, start_time, True)
finally:
self.params.put_bool("FastSleep", False)
def test_fast_sleep_low_power_entry(self, mocker):
self.params.put_bool("FastSleep", True)
try:
pm_patch(mocker, "HARDWARE.get_current_power_draw", 0)
# parked with the screen idled off: time-based entry at healthy voltage
pm = PowerMonitoring()
start_time = ssb
for _ in range(LOW_POWER_ENTRY_TIME_S + 10):
pm.calculate(GOOD_VOLTAGE, False)
assert pm.should_enter_low_power(False, True, start_time, screen_off=True)
assert not pm.should_enter_low_power(False, True, start_time, screen_off=False)
assert not pm.should_enter_low_power(True, True, start_time, screen_off=True)
assert not pm.should_enter_low_power(False, False, start_time, screen_off=True)
# sagging battery: voltage entry regardless of screen state
pm = PowerMonitoring()
start_time = ssb
for _ in range(100):
pm.calculate((VBATT_HARD_SHUTDOWN + 0.1) * 1e3, False)
assert pm.should_enter_low_power(False, True, start_time, screen_off=False)
self.params.put_bool("FastSleep", False)
assert not pm.should_enter_low_power(False, True, start_time, screen_off=True)
finally:
self.params.put_bool("FastSleep", False)

View File

@@ -78,17 +78,17 @@
},
{
"name": "system",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b.img.xz",
"hash": "654de83dd64190701c5a9dc7297fb569ba106cc63217725893920df6b8c9eaa0",
"hash_raw": "756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img.xz",
"hash": "0c9e7dee6c7365600c77b33c4996456459d04aad005e3e41bf6ee2e8af74ceb9",
"hash_raw": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
"size": 6291456000,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "8269bd017b18a1a4fd5e093b96efa181fd803e200fa92364fe9307f25445f596",
"ondevice_hash": "1ee589d3d728a03561718383bf640171f1aef048573e46fe17239617c7c19fff",
"alt": {
"hash": "756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b.img",
"hash": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img",
"size": 6291456000
}
}

View File

@@ -1 +1 @@
6ZRxEduDaLm1coUVvI/0QAZtmbf+tY0TWMZtaEaxV7I2fKFfGPG/6oOizuFsmd/SSQls84kap9MwKDEc8XfIDA==
+LyP/p4uBBxbTwRwPrwR/2qPaHkAXmlFkg0GdxOBuujPY89GxP4t5SNKxirqiw9LmMcfA0JfVwMeR3QD9rQ6Dg==

View File

@@ -67,17 +67,17 @@
},
{
"name": "system",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b.img.xz",
"hash": "654de83dd64190701c5a9dc7297fb569ba106cc63217725893920df6b8c9eaa0",
"hash_raw": "756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img.xz",
"hash": "0c9e7dee6c7365600c77b33c4996456459d04aad005e3e41bf6ee2e8af74ceb9",
"hash_raw": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
"size": 6291456000,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "8269bd017b18a1a4fd5e093b96efa181fd803e200fa92364fe9307f25445f596",
"ondevice_hash": "1ee589d3d728a03561718383bf640171f1aef048573e46fe17239617c7c19fff",
"alt": {
"hash": "756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b.img",
"hash": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img",
"size": 6291456000
}
}

View File

@@ -1 +1 @@
+gqhJbKMr9i9lW7WrtKebqKXlM0Asr/i7ZodOzIAHzAgzGEvPCWbfco2jo0/jO7O2UzZwKJUl/WzHlsOtJvnCw==
RCYk34Ifjx7S9PIhPm6p49vFMhamPwvw8Lfbb6a6HO8sUOi3J2PcY4VDUcqQZWU6+Am++HP3nJSea8OmAsckAg==

View File

@@ -69,6 +69,9 @@ NetworkStrength = log.DeviceState.NetworkStrength
MM_MODEM_ACCESS_TECHNOLOGY_UMTS = 1 << 5
MM_MODEM_ACCESS_TECHNOLOGY_LTE = 1 << 14
# MMModemStateFailedReason
MM_MODEM_STATE_FAILED_REASON_SIM_MISSING = 2
def affine_irq(val, action):
irqs = get_irqs_for_action(action)
@@ -605,6 +608,32 @@ class Tici(HardwareBase):
os.system(f"sudo cp {tf.name} {dest}")
os.system(f"sudo nmcli con load {dest}")
def recover_sim_detection(self) -> bool:
# A worn SIM-tray presence switch can read "removed" while the SIM pads still make
# contact; with hot-swap detect armed (AT+QSIMDET=1) the modem never powers the SIM
# and lands in failed/sim-missing. Disabling detect and rebooting the modem makes it
# probe the SIM electrically. Safe to retry on failure: firing disarms the QSIMDET
# gate, so a genuinely SIM-less device gets at most one extra modem reboot per boot.
if self.get_device_type() not in ("tici", "tizi"):
return False
try:
modem = self.get_modem()
state = modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
if state != MM_MODEM_STATE.FAILED:
return False
reason = modem.Get(MM_MODEM, 'StateFailedReason', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
if reason != MM_MODEM_STATE_FAILED_REASON_SIM_MISSING:
return False
detect = str(modem.Command('AT+QSIMDET?', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)).strip()
if not detect.startswith('+QSIMDET: 1'):
return False
modem.Command('AT+QSIMDET=0,0', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
modem.Command('AT+CFUN=1,1', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
return True
except Exception:
return False
def reboot_modem(self):
modem = None
try: