Refactor cellular data toggle descriptions for clarity and update GPS validation logic

This commit is contained in:
Prabhaav Pillai
2026-10-20 18:13:47 -04:00
parent a8d121bf45
commit cfaf5b05b2
6 changed files with 48 additions and 37 deletions
@@ -31,7 +31,7 @@ DESCRIPTIONS = {
),
'cellular_disabled': tr_noop(
"Hang up cellular data and don't redial. The modem stays registered and the SIM is untouched. " +
"Remote access over cellular stops until this is turned off."
"Remote access over cellular data is unavailable while this is enabled."
),
}
@@ -109,7 +109,7 @@ class DeveloperLayoutMici(NavScroller):
description="Show touch locations and the UI frame rate.")
self._cellular_off_toggle = BigParamControl("disable cellular", "CellularDisabled",
description="Hang up cellular data and don't redial. The modem stays registered and the " +
"SIM is untouched. Remote access over cellular stops until this is turned off.")
"SIM is untouched. Remote access over cellular data is unavailable while this is enabled.")
self._scroller.add_widgets([
self._adb_toggle,
+15 -9
View File
@@ -3,6 +3,8 @@ import { GxNotice } from "./notice.js"
import { GxState } from "./state.js"
import { GalaxySettingRow, switchRow } from "./galaxy-setting-row.js"
const validPassword = password => typeof password === 'string' && (password === '' || /^[\x21-\x7e]{12,63}$/.test(password))
export function validHotspot(value) {
return value?.version === 1 && /^[a-f0-9]{64}$/.test(value.revision) &&
typeof value.editable === 'boolean' && typeof value.active === 'boolean' &&
@@ -10,15 +12,18 @@ export function validHotspot(value) {
typeof value.reason === 'string' && value.reason.length <= 256 &&
value.url === 'http://172.31.254.1:8082/' && typeof value.config?.enabled === 'boolean' &&
/^TheGalaxy-[a-f0-9]{4}$/.test(value.config.ssid) &&
typeof value.config.password === 'string' && (value.config.password === '' || /^[\x21-\x7e]{12,63}$/.test(value.config.password))
validPassword(value.config.password)
}
export const GalaxyHotspot = {
components: { GxNotice, GxState, GalaxySettingRow },
props: { mode: String, unauthorized: Function },
data: () => ({ snapshot: null, draft: null, revision: '', busy: false, error: '', notice: '',
data: () => ({ snapshot: null, draft: null, revision: '', busy: false, error: '', saveError: '', notice: '',
showPassword: false, timer: null, controller: null, generation: 0, active: false }),
computed: { dirty() { return JSON.stringify(this.draft) !== JSON.stringify(this.snapshot?.config) } },
computed: {
dirty() { return JSON.stringify(this.draft) !== JSON.stringify(this.snapshot?.config) },
passwordValid() { return validPassword(this.draft?.password) },
},
mounted() { this.start() },
beforeUnmount() { this.stop() },
watch: { mode() { this.stop(); this.start() } },
@@ -27,7 +32,7 @@ export const GalaxyHotspot = {
start() { if (this.mode === 'local') { this.active = true; this.refresh() } },
stop() {
this.active = false; this.generation++; clearTimeout(this.timer); this.controller?.abort()
this.snapshot = this.draft = null; this.revision = ''; this.showPassword = false; this.busy = false
this.snapshot = this.draft = null; this.revision = ''; this.showPassword = false; this.busy = false; this.saveError = ''
},
adopt(value, reset = false) {
if (!validHotspot(value)) throw new Error('Invalid hotspot status')
@@ -53,10 +58,10 @@ export const GalaxyHotspot = {
}
},
async save() {
if (!this.active || this.busy || !this.snapshot?.editable) return
if (!this.active || this.busy || !this.snapshot?.editable || !this.passwordValid) return
clearTimeout(this.timer); this.controller?.abort()
const generation = ++this.generation
this.controller = new AbortController(); this.busy = true; this.error = ''; this.notice = ''
this.controller = new AbortController(); this.busy = true; this.saveError = ''; this.notice = ''
try {
const value = await requestJson('./api/galaxy/hotspot', { request: { signal: this.controller.signal,
method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -68,7 +73,7 @@ export const GalaxyHotspot = {
} catch (error) {
if (!this.active || generation !== this.generation) return
if (error.status === 401) { this.stop(); this.unauthorized(); return }
this.error = `${error.message} If Wi-Fi disconnected, reconnect and reload to check whether settings saved.`
this.saveError = `${error.message} If Wi-Fi disconnected, reconnect and reload to check whether settings saved.`
} finally {
if (this.active && generation === this.generation) { this.busy = false; this.timer = setTimeout(() => this.refresh(), 1000) }
}
@@ -87,7 +92,8 @@ export const GalaxyHotspot = {
<p class="gx-note">Fixed to TheGalaxy- plus the last four characters of your comma dongle ID. Once saved, the switch and password survive restarts.</p>
<label class="gx-field-group">Password <input class="gx-field" :type="showPassword ? 'text' : 'password'" v-model="draft.password" :disabled="busy || !snapshot.editable" maxlength="63" autocomplete="new-password" placeholder="Blank generates a password" /></label>
<label><input type="checkbox" v-model="showPassword" :disabled="busy || !snapshot.editable" /> Show password</label>
<div class="gx-settings__controls gx-actions"><button class="gx-btn" type="button" :disabled="!dirty || busy || !snapshot.editable" @click="save">{{ busy ? 'Saving…' : 'Save hotspot' }}</button></div>
<p v-if="!passwordValid" class="gx-note">Use a 12–63 character password without spaces, or leave it blank to generate one.</p>
<div class="gx-settings__controls gx-actions"><button class="gx-btn" type="button" :disabled="!dirty || busy || !snapshot.editable || !passwordValid" @click="save">{{ busy ? 'Saving…' : 'Save hotspot' }}</button></div>
</div>
<GxNotice v-if="!snapshot.editable">Park before changing hotspot settings.</GxNotice>
<p>Join the network, stay connected if warned of no Internet, then open and bookmark <a :href="snapshot.url">{{ snapshot.url }}</a>.</p>
@@ -99,6 +105,6 @@ export const GalaxyHotspot = {
<li><i class="bi bi-arrow-repeat" aria-hidden="true"></i><span>Network or channel changes may interrupt access. Changing the password or disabling the hotspot disconnects phones.</span></li>
<li><i class="bi bi-globe" aria-hidden="true"></i><span>The public galaxy.firestar.link address still needs Internet and its tunnel.</span></li>
</ul>
<GxNotice v-if="error" tone="danger">{{ error }}</GxNotice><GxNotice v-if="notice" tone="success">{{ notice }}</GxNotice>
<GxNotice v-if="error" tone="danger">{{ error }}</GxNotice><GxNotice v-if="saveError" tone="danger">{{ saveError }}</GxNotice><GxNotice v-if="notice" tone="success">{{ notice }}</GxNotice>
</section>`,
}
+12 -6
View File
@@ -23,6 +23,13 @@ def _optional(payload, key, low, high):
return value if low <= value < high else math.nan
def _required(payload, key, low, high, *, strict_low=False):
value = _number(payload, key, True)
if (value <= low if strict_low else value < low) or value >= high:
raise ValueError(f"Invalid {key}")
return value
def phone_gps(payload, now_ns):
if not isinstance(payload, dict) or set(payload) - FIELDS:
raise ValueError("Unexpected location fields")
@@ -37,18 +44,17 @@ def phone_gps(payload, now_ns):
timestamp = payload.get("timestamp", 0)
if type(timestamp) is not int or not 0 <= timestamp < 2 ** 53:
raise ValueError("Invalid timestamp")
bearing_accuracy = _optional(payload, "bearingAccuracy", 0, 180)
return {
"sourceMonoTime": now_ns - int(age_ms * 1e6),
"latitude": latitude,
"longitude": longitude,
"altitude": _optional(payload, "altitude", -1000, 10_000),
"speed": _optional(payload, "speed", 0, 100),
"bearingDeg": _optional(payload, "bearing", 0, 360) if math.isfinite(bearing_accuracy) else math.nan,
"altitude": _required(payload, "altitude", -1000, 10_000),
"speed": _required(payload, "speed", 0, 100),
"bearingDeg": _required(payload, "bearing", 0, 360),
"horizontalAccuracy": accuracy,
"verticalAccuracy": _optional(payload, "verticalAccuracy", 0, 10_000),
"bearingAccuracyDeg": bearing_accuracy,
"speedAccuracy": _optional(payload, "speedAccuracy", 0, 100),
"bearingAccuracyDeg": _required(payload, "bearingAccuracy", 0, 180, strict_low=True),
"speedAccuracy": _required(payload, "speedAccuracy", 0, 100),
"unixTimestampMillis": timestamp,
"hasFix": True,
}
@@ -15,8 +15,8 @@ from openpilot.starpilot.navigation.status import NavigationStatusSource
NOW = 101_000_000_000
PATH = Path(tempfile.gettempdir()) / "phone_gps.json"
FIX = {"latitude": 40., "longitude": -110., "horizontalAccuracy": 5., "ageMs": 300, "speed": 20., "bearing": 90.,
"bearingAccuracy": 10., "timestamp": 1_790_000_000_000}
FIX = {"latitude": 40., "longitude": -110., "horizontalAccuracy": 5., "ageMs": 300, "altitude": 100., "speed": 20., "bearing": 90.,
"bearingAccuracy": 10., "speedAccuracy": 1., "timestamp": 1_790_000_000_000}
class Messages:
@@ -105,13 +105,22 @@ def test_phone_handoff_ignores_missing_and_damaged_files(tmp_path):
assert read_phone_gps(NOW, tmp_path / "bad.json") is None
def test_unknown_values_are_not_used():
gps = phone_gps({**FIX, "speed": -1, "bearing": 45., "bearingAccuracy": -1, "verticalAccuracy": -1, "altitude": None}, NOW)
assert all(math.isnan(gps[key]) for key in ("speed", "bearingDeg", "bearingAccuracyDeg", "verticalAccuracy", "altitude"))
def test_unknown_vertical_accuracy_is_not_used():
gps = phone_gps({**FIX, "verticalAccuracy": -1}, NOW)
assert math.isnan(gps["verticalAccuracy"])
@pytest.mark.parametrize("field", ("altitude", "speed", "bearing", "bearingAccuracy", "speedAccuracy"))
def test_partial_fix_is_rejected(field):
payload = dict(FIX)
payload.pop(field)
with pytest.raises(ValueError, match=field):
phone_gps(payload, NOW)
@pytest.mark.parametrize("change", [{"ageMs": 2500}, {"ageMs": -1}, {"latitude": 91.}, {"latitude": 0., "longitude": 0.},
{"horizontalAccuracy": 0}, {"latitude": True}, {"extra": 1}, {"timestamp": 1.5}])
{"horizontalAccuracy": 0}, {"latitude": True}, {"extra": 1}, {"timestamp": 1.5},
{"speed": -1}, {"altitude": None}, {"bearingAccuracy": 0}, {"speedAccuracy": -1}])
def test_invalid_fixes_are_rejected(change):
with pytest.raises(ValueError):
phone_gps({**FIX, **change}, NOW)
@@ -188,9 +197,11 @@ def test_phone_map_lease_is_capped_without_shortening_source_timeout():
status.boot_clock = lambda: clock[0] + 1_000_000_000
publish(sm)
with patch('openpilot.starpilot.navigation.status.gps_mode', return_value='backup'):
assert status.map_position()['validForMs'] == 2500
position = status.map_position()
assert position is not None and position['validForMs'] == 2500
clock[0] += 8_000_000_000
assert status.map_position()['validForMs'] == 1700
position = status.map_position()
assert position is not None and position['validForMs'] == 1700
clock[0] += 2_000_000_000
assert status.map_position() is None
-12
View File
@@ -229,13 +229,6 @@ def main() -> None:
# SystemExit on sigterm
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit(1))
analytics = None
try:
from openpilot.starpilot.analytics.runtime import start_optional_worker
analytics = start_optional_worker()
except Exception:
cloudlog.warning("Usage statistics worker unavailable")
try:
manager_thread()
except Exception:
@@ -244,11 +237,6 @@ def main() -> None:
sentry.capture_exception()
sentry.flush()
finally:
if analytics is not None:
try:
analytics.close()
except Exception:
cloudlog.warning("Usage statistics shutdown failed")
try:
from openpilot.starpilot.system.jetlink.lifecycle import shutdown as shutdown_jetlink
shutdown_jetlink(Params())