Jetlink offroad alert and optional owner

- jetlinkd is ancillary: a dead owner sends the drive back to the small
  model and no longer blocks engaging
- a crashed owner now restarts, as porting.md asks
- Offroad_JetlinkUnavailable says why a Jetlink the user turned on cannot
  run (jetlink's reason())
- JetlinkBigModel, JetlinkCatalog and JetlinkRuntime are no longer used

params_keys.h changed: libparams_c.so, libcommon.a and the prebuilt
manifest need regenerating with the prebuilt build. Until then the
tracked params library does not know Offroad_JetlinkUnavailable, so
hardwared skips that alert (logged once) and it stays inert.
This commit is contained in:
Zeph Leggett
2026-10-16 03:54:44 -05:00
parent af96e527d7
commit 9a7f3d4174
8 changed files with 92 additions and 7 deletions
+1 -3
View File
@@ -257,6 +257,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
{"Offroad_JetlinkUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_Pairing", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_Recalibration", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
@@ -347,11 +348,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"JetlinkMode", {PERSISTENT, INT, "0"}},
{"JetlinkChargePhone", {PERSISTENT, BOOL, "0"}},
{"JetlinkBigModel", {PERSISTENT | DONT_LOG, JSON}},
{"JetlinkCatalog", {PERSISTENT | DONT_LOG, JSON}},
{"JetlinkSpec", {PERSISTENT | DONT_LOG, JSON}},
{"JetlinkPointers", {PERSISTENT | DONT_LOG, JSON}},
{"JetlinkProgress", {CLEAR_ON_MANAGER_START | DONT_LOG, JSON}},
{"JetlinkRuntime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | DONT_LOG, JSON}},
{"Version", {PERSISTENT, STRING}},
};
@@ -49,6 +49,10 @@
"text": "Chestnut USB link is slow. Check the USB cable. The current speed is %1.",
"severity": 0
},
"Offroad_JetlinkUnavailable": {
"text": "Jetlink unavailable: %1",
"severity": 0
},
"Offroad_UnregisteredHardware": {
"text": "Failed to register with comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support.",
"severity": 1
@@ -25,7 +25,7 @@ def process_health(processes):
return drive
@pytest.mark.parametrize('name', ['android_autod', 'galaxy_hotspot'])
@pytest.mark.parametrize('name', ['android_autod', 'galaxy_hotspot', 'jetlinkd'])
@pytest.mark.parametrize('running,requested', [(False, False), (False, True), (True, True)])
def test_optional_lifecycle_does_not_block_engagement(name, running, requested):
drive = process_health([(name, running, requested)])
+20 -1
View File
@@ -17,7 +17,7 @@ from openpilot.cereal import log
from openpilot.cereal.services import SERVICE_LIST
from openpilot.common.utils import strip_deprecated_keys
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.common.params import Params, UnknownKeyName
from openpilot.common.realtime import DT_HW
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE, PC
@@ -33,6 +33,7 @@ from openpilot.system.hardware.chestnut.monitoring import chestnut_state_thread
from openpilot.system.hardware.chestnut.status import ChestnutStatus
from openpilot.common.version import terms_version, training_version
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
from openpilot.starpilot.models import jetlink_adapter
ThermalStatus = log.DeviceState.ThermalStatus
NetworkType = log.DeviceState.NetworkType
@@ -123,6 +124,23 @@ def set_offroad_alert_if_changed(offroad_alert: str, show_alert: bool, extra_tex
prev_offroad_states[offroad_alert] = (show_alert, extra_text)
set_offroad_alert(offroad_alert, show_alert, extra_text)
# A prebuilt params library not yet regenerated lacks Offroad_JetlinkUnavailable: the alert is skipped rather than
# taking hardwared down. The guard can go once the library knows the key.
jetlink_alert_usable = True
def update_jetlink_alert() -> None:
"""Why the Jetlink the user turned on cannot run. Reads files only; with the link off, jetlink binds nothing."""
global jetlink_alert_usable
if not jetlink_alert_usable:
return
reason = jetlink_adapter.hooks().reason()
try:
set_offroad_alert_if_changed("Offroad_JetlinkUnavailable", reason is not None, extra_text=reason)
except UnknownKeyName:
jetlink_alert_usable = False
cloudlog.warning("Offroad_JetlinkUnavailable is not in this params library: Jetlink's offroad alert is off")
def touch_thread(end_event):
count = 0
@@ -394,6 +412,7 @@ def hardware_thread(end_event, hw_queue) -> None:
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)
update_jetlink_alert()
if show_alert:
msg.deviceState.fanSpeedPercentDesired = 100
@@ -0,0 +1,47 @@
"""hardwared's Jetlink offroad alert says why the link cannot run, and never takes hardwared down."""
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
from openpilot.common.params import UnknownKeyName
from openpilot.system.hardware import hardwared
class TestJetlinkAlert(unittest.TestCase):
def setUp(self):
hardwared.jetlink_alert_usable = True
self.addCleanup(setattr, hardwared, 'jetlink_alert_usable', True)
self.reason = Mock(return_value='no usable gadget')
self.link = SimpleNamespace(hooks=lambda: SimpleNamespace(reason=self.reason))
self.set_alert = Mock()
for name, value in (('jetlink_adapter', self.link), ('set_offroad_alert_if_changed', self.set_alert), ('cloudlog', Mock())):
patcher = patch.object(hardwared, name, value)
patcher.start()
self.addCleanup(patcher.stop)
def test_the_alert_follows_jetlinks_reason(self):
hardwared.update_jetlink_alert()
self.set_alert.assert_called_with('Offroad_JetlinkUnavailable', True, extra_text='no usable gadget')
self.reason.return_value = None
hardwared.update_jetlink_alert()
self.set_alert.assert_called_with('Offroad_JetlinkUnavailable', False, extra_text=None)
def test_with_the_link_off_jetlink_is_not_bound(self):
from jetlink.openpilot.hooks import Hooks
from openpilot.starpilot.models.jetlink_adapter import API
factory = Mock()
self.link.hooks = lambda: Hooks(API, factory, link_on=lambda: False)
hardwared.update_jetlink_alert()
factory.assert_not_called()
self.set_alert.assert_called_once_with('Offroad_JetlinkUnavailable', False, extra_text=None)
def test_a_params_library_without_the_key_turns_the_alert_off_once(self):
self.set_alert.side_effect = UnknownKeyName(b'Offroad_JetlinkUnavailable')
hardwared.update_jetlink_alert()
hardwared.update_jetlink_alert()
self.set_alert.assert_called_once()
hardwared.cloudlog.warning.assert_called_once()
if __name__ == '__main__':
unittest.main()
+2 -1
View File
@@ -137,7 +137,8 @@ procs = [
enabled=platform.system() == "Linux", restart_on_exit=True),
PythonProcess("galaxy_hotspot", "openpilot.starpilot.galaxy.hotspot_daemon", galaxy_local,
enabled=COMMA_HARDWARE and not os.environ.get('OPENPILOT_PREFIX'), restart_on_exit=True),
JetlinkProcess("jetlinkd", "openpilot.starpilot.models.jetlink_adapter", jetlink_enabled, enabled=COMMA_HARDWARE),
JetlinkProcess("jetlinkd", "openpilot.starpilot.models.jetlink_adapter", jetlink_enabled, enabled=COMMA_HARDWARE,
restart_on_exit=True),
PythonProcess("android_autod", "openpilot.starpilot.system.android_auto.daemon", android_auto_enabled, enabled=COMMA_HARDWARE,
restart_on_exit=True), # boot can race bluetoothd (NoSuchUnit)
# Boot can race the audio device; selfdrived blocks engagement while soundd is down.
+2 -1
View File
@@ -2,7 +2,8 @@
# Projection and local phone access are optional even when requested.
# Their lifecycles remain visible in managerState and their own status.
ANCILLARY_PROCESSES = frozenset({'android_autod', 'galaxy_hotspot'})
# A dead Jetlink owner sends the drive back to the small model.
ANCILLARY_PROCESSES = frozenset({'android_autod', 'galaxy_hotspot', 'jetlinkd'})
def driving_process_failures(processes) -> set[str]:
@@ -102,6 +102,21 @@ class TestProcessRecovery(unittest.TestCase):
self.assertEqual(p.restart_failures, 0)
self.assertEqual(p.restart_at, 0)
def test_a_crashed_jetlink_owner_restarts(self):
from openpilot.system.manager.process_config import managed_processes
configured = managed_processes["jetlinkd"]
self.assertTrue(configured.restart_on_exit)
p = type(configured)("jetlinkd", "unused", lambda started, params, cp: True, restart_on_exit=configured.restart_on_exit)
self.step(p)
old = p.proc
old.exitcode = 1
self.step(p)
self.assertIs(p.proc, old)
self.now += 1.01
self.step(p)
self.assertIsNot(p.proc, old)
self.assertTrue(old.joined)
def test_boot_racing_processes_restart(self):
from openpilot.system.manager.process_config import procs
restarting = {p.name for p in procs if getattr(p, "restart_on_exit", False)}