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())