mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 09:03:46 +08:00
Refactor cellular data toggle descriptions for clarity and update GPS validation logic
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>`,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user