Queue model selections for the next onroad cycle

This commit is contained in:
firestar5683
2026-11-05 12:03:41 -06:00
parent faf81bb461
commit 5c9eeb707a
7 changed files with 43 additions and 24 deletions
@@ -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)
+6 -5
View File
@@ -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 = {
<p>Manage models, downloads, and Jetlink connections.</p></div></header>
<GxNotice v-if="message" tone="info">{{ message }}</GxNotice>
<GxNotice v-if="error" tone="danger" title="Model Manager">{{ error }}</GxNotice>
<GxNotice v-if="status.isOnroad" tone="warn">Park the vehicle before changing or downloading models.</GxNotice>
<GxNotice v-if="status.isOnroad" tone="warn">Selections apply next onroad cycle. Park before downloading models or changing Jetlink.</GxNotice>
<GxNotice v-if="status.downloading" :title="'Downloading ' + downloadTargetLabel">{{ status.progress || 'Keep the device connected until the download finishes.' }}</GxNotice>
<section class="gx-card gx-model-overview">
<div class="gx-section__header">
@@ -544,14 +545,14 @@ export const ModelsPage = {
<div class="gx-model-control">
<label class="gx-row__label" for="gx-model-small">Active Small · On-device</label>
<GalaxySelect id="gx-model-small" class="gx-field" :value="pendingChoice?.action === 'select-small' ? pendingChoice.value : activeSmallModel" :disabled="selectionUncertain || capabilities.select !== true || status.isOnroad || status.randomizer" @change="runAction('select-small', installedSmallModels.find(m => m.value === $event.target.value))">
<GalaxySelect id="gx-model-small" class="gx-field" :value="pendingChoice?.action === 'select-small' ? pendingChoice.value : activeSmallModel" :disabled="selectionUncertain || capabilities.select !== true || status.randomizer" @change="runAction('select-small', installedSmallModels.find(m => m.value === $event.target.value))">
<option v-for="m in installedSmallModels" :key="m.value" :value="m.value" :disabled="!m.selectable">{{ m.label || m.value }}</option>
</GalaxySelect>
</div>
<div class="gx-model-control">
<label class="gx-row__label" for="gx-model-big">Active Big · GPU</label>
<GalaxySelect id="gx-model-big" class="gx-field" :value="pendingChoice?.action === 'select-big' ? pendingChoice.value : activeBigModel" :disabled="selectionUncertain || capabilities.select !== true || status.isOnroad || status.randomizer" @change="$event.target.value ? runAction('select-big', installedBigModels.find(m => m.value === $event.target.value)) : runAction('select-big')">
<GalaxySelect id="gx-model-big" class="gx-field" :value="pendingChoice?.action === 'select-big' ? pendingChoice.value : activeBigModel" :disabled="selectionUncertain || capabilities.select !== true || status.randomizer || status.jetlinkBig && status.isOnroad !== false" @change="$event.target.value ? runAction('select-big', installedBigModels.find(m => m.value === $event.target.value)) : runAction('select-big')">
<option value="">{{ bigNoneLabel }}</option>
<option v-for="m in installedBigModels" :key="m.value" :value="m.value" :disabled="!m.selectable">{{ m.label || m.value }}</option>
</GalaxySelect>
+5 -3
View File
@@ -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")
@@ -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'):
+6 -3
View File
@@ -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)
+5 -2
View File
@@ -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))
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "3f48edce7f387d0e45d93fd3e6dd8cdad10f0e88def45bb818e65882bf2d977e", "schema_version": 1}
{"protected_inventory_sha256": "03e24bc36f837e5456c78ef3a7b9a2739826cbe2ebe67a9f592987be876893e6", "schema_version": 1}