diff --git a/openpilot/starpilot/galaxy/tests/test_models.mjs b/openpilot/starpilot/galaxy/tests/test_models.mjs index de602dbd6f..72b0c71b57 100644 --- a/openpilot/starpilot/galaxy/tests/test_models.mjs +++ b/openpilot/starpilot/galaxy/tests/test_models.mjs @@ -142,7 +142,10 @@ assert.equal(modelActionAllowed(managerPayload, "download", missing), true) assert.equal(modelActionAllowed(managerPayload, "delete", big), false) assert.equal(modelActionAllowed({ ...managerPayload, activeBigModel: "" }, "delete", big), true) assert.equal(modelActionAllowed({ ...managerPayload, isOnroad: true }, "favorite", small), true) -assert.equal(modelActionAllowed({ ...managerPayload, isOnroad: true }, "select-small", small), false) +assert.equal(modelActionAllowed({ ...managerPayload, isOnroad: true }, "select-small", small), true) +assert.equal(modelActionAllowed({ ...managerPayload, isOnroad: true }, "select-big", big), true) +assert.equal(modelActionAllowed({ ...managerPayload, isOnroad: true, jetlinkBig: true }, "select-big", big), false) +assert.equal(modelActionAllowed({ ...managerPayload, isOnroad: true }, "download", missing), false) assert.equal(modelActionAllowed({ ...managerPayload, isOnroad: true, downloading: true }, "cancel"), true) assert.equal(modelActionAllowed({ ...managerPayload, capabilities: {} }, "favorite", small), false) assert.equal(fitsModelProfile({ ...small, profiles: ["small", "big"] }, "big"), true) @@ -239,7 +242,7 @@ await disabledRandomizer randomManagement.manager.stop() console.log('Randomizer and exclusion interactions passed') -assert.match(ModelsPage.template, /Park the vehicle before changing or downloading models/) +assert.match(ModelsPage.template, /Selections apply next onroad cycle/) assert.doesNotMatch(ModelsPage.template, /Onroad: switching/) assert.equal((ModelsPage.template.match(/class="gx-model-control"/g) || []).length, 2) assert.equal(ModelsPage.data().hardwareFilter, 'both') @@ -315,7 +318,7 @@ const refreshingCatalog = { ...managerPayload, downloading: true, refreshing: tr assert.equal(modelActionAllowed(refreshingCatalog, "select-small", small), true) assert.equal(modelActionAllowed(refreshingCatalog, "select-big", big), true) assert.equal(modelActionAllowed(refreshingCatalog, "select-big", missing), false) -assert.equal(modelActionAllowed({ ...refreshingCatalog, isOnroad: true }, "select-big", big), false) +assert.equal(modelActionAllowed({ ...refreshingCatalog, isOnroad: true }, "select-big", big), true) assert.equal(modelActionAllowed({ ...refreshingCatalog, randomizer: true }, "select-big", big), false) assert.equal(modelActionAllowed({ ...refreshingCatalog, refreshing: false }, "select-big", big), false) assert.equal(modelActionAllowed(refreshingCatalog, "delete", small), false) diff --git a/openpilot/starpilot/galaxy/web/js/models.js b/openpilot/starpilot/galaxy/web/js/models.js index a794e7bc07..b5ac4ccdc3 100644 --- a/openpilot/starpilot/galaxy/web/js/models.js +++ b/openpilot/starpilot/galaxy/web/js/models.js @@ -162,9 +162,10 @@ export function fitsModelProfile(model, profile) { export function modelActionAllowed(data, action, model = null) { if (!data || data.capabilities?.[actionCapabilities[action]] !== true) return false - if (data.isOnroad !== false && !["favorite", "unfavorite", "cancel"].includes(action)) return false + if (data.isOnroad !== false && !["favorite", "unfavorite", "cancel", "select-small", "select-big"].includes(action)) return false if (action === "jetlink") return data.jetlink !== undefined if (["select-small", "select-big"].includes(action)) { + if (action === "select-big" && data.jetlinkBig && data.isOnroad !== false) return false if (data.downloading && data.refreshing !== true || data.randomizer === true) return false if (action === "select-big" && model === null) return true // the manager marks what each Active can take: installed models, or under Jetlink a big one with an ONNX @@ -469,7 +470,7 @@ export const ModelsPage = { if (!this.disposed && result) { this.trackingProgress = ["download", "downloadAll", "refresh"].includes(action) this.message = (this.trackingProgress && this.status.progress) || result.message || - (action.startsWith("select-") ? "Selection saved for the next start." : "Saved.") + (action.startsWith("select-") ? "Selection saved for the next onroad cycle." : "Saved.") } } finally { if (!this.disposed) { this.pendingChoice = null; this.busy = "" } } }, @@ -485,7 +486,7 @@ export const ModelsPage = {

Manage models, downloads, and Jetlink connections.

{{ message }} {{ error }} - Park the vehicle before changing or downloading models. + Selections apply next onroad cycle. Park before downloading models or changing Jetlink. {{ status.progress || 'Keep the device connected until the download finishes.' }}
@@ -544,14 +545,14 @@ export const ModelsPage = {
- +
- + diff --git a/openpilot/starpilot/models/manager.py b/openpilot/starpilot/models/manager.py index e9a806a0ee..3a4db0a2a8 100644 --- a/openpilot/starpilot/models/manager.py +++ b/openpilot/starpilot/models/manager.py @@ -557,7 +557,7 @@ class ModelManager: "modelToDownload": job.get("model", ""), "downloadAll": job.get("downloadAll", False), "downloading": job["downloading"], "refreshing": job["refreshing"], "progress": job.get("progress", ""), "isOnroad": not parked, "gpuAvailable": gpu, "jetlink": jetlink, - "capabilities": {"select": parked, "download": parked, "downloadAll": parked, "cancel": True, + "capabilities": {"select": True, "download": parked, "downloadAll": parked, "cancel": True, "jetlink": parked, "refresh": parked, "favorites": True, "delete": parked, "randomizer": parked, "exclusions": parked}} def _require_parked(self) -> None: @@ -609,7 +609,8 @@ class ModelManager: saved.update(payload) atomic_json(self.root / "preferences.json", saved) return {"message": "Model preferences saved"} - self._require_parked() + if action != "active": + self._require_parked() if action in ("download", "download_all", "refresh_manifest"): if self.worker is not None and self.worker.is_alive(): raise ModelError("A model download is already running") @@ -682,7 +683,8 @@ class ModelManager: elif not self._installed(mid, entries): raise ModelError("Download and verify this model first") with state_lock(self.root): - self._require_parked() + if jetlink: + self._require_parked() saved = preferences(self.root) if saved["randomizer"]: raise ModelError("Turn Model Randomizer off before choosing a model") diff --git a/openpilot/starpilot/models/tests/test_manager.py b/openpilot/starpilot/models/tests/test_manager.py index 3f1ad5c357..c8b7fc898a 100644 --- a/openpilot/starpilot/models/tests/test_manager.py +++ b/openpilot/starpilot/models/tests/test_manager.py @@ -286,13 +286,20 @@ class ModelManagerTest(unittest.TestCase): self.assertFalse(self.model_status()['installed']) self.assertTrue(self.wait_for_check()['installed']) - def test_offroad_authority_rechecked_after_validation(self): + def test_onroad_selection_preserves_current_runtime_until_next_start(self): self.download() - samples = iter([True, False]) - self.manager.parked = lambda: next(samples) + current = resolve_runtime(False, root=self.root) + self.manager.parked = lambda: False + self.assertTrue(self.manager.snapshot()['capabilities']['select']) + self.manager.action('active', {'profile': 'small', 'model': 'gwm8223'}) + self.assertEqual(preferences(self.root)['small'], 'gwm8223') + self.assertEqual(current.small_id, DEFAULT_SMALL) + self.assertEqual(resolve_runtime(False, root=self.root).small_id, 'gwm8223') with self.assertRaises(ModelError): - self.manager.action('active', {'profile': 'small', 'model': 'gwm8223'}) - self.assertEqual(preferences(self.root)['small'], DEFAULT_SMALL) + self.manager.action('download', {'model': 'gwm8223'}) + owner = SimpleNamespace(chooses_big=lambda: True, choose=lambda *args: self.fail('Live Jetlink selection changed')) + with patch.object(self.manager, 'jetlink_owner', return_value=owner), self.assertRaises(ModelError): + self.manager.action('active', {'profile': 'big', 'model': ''}) def test_hardware_profiles_and_selected_delete(self): self.download() @@ -811,8 +818,8 @@ class ModelManagerTest(unittest.TestCase): with patch('openpilot.starpilot.models.manager.verified_artifact', side_effect=AssertionError('unchanged local file rehashed')): observer.action('active', {'profile': 'big', 'model': 'cinquev3'}) self.parked = False - with self.assertRaises(ModelError): - observer.action('active', {'profile': 'big', 'model': 'cinquev3'}) + observer.action('active', {'profile': 'big', 'model': 'cinquev3'}) + self.assertEqual(preferences(self.root)['big'], 'cinquev3') self.parked = True (self.root / 'cinquev3/cinquev3_driving_tinygrad.pkl').write_bytes(b'corrupt') with self.assertRaisesRegex(ModelError, 'verify'): diff --git a/openpilot/starpilot/ui/models_state.py b/openpilot/starpilot/ui/models_state.py index 613200a0d4..6e1a722ba1 100644 --- a/openpilot/starpilot/ui/models_state.py +++ b/openpilot/starpilot/ui/models_state.py @@ -61,9 +61,11 @@ def model_action_allowed(data: dict, action: str, model: dict | None = None, pro "exclusion": "exclusions"}.get(action, action) if data.get("capabilities", {}).get(capability) is not True: return False - if data.get("isOnroad") is not False and action not in ("preferences", "cancel"): + if data.get("isOnroad") is not False and action not in ("preferences", "cancel", "active"): return False if action == "active": + if profile == "big" and data.get("jetlinkBig") and data.get("isOnroad") is not False: + return False return (not data.get("randomizer") and (not data.get("downloading") or data.get("refreshing") is True) and profile in ("small", "big") and ((model is None and profile == "big") or bool(model and model.get("selectable") is True and model_fits_profile(model, profile)))) @@ -118,11 +120,12 @@ def model_manager_rows(data: dict) -> tuple[FeatureRow, ...]: for profile, field in (("small", "activeSmallModel"), ("big", "activeBigModel")): mid = data.get(field, "") label = next((m.get("label", mid) for m in data.get("models", ()) if m.get("value") == mid), mid or big_none_label(data)) - available = (data.get("capabilities", {}).get("select") is True and data.get("isOnroad") is False and + available = (data.get("capabilities", {}).get("select") is True and + not (profile == "big" and data.get("jetlinkBig") and data.get("isOnroad") is not False) and (not data.get("downloading") or data.get("refreshing") is True) and not data.get("randomizer")) rows.append(FeatureRow("", f"Active {profile.title()}", label, source=mid.encode(), available=available, reason="Randomizer selects at the next start" if data.get("randomizer") else - "Change for next start" if available else "Turn the vehicle off to change models", page=f"models:{profile}")) + "Applies next onroad cycle" if available else "Model selection unavailable", page=f"models:{profile}")) return tuple(rows) diff --git a/openpilot/starpilot/ui/tests/test_models.py b/openpilot/starpilot/ui/tests/test_models.py index 5e1edddcd0..d1b160faea 100644 --- a/openpilot/starpilot/ui/tests/test_models.py +++ b/openpilot/starpilot/ui/tests/test_models.py @@ -156,7 +156,10 @@ class TestModelManager(unittest.TestCase): self.assertTrue(model_action_allowed({**self.data, "activeBigModel": ""}, "delete", self.big)) self.assertFalse(model_action_allowed({**self.data, "capabilities": {}}, "active", self.small, "small")) moving = {**self.data, "isOnroad": True, "downloading": True} - self.assertFalse(model_action_allowed(moving, "active", self.small, "small")) + self.assertTrue(model_action_allowed(moving, "active", self.small, "small")) + self.assertTrue(model_action_allowed(moving, "active", self.big, "big")) + self.assertFalse(model_action_allowed(moving | {"jetlinkBig": True}, "active", self.big, "big")) + self.assertFalse(model_action_allowed(moving, "download", self.missing)) self.assertTrue(model_action_allowed(moving, "preferences", self.small)) self.assertTrue(model_action_allowed(moving, "cancel")) @@ -248,7 +251,7 @@ class TestModelManager(unittest.TestCase): self.assertTrue(model_action_allowed(data, "active", self.small, "small")) self.assertTrue(model_action_allowed(data, "active", self.big, "big")) self.assertFalse(model_action_allowed(data, "active", self.pending, "small")) - self.assertFalse(model_action_allowed(data | {"isOnroad": True}, "active", self.big, "big")) + self.assertTrue(model_action_allowed(data | {"isOnroad": True}, "active", self.big, "big")) self.assertFalse(model_action_allowed(data | {"randomizer": True}, "active", self.big, "big")) self.assertFalse(model_action_allowed(data | {"refreshing": False}, "active", self.big, "big")) self.assertFalse(model_action_allowed(data, "delete", self.small)) diff --git a/prebuilt.json b/prebuilt.json index e2fc2b882f..88f9bf5d5e 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "3f48edce7f387d0e45d93fd3e6dd8cdad10f0e88def45bb818e65882bf2d977e", "schema_version": 1} +{"protected_inventory_sha256": "03e24bc36f837e5456c78ef3a7b9a2739826cbe2ebe67a9f592987be876893e6", "schema_version": 1}