From d69a9f4db56d96db050efae97981902f0e0621ae Mon Sep 17 00:00:00 2001
From: firestar5683 <168790843+firestar5683@users.noreply.github.com>
Date: Thu, 22 Oct 2026 10:52:58 -0500
Subject: [PATCH] Restore Small UI sign and parked Galaxy offroad
---
openpilot/starpilot/drive_state/control.py | 9 +++++-
openpilot/starpilot/drive_state/evidence.py | 4 +--
.../drive_state/tests/test_evidence.py | 11 +++++++
.../starpilot/drive_state/tests/test_http.py | 31 ++++++++++---------
openpilot/starpilot/galaxy/batch_routes.py | 5 +--
.../galaxy/tests/test_drive_state.mjs | 20 ++++++------
.../starpilot/galaxy/web/js/drive-state.js | 11 +++++--
.../galaxy/web/js/software-status.js | 5 +--
openpilot/starpilot/ui/speed_limit_widget.py | 3 +-
prebuilt.json | 2 +-
10 files changed, 61 insertions(+), 40 deletions(-)
diff --git a/openpilot/starpilot/drive_state/control.py b/openpilot/starpilot/drive_state/control.py
index ed658fd296..6972bff4f9 100644
--- a/openpilot/starpilot/drive_state/control.py
+++ b/openpilot/starpilot/drive_state/control.py
@@ -31,13 +31,20 @@ class DriveStateControl:
'overrideAllowed': self.cached[2], 'reason': self.cached[3]}
def change(self, mode, revision, authorized):
- # Deliberate local requests may stop driving services while moving; Galaxy rejects Offroad in its action handler.
+ # Deliberate local requests may stop driving services while moving; Galaxy uses a separate Park-checked request.
result = self.owner.request(mode, expected_revision=revision, authorized=authorized,
override_allowed=lambda: True)
with self.lock:
self.cached = None
return result
+ def change_galaxy(self, mode, revision, authorized):
+ result = self.owner.request(mode, expected_revision=revision, authorized=authorized,
+ override_allowed=lambda: self.physical.status(require_park=True)['allowed'])
+ with self.lock:
+ self.cached = None
+ return result
+
@staticmethod
def next_mode(status):
mode = Mode(status['mode'])
diff --git a/openpilot/starpilot/drive_state/evidence.py b/openpilot/starpilot/drive_state/evidence.py
index 7451400261..3b7a77f5d7 100644
--- a/openpilot/starpilot/drive_state/evidence.py
+++ b/openpilot/starpilot/drive_state/evidence.py
@@ -22,7 +22,7 @@ class PhysicalSource:
def allowed(self) -> bool:
return self.status()['allowed']
- def status(self, *, updating_offroad=False):
+ def status(self, *, updating_offroad=False, require_park=False):
def result(allowed, reason=None):
return {'allowed': bool(allowed), 'reason': reason}
with self.lock:
@@ -66,7 +66,7 @@ class PhysicalSource:
if len(pandas) == 0 or any(str(p.pandaType) == 'unknown' for p in pandas):
return result(False, 'No recognized Panda is reporting vehicle status')
ignition = any(p.ignitionLine or p.ignitionCan for p in pandas)
- if not ignition or updating_offroad and all(str(p.safetyModel) == 'noOutput' and not p.controlsAllowed for p in pandas):
+ if not require_park and (not ignition or updating_offroad and all(str(p.safetyModel) == 'noOutput' and not p.controlsAllowed for p in pandas)):
safe = all(str(p.safetyModel) == 'noOutput' and not p.controlsAllowed for p in pandas)
return result(safe, None if safe else 'Panda must confirm noOutput safety and controls disabled')
if not fresh('carState', True):
diff --git a/openpilot/starpilot/drive_state/tests/test_evidence.py b/openpilot/starpilot/drive_state/tests/test_evidence.py
index b8f9af2c24..4dc31fbe41 100644
--- a/openpilot/starpilot/drive_state/tests/test_evidence.py
+++ b/openpilot/starpilot/drive_state/tests/test_evidence.py
@@ -181,3 +181,14 @@ def test_forced_offroad_update_rechecks_freshness(fixture, service):
assert f.source.update_ready(True)
f.sm.logMonoTime[service] = 1
assert not f.source.update_ready(True)
+
+
+@pytest.mark.parametrize('ignition', [False, True])
+@pytest.mark.parametrize('gear', ['park', 'drive', 'reverse', 'neutral', 'unknown'])
+def test_galaxy_requires_fresh_explicit_park_even_with_ignition_off(fixture, ignition, gear):
+ f = fixture
+ f.sm.values['pandaStates'][0].ignitionLine = ignition
+ f.sm.values['carState'].gearShifter = gear
+ assert f.source.status(require_park=True)['allowed'] == (gear == 'park')
+ f.sm.logMonoTime['carState'] = 1
+ assert not f.source.status(require_park=True)['allowed']
diff --git a/openpilot/starpilot/drive_state/tests/test_http.py b/openpilot/starpilot/drive_state/tests/test_http.py
index 36ae119bf1..3b3bf55a1a 100644
--- a/openpilot/starpilot/drive_state/tests/test_http.py
+++ b/openpilot/starpilot/drive_state/tests/test_http.py
@@ -20,7 +20,7 @@ class ForceHttpTest(helpers.NavigationHttpTest):
self.drive = DriveStateOwner(self.params, Path(temporary.name) / 'drive', alive=lambda _: True)
self.drive.initialize(pid=1, birth=2, boot=BOOT)
self.allowed = True
- self.physical = NS(allowed=lambda: self.allowed)
+ self.physical = NS(allowed=lambda: self.allowed, status=lambda **kwargs: {"allowed": self.allowed, "reason": None})
self.control = DriveStateControl(self.drive, self.physical, effective=lambda: False)
original = helpers.make_server
with patch.object(helpers, 'make_server', side_effect=lambda **kwargs: original(**kwargs, drive_state=self.control)):
@@ -41,17 +41,17 @@ class ForceHttpTest(helpers.NavigationHttpTest):
self.assertEqual((result[1]['mode'], result[1]['effective']), ('onroad', 'offroad'))
status = self.request('/api/drive-state/status', remote=True, cookie=self.remote_cookie)
self.assertEqual(status[1]['revision'], result[1]['revision'])
- self.assertEqual(self.drive_action('offroad', remote=True)[0], 403)
+ self.assertEqual(self.drive_action('offroad', remote=True)[0], 200)
self.control.change('offroad', self.drive.snapshot().revision, lambda: True)
self.assertEqual(self.drive_action('auto', revision=result[1]['revision'])[0], 409)
- def test_galaxy_blocks_offroad_but_onroad_and_auto_remain_recoverable(self):
- self.assertEqual(self.drive_action('offroad')[0], 403)
+ def test_galaxy_requires_park_but_onroad_and_auto_remain_recoverable(self):
+ self.assertEqual(self.drive_action('offroad')[0], 200)
self.control.change('offroad', self.drive.snapshot().revision, lambda: True)
self.allowed = False
self.assertEqual(self.drive_action('onroad')[0], 200)
self.assertEqual(self.drive.snapshot().mode, Mode.ONROAD)
- self.assertEqual(self.drive_action('offroad')[0], 403)
+ self.assertEqual(self.drive_action('offroad')[0], 409)
self.assertEqual(self.drive.snapshot().mode, Mode.ONROAD)
self.assertEqual(self.drive_action('auto', remote=True)[0], 200)
self.assertEqual(self.drive.snapshot().mode, Mode.AUTO)
@@ -62,7 +62,7 @@ class ForceHttpTest(helpers.NavigationHttpTest):
self.assertEqual(self.drive_action('onroad', origin='https://unrelated.invalid')[0], 403)
self.assertEqual(self.drive_action('badmode')[0], 409)
self.assertEqual(self.params.writes, before)
- original_change = self.control.change
+ original_change = self.control.change_galaxy
def revoke_remote(mode, revision, authorized):
self.assertEqual(mode, 'onroad')
@@ -71,7 +71,7 @@ class ForceHttpTest(helpers.NavigationHttpTest):
self.assertFalse(authorized())
return original_change(mode, revision, authorized)
- with patch.object(self.control, 'change', side_effect=revoke_remote):
+ with patch.object(self.control, 'change_galaxy', side_effect=revoke_remote):
self.assertEqual(self.drive_action('onroad', remote=True)[0], 409)
self.assertEqual(self.params.writes, before)
@@ -82,14 +82,15 @@ class ForceHttpTest(helpers.NavigationHttpTest):
self.control.change('offroad', self.drive.snapshot().revision, lambda: True)
self.assertEqual(self.drive.snapshot().mode, Mode.OFFROAD)
- def test_galaxy_force_offroad_never_reaches_owner_for_local_or_remote_sessions(self):
+ def test_galaxy_offroad_rechecks_park_for_local_and_remote_sessions(self):
for remote in (False, True):
- for allowed in (False, True):
- self.allowed = allowed
- before = list(self.params.writes)
- with patch.object(self.control, 'change', side_effect=AssertionError('Galaxy must reject before dispatch')):
- self.assertEqual(self.drive_action('offroad', remote=remote)[0], 403)
- self.assertEqual(self.params.writes, before)
+ self.allowed = False
+ before = list(self.params.writes)
+ self.assertEqual(self.drive_action('offroad', remote=remote)[0], 409)
+ self.assertEqual(self.params.writes, before)
+ self.allowed = True
+ self.assertEqual(self.drive_action('offroad', remote=remote)[0], 200)
+ self.assertEqual(self.drive_action('auto', remote=remote)[0], 200)
def test_authenticated_force_offroad_from_live_onroad_state(self):
self._force_offroad_from_live_onroad_state(True)
@@ -117,7 +118,7 @@ class ForceHttpTest(helpers.NavigationHttpTest):
self.assertEqual(status[1]['effective'], 'onroad')
self.assertEqual(status[1]['overrideAllowed'], can_valid)
result = self.drive_action('offroad', remote=True)
- self.assertEqual(result[0], 403)
+ self.assertEqual(result[0], 200 if can_valid else 409)
self.control.change('offroad', self.drive.snapshot().revision, lambda: True)
self.assertEqual(self.drive.snapshot().mode, Mode.OFFROAD)
# hardwared consumes this same owner request through the actual resolver.
diff --git a/openpilot/starpilot/galaxy/batch_routes.py b/openpilot/starpilot/galaxy/batch_routes.py
index 0fc6ab91f7..d0a3a1e256 100644
--- a/openpilot/starpilot/galaxy/batch_routes.py
+++ b/openpilot/starpilot/galaxy/batch_routes.py
@@ -111,13 +111,10 @@ def change_drive_state(self, payload, effect_lock, drive_owner):
len(payload['revision']) != 32):
self.json(400, {'error': 'Invalid drive state request'})
return
- if payload['mode'] == 'offroad':
- self.json(403, {'error': 'Use the in-car Force Off-road control'})
- return
from openpilot.starpilot.drive_state.owner import Rejected
try:
with effect_lock:
- drive_owner().change(payload['mode'], payload['revision'], lambda: self.settings_session() == identity)
+ drive_owner().change_galaxy(payload['mode'], payload['revision'], lambda: self.settings_session() == identity)
result = drive_owner().snapshot()
except Rejected as error:
self.json(409, {'error': str(error)})
diff --git a/openpilot/starpilot/galaxy/tests/test_drive_state.mjs b/openpilot/starpilot/galaxy/tests/test_drive_state.mjs
index 1701dcd00e..61485cf47b 100644
--- a/openpilot/starpilot/galaxy/tests/test_drive_state.mjs
+++ b/openpilot/starpilot/galaxy/tests/test_drive_state.mjs
@@ -30,16 +30,14 @@ assert.equal(denied.error,'Drive state unavailable')
console.log('PASS: strict status; captured revision; aborted/out-of-order status; requested versus effective; unmount; unauthorized state')
const onroad=panel();onroad.state={...snapshot(),effective:'onroad',overrideAllowed:false}
-globalThis.window={confirm:()=>false}
-const before=requests.length
-await onroad.change('offroad');assert.equal(requests.length,before)
-window.confirm=()=>true
-await onroad.change('offroad');assert.equal(requests.length,before)
-onroad.state.overrideAllowed=true
-await onroad.change('offroad');assert.equal(requests.length,before)
-assert.match(DriveStatePanel.template, /\['onroad', 'auto'\]/)
-assert.doesNotMatch(DriveStatePanel.template, /v-for="mode in \['offroad'/)
-
+let before=requests.length
+const forceOffroad=onroad.change('offroad')
+assert.equal(requests.length,before+1)
+assert.equal(JSON.parse(requests.at(-1).options.body).mode,'offroad')
+requests.at(-1).resolve(response({error:'Park the vehicle'},409));await flush()
+requests.at(-1).resolve(response(snapshot()));await forceOffroad;await flush()
+assert.match(DriveStatePanel.template,/role="switch"/)
+before=requests.length
// A missing parked admission must not disable Galaxy Force Onroad.
onroad.state.overrideAllowed=false
const forceOnroad=onroad.change('onroad')
@@ -50,4 +48,4 @@ requests.at(-1).resolve(response(live));await flush()
requests.at(-1).resolve(response(live));await forceOnroad;await flush()
assert.equal(onroad.state.mode,'onroad')
assert.equal(DriveStatePanel.computed.pending.call(onroad),false)
-console.log('PASS: Galaxy has no Offroad action; direct JS Offroad dispatch is ignored; Force Onroad works without parked admission')
+console.log('PASS: Galaxy Offroad dispatch uses server admission; Force Onroad remains available')
diff --git a/openpilot/starpilot/galaxy/web/js/drive-state.js b/openpilot/starpilot/galaxy/web/js/drive-state.js
index 48548c1970..19701247fb 100644
--- a/openpilot/starpilot/galaxy/web/js/drive-state.js
+++ b/openpilot/starpilot/galaxy/web/js/drive-state.js
@@ -6,6 +6,7 @@ export const validDriveState = (value) => !!value && ["auto", "offroad", "onroad
export const DriveStatePanel = {
components: { GxNotice },
+ props: { offroadOnly: { type: Boolean, default: false } },
data: () => ({ state: null, error: "", busy: false }),
computed: {
pending() { return this.state?.mode !== "auto" && this.state?.effective !== this.state?.mode },
@@ -35,7 +36,7 @@ export const DriveStatePanel = {
},
async change(mode) {
if (this.busy || !this.state?.available) return
- if (!["onroad", "auto"].includes(mode)) return
+ if (!["onroad", "offroad", "auto"].includes(mode)) return
const revision = this.state.revision
this.busy = true
this.loading = false
@@ -59,8 +60,12 @@ export const DriveStatePanel = {
Device: {{ state?.effective === "onroad" ? "Onroad" : state?.effective === "offroad" ? "Offroad" : "Checking…" }}
-Auto follows the car. Force Onroad starts driving services at any time. Return to Auto restores normal operation.
-