mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
Jetlink adapter checks and guide
jetlink's own kit checks the adapter module and modeld's hook order. Tests cover in_control with AOL armed and paused, the driving warp jetlink runs, and the catalog runner's history reset on tinygrad keeping its buffers. JETLINK.md describes the stay-engaged hand-back and the cable rule.
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
## Jetlink external inference
|
||||
|
||||
The comma-side Jetlink client and USB transport in `jetlink_repo` are by Zeph Leggett and the [Jetlink contributors](https://github.com/zoompilot/jetlink). They are vendored from revision `d24328d849b0e5154b55f149dc18c5dfe8bd79cc` under the MIT license, retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE). StarPilot owns the adapter to its model runner, settings and process lifecycle. Companion applications and inference servers remain distributed by Jetlink.
|
||||
The comma-side Jetlink client and USB transport in `jetlink_repo` are by Zeph Leggett and the [Jetlink contributors](https://github.com/zoompilot/jetlink). They are vendored from revision `d24328d849b0e5154b55f149dc18c5dfe8bd79cc` under the MIT license, retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE). StarPilot's adapter (`openpilot/starpilot/models/jetlink_adapter.py`) builds on Jetlink's openpilot layer and owns its param names, Always On Lateral rule, model runner reset, settings and alerts. Companion applications and inference servers remain distributed by Jetlink.
|
||||
|
||||
## MRR35 Hyundai angle and radar
|
||||
|
||||
|
||||
@@ -69,10 +69,16 @@ prompt and benchmark sustained performance before use.
|
||||
## Use and recovery
|
||||
|
||||
Before changing the remote model, disengage cruise **and** any independent
|
||||
Always On Lateral steering. Wait for Galaxy's fresh **Active · Jetlink remote model**
|
||||
status before re-engaging. If the link stops or stalls, StarPilot falls back to
|
||||
the local Small model; be ready to take control as behavior changes. Set Jetlink
|
||||
to **Off** while parked to stop using the remote connection.
|
||||
Always On Lateral steering, armed or paused. Wait for Galaxy's fresh **Active · Jetlink remote model**
|
||||
status before re-engaging; engaging is refused for a second after the switch. If
|
||||
the link stops or stalls while engaged, StarPilot stays engaged on the local Small
|
||||
model and shows **Big Model Lost**; be ready to take control as behavior changes.
|
||||
Jetlink reconnects by itself and switches back at the next full disengagement. Set
|
||||
Jetlink to **Off** while parked to stop using the remote connection.
|
||||
|
||||
With Jetlink on, the USB-C port belongs to its host: a downloaded Chestnut pick is
|
||||
used only when a Chestnut is detected, never because a cable is plugged in. When
|
||||
Jetlink is on but cannot run, an offroad alert says why.
|
||||
|
||||
If a connection fails, check separate power, USB 3 support, host sleep, cooling,
|
||||
and the chosen StarPilot connection mode. An iPhone must stay unlocked with the
|
||||
|
||||
@@ -31,4 +31,5 @@ charging over USB is optional and starts off.
|
||||
For a remote model switch, fully disengage cruise and any lateral control that
|
||||
stays on without cruise. Wait for Galaxy's fresh **Active · Jetlink remote model**
|
||||
status before engaging again. A lost or stalled link falls back to the local
|
||||
Small model; remain ready to take over while its behavior changes.
|
||||
Small model and stays engaged with a **Big Model Lost** warning; remain ready to
|
||||
take over while its behavior changes.
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
import ast
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from jetlink.openpilot import Openpilot, conformance
|
||||
from jetlink.openpilot.hooks import Hooks, Off
|
||||
from jetlink.openpilot.stock import Stock
|
||||
from jetlink.openpilot.testing import FakeSubMaster, check_adapter, check_fallback, check_seam, install_guard
|
||||
from openpilot.starpilot.models import jetlink_adapter as adapter
|
||||
from openpilot.starpilot.models.tests.jetlink_fixture import ARMED_PAUSED, AXES_IDLE, rule
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
GUARD = install_guard()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def no_machine_files():
|
||||
# on a comma, a test that reaches the live params would clear JetlinkSpec
|
||||
GUARD.start()
|
||||
yield
|
||||
assert GUARD.stop() == []
|
||||
|
||||
|
||||
def test_the_adapter_module():
|
||||
assert check_adapter(adapter, heavy=("openpilot.common.params", "openpilot.common.swaglog", "openpilot.cereal")) == []
|
||||
|
||||
|
||||
def test_the_modeld_seam():
|
||||
assert check_seam(ROOT / "openpilot/selfdrive/modeld/modeld.py") == []
|
||||
|
||||
|
||||
def test_interface_keys():
|
||||
# the params directory is Stock's, held against openpilot's Params in test_jetlink_lifecycle
|
||||
# conformance is check_adapter's; the params directory is Stock's, held against Params in test_jetlink_lifecycle
|
||||
op = adapter.adapter()
|
||||
assert conformance(op, Openpilot) == []
|
||||
assert adapter.owner_config().adapter == adapter.__name__
|
||||
assert op.keys.link == "JetlinkMode"
|
||||
# nothing writes a big-model slot or catalog: jetlink's default model, so a fresh Jetson is provisioned
|
||||
@@ -38,6 +62,18 @@ def test_guard_answers_link_off_without_a_usable_jetlink():
|
||||
adapter.hooks.cache_clear()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", [None, "0", "1"])
|
||||
def test_the_fallback_answers_link_off(monkeypatch, tmp_path, mode):
|
||||
# the hooks a missing jetlink reaches: modeld stops at prepare(), selfdrived builds no swap events
|
||||
monkeypatch.setenv("PARAMS_ROOT", str(tmp_path))
|
||||
monkeypatch.setenv("OPENPILOT_PREFIX", "jetlink-test")
|
||||
(tmp_path / "jetlink-test").mkdir()
|
||||
if mode is not None:
|
||||
(tmp_path / "jetlink-test/JetlinkMode").write_text(mode)
|
||||
hooks = ("should_run", "status", "reason", "prepare", "shutdown_step", "runtime", "swap_events")
|
||||
assert check_fallback(adapter._NoJetlink("jetlink is missing"), hooks, link_on=adapter.link_on) == []
|
||||
|
||||
|
||||
def test_one_action_call_for_every_small_model():
|
||||
from openpilot.selfdrive.modeld import modeld
|
||||
from openpilot.starpilot.models import runner
|
||||
@@ -60,3 +96,117 @@ def test_unsupported_device():
|
||||
assert adapter.supported_device("mici")
|
||||
for device in ("tici", "pc", "unknown", "neo"):
|
||||
assert not adapter.supported_device(device)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device,camera", [("tizi", (1928, 1208)), ("mici", (1344, 760))])
|
||||
def test_jetlink_runs_the_driving_warp_modeld_ships(device, camera):
|
||||
op = adapter.adapter()
|
||||
with patch("openpilot.common.hardware.HARDWARE.get_device_type", return_value=device):
|
||||
geometry = op.camera()
|
||||
assert geometry == (*camera, 512, 256)
|
||||
assert op.warp_path(*geometry).is_file()
|
||||
|
||||
|
||||
def test_chestnut_ids_are_openpilots():
|
||||
from openpilot.common.hardware.usb import CHESTNUT_ROM_USB_IDS, CHESTNUT_USB_IDS
|
||||
assert adapter.adapter().CHESTNUT_IDS == frozenset(CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS) == Stock.CHESTNUT_IDS
|
||||
|
||||
|
||||
DISARMED = SimpleNamespace(allowedLatch=False, lateralArmed=False)
|
||||
|
||||
|
||||
def in_control(core=None, aol=None, intent=DISARMED):
|
||||
with patch("openpilot.starpilot.aol.wire.decode_intent", return_value=intent):
|
||||
return rule(core, aol).in_control()
|
||||
|
||||
|
||||
# openpilot's own services are Stock's rule, tested in jetlink: one row here, then AOL's
|
||||
@pytest.mark.parametrize("case,expected", [
|
||||
({}, False),
|
||||
({"core": {"selfdriveState": SimpleNamespace(enabled=True, active=True)}}, True),
|
||||
# AOL armed but paused: its axes are idle and only the intent says so
|
||||
({"aol": {"aolIntentWire": b"intent", "aolAxisState": AXES_IDLE}, "intent": ARMED_PAUSED}, True),
|
||||
({"aol": {"aolIntentWire": b"intent"}, "intent": SimpleNamespace(allowedLatch=True, lateralArmed=False)}, True),
|
||||
({"aol": {"aolIntentWire": b"intent"}, "intent": None}, True),
|
||||
({"aol": {"aolIntentWire": b"intent", "aolAxisState": SimpleNamespace(**{**vars(AXES_IDLE), "desiredLateral": True})}}, True),
|
||||
({"aol": {"aolIntentWire": b"intent", "aolAxisState": AXES_IDLE}}, False),
|
||||
])
|
||||
def test_in_control(case, expected):
|
||||
assert in_control(**case) is expected
|
||||
|
||||
|
||||
def test_in_control_holds_the_swap_off_when_aol_cannot_be_read():
|
||||
op = rule(aol={"aolIntentWire": b"intent"})
|
||||
op._aol.alive["aolIntentWire"] = False
|
||||
assert op.in_control() is True
|
||||
op._aol = SimpleNamespace(update=lambda timeout: (_ for _ in ()).throw(RuntimeError("no msgq")))
|
||||
assert op.in_control() is True
|
||||
|
||||
|
||||
def test_an_aol_intent_not_heard_from_yet_counts_as_armed():
|
||||
# the first frames after the AOL subscription starts have seen nothing yet
|
||||
op = rule()
|
||||
op._aol = None
|
||||
with patch("openpilot.cereal.messaging.SubMaster", side_effect=lambda services: FakeSubMaster(services)), \
|
||||
patch("openpilot.starpilot.models.jetlink_adapter.time.monotonic", return_value=100.):
|
||||
assert op.in_control() is True
|
||||
with patch("openpilot.starpilot.models.jetlink_adapter.time.monotonic", return_value=100. + adapter.AOL_GRACE_S):
|
||||
assert op.in_control() is False
|
||||
|
||||
|
||||
def test_aols_reader_is_built_while_openpilot_is_engaged():
|
||||
# so it has heard AOL by the time openpilot disengages and an offer or a fall asks
|
||||
op = rule(core={"selfdriveState": SimpleNamespace(enabled=True, active=True)})
|
||||
op._aol = None
|
||||
with patch("openpilot.cereal.messaging.SubMaster", side_effect=lambda services: FakeSubMaster(services)) as built:
|
||||
assert op.in_control() is True
|
||||
assert op.in_control() is True
|
||||
built.assert_called_once_with(list(adapter.AOL_SERVICES))
|
||||
assert op._aol.timeouts == [0, 0]
|
||||
|
||||
|
||||
STATEFUL = {"input_shapes": {"new_img": (2, 6, 4, 8), "desire_pulse": (1, 8), "traffic_convention": (1, 2), "state_feat": (1, 3, 4)},
|
||||
"input_dtypes": {"state_feat": "float32"}, "state_pairs": ["state_feat"], "mapped_image_input": True}
|
||||
VISION = {"img": (1, 12, 4, 8), "big_img": (1, 12, 4, 8)}
|
||||
POLICY = {"features_buffer": (1, 3, 4), "desire_pulse": (1, 3, 8), "traffic_convention": (1, 2)}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", ["split", "supercombo", "stateful"])
|
||||
def test_the_small_models_history_clears_in_place(layout):
|
||||
# after the big model drove, the catalog runner starts from a zero history on the buffers its JIT captured
|
||||
from tinygrad import Device, Tensor
|
||||
from jetlink.openpilot import warp
|
||||
from openpilot.starpilot.models import compile as catalog
|
||||
from openpilot.starpilot.models.runner import CatalogModelState
|
||||
|
||||
queues, npy = {"split": lambda: catalog.make_split_input_queues(VISION, POLICY, 2, Device.DEFAULT),
|
||||
"supercombo": lambda: catalog.make_supercombo_input_queues({**VISION, **POLICY}, 2, Device.DEFAULT),
|
||||
"stateful": lambda: catalog.make_stateful_input_queues(STATEFUL, Device.DEFAULT)}[layout]()
|
||||
small = CatalogModelState.__new__(CatalogModelState)
|
||||
small.input_queues, small.npy, small.prev_desire = queues, npy, np.ones(8, dtype=np.float32)
|
||||
buffers = {name: q.uop.base.buffer for name, q in queues.items()}
|
||||
warp.init_device(logging.getLogger(__name__)) # as modeld: the reset's kernels compile with no compile pool
|
||||
reset = adapter.adapter().small_reset(small)
|
||||
history = [name for name, q in queues.items() if q.device != "NPY" and name not in adapter.FRAME_INPUTS]
|
||||
for _ in range(3):
|
||||
for name in history:
|
||||
queues[name].assign(Tensor.full(queues[name].shape, 7, dtype=queues[name].dtype, device=queues[name].device)).realize()
|
||||
for array in npy.values():
|
||||
array.fill(3)
|
||||
small.prev_desire.fill(1)
|
||||
reset()
|
||||
assert {name: q.uop.base.buffer for name, q in queues.items()} == buffers
|
||||
for name in history:
|
||||
assert not queues[name].numpy().any(), name
|
||||
assert not any(array.any() for array in npy.values())
|
||||
assert not queues["packed_npy_inputs"].numpy().any()
|
||||
assert not small.prev_desire.any()
|
||||
|
||||
|
||||
def test_the_reset_reads_what_the_catalog_runner_keeps():
|
||||
# small_reset reads these; a rename would leave every fallback on a stale history
|
||||
tree = ast.parse((ROOT / "openpilot/starpilot/models/runner.py").read_text())
|
||||
runner = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "CatalogModelState")
|
||||
assigned = {n.attr for n in ast.walk(runner) if isinstance(n, ast.Attribute) and isinstance(n.ctx, ast.Store)
|
||||
and isinstance(n.value, ast.Name) and n.value.id == "self"}
|
||||
assert {"input_queues", "npy", "prev_desire"} <= assigned
|
||||
|
||||
Reference in New Issue
Block a user