diff --git a/openpilot/selfdrive/ui/layouts/settings/developer.py b/openpilot/selfdrive/ui/layouts/settings/developer.py
index 09c8590b26..f9dcae51f1 100644
--- a/openpilot/selfdrive/ui/layouts/settings/developer.py
+++ b/openpilot/selfdrive/ui/layouts/settings/developer.py
@@ -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."
),
}
diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/developer.py b/openpilot/selfdrive/ui/mici/layouts/settings/developer.py
index 462d2a9a63..7b3a68f55e 100644
--- a/openpilot/selfdrive/ui/mici/layouts/settings/developer.py
+++ b/openpilot/selfdrive/ui/mici/layouts/settings/developer.py
@@ -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,
diff --git a/openpilot/starpilot/galaxy/web/js/hotspot.js b/openpilot/starpilot/galaxy/web/js/hotspot.js
index cde50c6b0a..a453c82e6e 100644
--- a/openpilot/starpilot/galaxy/web/js/hotspot.js
+++ b/openpilot/starpilot/galaxy/web/js/hotspot.js
@@ -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 = {
Fixed to TheGalaxy- plus the last four characters of your comma dongle ID. Once saved, the switch and password survive restarts.
-
+ Use a 12–63 character password without spaces, or leave it blank to generate one.
+
Park before changing hotspot settings.
Join the network, stay connected if warned of no Internet, then open and bookmark {{ snapshot.url }}.
@@ -99,6 +105,6 @@ export const GalaxyHotspot = {
Network or channel changes may interrupt access. Changing the password or disabling the hotspot disconnects phones.
The public galaxy.firestar.link address still needs Internet and its tunnel.
- {{ error }}{{ notice }}
+ {{ error }}{{ saveError }}{{ notice }}
`,
}
diff --git a/openpilot/starpilot/gps/phone.py b/openpilot/starpilot/gps/phone.py
index d0c956d46a..03715c56c2 100644
--- a/openpilot/starpilot/gps/phone.py
+++ b/openpilot/starpilot/gps/phone.py
@@ -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,
}
diff --git a/openpilot/starpilot/gps/tests/test_phone_gps.py b/openpilot/starpilot/gps/tests/test_phone_gps.py
index e5883c0953..1b4670e921 100644
--- a/openpilot/starpilot/gps/tests/test_phone_gps.py
+++ b/openpilot/starpilot/gps/tests/test_phone_gps.py
@@ -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
diff --git a/openpilot/system/manager/manager.py b/openpilot/system/manager/manager.py
index f9b5bc9411..6bf56129b1 100755
--- a/openpilot/system/manager/manager.py
+++ b/openpilot/system/manager/manager.py
@@ -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())