mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-18 15:52:09 +08:00
leedle
This commit is contained in:
@@ -462,6 +462,47 @@ def test_post_stop_speed_trigger_is_suppressed_after_red_light_release(monkeypat
|
||||
assert cem.status_value == conditional_experimental_mode_module.CEStatus["SPEED"]
|
||||
|
||||
|
||||
def test_post_stop_suppression_survives_hold_release_before_motion(monkeypatch):
|
||||
cem = make_cem(model_length=80.0, model_stopped=False)
|
||||
toggles = make_update_toggles()
|
||||
toggles.conditional_limit = 5.0 * CV.MPH_TO_MS
|
||||
standstill_sm = make_update_sm(standstill=True)
|
||||
moving_sm = make_update_sm(standstill=False)
|
||||
|
||||
now = [100.0]
|
||||
monkeypatch.setattr(conditional_experimental_mode_module.time, "monotonic", lambda: now[0])
|
||||
|
||||
def hold_red_light(*args, **kwargs):
|
||||
cem.stop_light_detected = True
|
||||
|
||||
monkeypatch.setattr(cem, "stop_sign_and_light", hold_red_light)
|
||||
cem.update(0.0, standstill_sm, toggles)
|
||||
assert cem.experimental_mode
|
||||
|
||||
def clear_red_light(*args, **kwargs):
|
||||
cem.stop_light_detected = False
|
||||
cem.stop_light_model_detected = False
|
||||
|
||||
monkeypatch.setattr(cem, "stop_sign_and_light", clear_red_light)
|
||||
|
||||
now[0] = 100.1
|
||||
cem.update(0.0, standstill_sm, toggles)
|
||||
assert not cem.experimental_mode
|
||||
assert cem.standstill_stop_release_pending
|
||||
|
||||
low_speed_trigger_v_ego = 4.0 * CV.MPH_TO_MS
|
||||
|
||||
now[0] = 100.2
|
||||
cem.update(low_speed_trigger_v_ego, moving_sm, toggles)
|
||||
assert not cem.experimental_mode
|
||||
assert not cem.standstill_stop_release_pending
|
||||
|
||||
now[0] = 102.5
|
||||
cem.update(low_speed_trigger_v_ego, moving_sm, toggles)
|
||||
assert cem.experimental_mode
|
||||
assert cem.status_value == conditional_experimental_mode_module.CEStatus["SPEED"]
|
||||
|
||||
|
||||
def test_post_stop_slow_lead_trigger_is_suppressed_after_red_light_release(monkeypatch):
|
||||
cem = make_cem(model_length=80.0, model_stopped=False)
|
||||
toggles = make_update_toggles()
|
||||
@@ -825,6 +866,43 @@ def test_tracked_following_slower_lead_still_triggers_slow_lead():
|
||||
assert cem.slow_lead_detected
|
||||
|
||||
|
||||
def test_tracked_vision_slow_lead_continues_existing_experimental_mode():
|
||||
v_ego = 8.0
|
||||
cem = make_cem(
|
||||
model_length=80.0,
|
||||
tracking_lead=True,
|
||||
lead_status=True,
|
||||
lead_d_rel=22.0,
|
||||
lead_v_lead=6.4,
|
||||
lead_model_prob=0.99,
|
||||
)
|
||||
toggles = SimpleNamespace(conditional_slower_lead=True, conditional_stopped_lead=False)
|
||||
|
||||
cem.prev_experimental_mode = True
|
||||
for _ in range(24):
|
||||
cem.slow_lead(toggles, v_ego)
|
||||
|
||||
assert cem.slow_lead_detected
|
||||
|
||||
|
||||
def test_tracked_vision_slow_lead_does_not_start_experimental_mode():
|
||||
v_ego = 8.0
|
||||
cem = make_cem(
|
||||
model_length=80.0,
|
||||
tracking_lead=True,
|
||||
lead_status=True,
|
||||
lead_d_rel=22.0,
|
||||
lead_v_lead=6.4,
|
||||
lead_model_prob=0.99,
|
||||
)
|
||||
toggles = SimpleNamespace(conditional_slower_lead=True, conditional_stopped_lead=False)
|
||||
|
||||
for _ in range(24):
|
||||
cem.slow_lead(toggles, v_ego)
|
||||
|
||||
assert not cem.slow_lead_detected
|
||||
|
||||
|
||||
def test_slow_lead_does_not_linger_at_crawl_when_stopped_lead_disabled():
|
||||
v_ego = 1.5
|
||||
cem = make_cem(
|
||||
|
||||
@@ -1065,6 +1065,7 @@ class StarPilotVariables:
|
||||
toggle.force_torque_controller = self.get_value("ForceTorqueController", condition=lateral_tuning and not is_torque_car and not is_angle_car)
|
||||
toggle.nnff = self.get_value("NNFF", condition=lateral_tuning and has_nnff and not is_angle_car)
|
||||
toggle.nnff_lite = self.get_value("NNFFLite", condition=not toggle.nnff and lateral_tuning and not is_angle_car)
|
||||
toggle.nav_desires_allowed = self.get_value("NavDesiresAllowed")
|
||||
toggle.use_turn_desires = self.get_value("TurnDesires", condition=lateral_tuning)
|
||||
|
||||
lkas_button_control = self.get_button_function("LKASButtonControl", condition=toggle.car_make != "subaru")
|
||||
|
||||
@@ -109,6 +109,7 @@ class ConditionalExperimentalMode:
|
||||
self._prev_ce_status = None
|
||||
self.prev_standstill = False
|
||||
self.prev_standstill_stop_hold = False
|
||||
self.standstill_stop_release_pending = False
|
||||
self.post_stop_launch_trigger_suppress_until = 0.0
|
||||
|
||||
def update(self, v_ego, sm, starpilot_toggles):
|
||||
@@ -116,13 +117,15 @@ class ConditionalExperimentalMode:
|
||||
standstill = bool(sm["carState"].standstill)
|
||||
current_standstill_stop_hold = False
|
||||
released_standstill_stop_hold = self.prev_standstill and self.prev_standstill_stop_hold and not standstill
|
||||
completed_pending_stop_release = self.standstill_stop_release_pending and not standstill
|
||||
|
||||
if released_standstill_stop_hold:
|
||||
if released_standstill_stop_hold or completed_pending_stop_release:
|
||||
self.post_stop_launch_trigger_suppress_until = now + self.POST_STOP_LAUNCH_TRIGGER_SUPPRESS_TIME
|
||||
self.mode_hold_until = 0.0
|
||||
self.mode_false_since = 0.0
|
||||
self.slow_lead_mode_hold_until = 0.0
|
||||
self.prev_experimental_mode = False
|
||||
self.standstill_stop_release_pending = False
|
||||
|
||||
if not standstill:
|
||||
self.standstill_stop_reason = None
|
||||
@@ -175,6 +178,14 @@ class ConditionalExperimentalMode:
|
||||
standstill_stop_hold = self.get_standstill_stop_hold(sm)
|
||||
current_standstill_stop_hold = standstill_stop_hold
|
||||
|
||||
if current_standstill_stop_hold:
|
||||
self.standstill_stop_release_pending = False
|
||||
elif self.prev_standstill_stop_hold:
|
||||
self.standstill_stop_release_pending = True
|
||||
|
||||
if self.standstill_stop_release_pending:
|
||||
self.post_stop_launch_trigger_suppress_until = now + self.POST_STOP_LAUNCH_TRIGGER_SUPPRESS_TIME
|
||||
|
||||
self.experimental_mode = standstill_stop_hold
|
||||
self.prev_experimental_mode = self.experimental_mode
|
||||
self.status_value = CEStatus["STOP_LIGHT"] if self.experimental_mode else CEStatus["OFF"]
|
||||
@@ -187,6 +198,7 @@ class ConditionalExperimentalMode:
|
||||
self.mode_false_since = 0.0
|
||||
self.slow_lead_mode_hold_until = 0.0
|
||||
self._prev_ce_status = None
|
||||
self.standstill_stop_release_pending = False
|
||||
self.experimental_mode = self.status_value == CEStatus["USER_OVERRIDDEN"]
|
||||
self.prev_experimental_mode = self.experimental_mode
|
||||
self.stop_light_detected &= not is_manual_ce_status(self.status_value)
|
||||
@@ -323,8 +335,14 @@ class ConditionalExperimentalMode:
|
||||
now < self.slow_lead_continuity_until and
|
||||
vision_slow_lead_candidate
|
||||
)
|
||||
tracked_vision_mode_continuation = bool(
|
||||
starpilot_toggles.conditional_slower_lead and
|
||||
tracking_lead and
|
||||
self.prev_experimental_mode and
|
||||
vision_slow_lead_candidate
|
||||
)
|
||||
|
||||
slow_lead_active = bool(slower_lead or raw_vision_slow_lead or stopped_lead)
|
||||
slow_lead_active = bool(slower_lead or raw_vision_slow_lead or stopped_lead or tracked_vision_mode_continuation)
|
||||
if slow_lead_active:
|
||||
self.slow_lead_clear_since = 0.0
|
||||
self.slow_lead_filter.update(True)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, reactive } from "/assets/vendor/arrow-core.js"
|
||||
import { createBrowserHistory, createRouter } from "/assets/vendor/remix-router-1.3.1.js"
|
||||
import { hideSidebar } from "/assets/js/utils.js"
|
||||
import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=flm-overrides-1"
|
||||
import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=favorite-live-values-1"
|
||||
import { ErrorLogs } from "/assets/components/tools/error_logs.js"
|
||||
import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js"
|
||||
import { GalaxyPairing } from "/assets/components/tools/galaxy.js"
|
||||
|
||||
@@ -14,6 +14,8 @@ let syncScheduled = false
|
||||
let lastParams = null
|
||||
let flmWorkspaceInflight = null
|
||||
let lastFlmWorkspaceFetch = 0
|
||||
let favoritePollInflight = null
|
||||
let favoritePollTimer = null
|
||||
const DYNAMIC_DEFAULT_DEP_KEYS = new Set(["AccelerationProfile", "EVTuning", "TruckTuning"])
|
||||
const PANDA_FIRMWARE_TOGGLE_KEYS = new Set(["IgnoreIgnitionLine", "RemoteStartBootsComma", "HKGRemoteStartBootsComma"])
|
||||
const FLM_ADVANCED_LATERAL_KEYS = new Set([
|
||||
@@ -531,7 +533,7 @@ function populateFavoriteSelect(index, selectEl = null) {
|
||||
async function fetchFavoriteSlots() {
|
||||
state.favoriteLoading = true
|
||||
try {
|
||||
const res = await fetch("/api/favorites/slots")
|
||||
const res = await fetch("/api/favorites/slots", { cache: "no-store" })
|
||||
const data = await res.json()
|
||||
if (res.ok) {
|
||||
state.favoriteOptions = normalizeFavoriteOptions(data.options)
|
||||
@@ -545,6 +547,45 @@ async function fetchFavoriteSlots() {
|
||||
state.favoriteLoading = false
|
||||
}
|
||||
|
||||
async function refreshFavoriteValues() {
|
||||
if (favoritePollInflight || state.favoriteSaving || state.favoriteLoading) return favoritePollInflight
|
||||
|
||||
favoritePollInflight = fetch("/api/favorites/values", { cache: "no-store" })
|
||||
.then(async res => {
|
||||
if (!res.ok) return
|
||||
|
||||
const data = await res.json()
|
||||
const values = (data.values && typeof data.values === "object") ? data.values : {}
|
||||
const changed = Object.entries(values).some(([key, value]) => state.values[key] !== value)
|
||||
if (!changed) return
|
||||
|
||||
state.favoriteValues = { ...state.favoriteValues, ...values }
|
||||
state.values = { ...state.values, ...values }
|
||||
scheduleSyncInputs()
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
favoritePollInflight = null
|
||||
})
|
||||
|
||||
return favoritePollInflight
|
||||
}
|
||||
|
||||
function ensureFavoriteValuePolling() {
|
||||
if (favoritePollTimer !== null) return
|
||||
|
||||
favoritePollTimer = setInterval(() => {
|
||||
if (!window.location.pathname.startsWith("/device_settings")) {
|
||||
clearInterval(favoritePollTimer)
|
||||
favoritePollTimer = null
|
||||
return
|
||||
}
|
||||
if (document.visibilityState === "visible") {
|
||||
refreshFavoriteValues()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function saveFavoriteSlots(slots) {
|
||||
if (state.favoriteSaving) return
|
||||
|
||||
@@ -1487,6 +1528,7 @@ export function DeviceSettings({ params }) {
|
||||
lastParams = params
|
||||
|
||||
fetchFlmWorkspace()
|
||||
ensureFavoriteValuePolling()
|
||||
|
||||
if (!state.fetched) {
|
||||
state.fetched = true
|
||||
|
||||
@@ -6,7 +6,6 @@ const FACTORY_RESET_STATUS_POLL_INTERVAL_MS = 1000
|
||||
|
||||
const state = reactive({
|
||||
showResetDefaultModal: false,
|
||||
showResetStockModal: false,
|
||||
showSaveMeModal: false,
|
||||
factoryResetBusy: false,
|
||||
factoryResetStatus: null,
|
||||
@@ -99,9 +98,18 @@ async function fetchFactoryResetStatus() {
|
||||
|
||||
async function restoreToggles(event) {
|
||||
const uploadedFile = event.target.files[0]
|
||||
if (uploadedFile) {
|
||||
if (!uploadedFile) return
|
||||
|
||||
try {
|
||||
if (uploadedFile.size > 5_000_000) {
|
||||
throw new Error("That toggle backup file is too large.")
|
||||
}
|
||||
|
||||
const fileContents = await uploadedFile.text()
|
||||
const toggleData = JSON.parse(fileContents)
|
||||
if (!toggleData || typeof toggleData !== "object" || Array.isArray(toggleData)) {
|
||||
throw new Error("That file is not a valid toggle backup.")
|
||||
}
|
||||
|
||||
const response = await fetch("/api/toggles/restore", {
|
||||
method: "POST",
|
||||
@@ -109,9 +117,17 @@ async function restoreToggles(event) {
|
||||
body: JSON.stringify(toggleData),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
const result = await response.json().catch(() => ({}))
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || "Failed to restore toggles.")
|
||||
}
|
||||
showSnackbar(result.message || "Toggles restored!")
|
||||
|
||||
} catch (error) {
|
||||
const message = error instanceof SyntaxError
|
||||
? "That file is not a valid toggle backup."
|
||||
: (error?.message || "Failed to restore toggles.")
|
||||
showSnackbar(message, "error")
|
||||
} finally {
|
||||
event.target.value = ""
|
||||
}
|
||||
}
|
||||
@@ -139,15 +155,27 @@ export function ToggleControl() {
|
||||
fetchFactoryResetStatus()
|
||||
|
||||
async function backupToggles() {
|
||||
const response = await fetch("/api/toggles/backup", { method: "POST" })
|
||||
const blob = await response.blob()
|
||||
try {
|
||||
const response = await fetch("/api/toggles/backup", { method: "POST" })
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}))
|
||||
throw new Error(result.message || "Failed to create toggle backup.")
|
||||
}
|
||||
|
||||
const downloadUrl = URL.createObjectURL(blob)
|
||||
const downloadLink = document.createElement("a")
|
||||
downloadLink.href = downloadUrl
|
||||
downloadLink.download = "toggle-backup.json"
|
||||
downloadLink.click()
|
||||
URL.revokeObjectURL(downloadUrl)
|
||||
const blob = await response.blob()
|
||||
const downloadUrl = URL.createObjectURL(blob)
|
||||
const downloadLink = document.createElement("a")
|
||||
downloadLink.href = downloadUrl
|
||||
downloadLink.download = "toggle-backup.json"
|
||||
downloadLink.style.display = "none"
|
||||
document.body.appendChild(downloadLink)
|
||||
downloadLink.click()
|
||||
downloadLink.remove()
|
||||
setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000)
|
||||
showSnackbar("Toggle backup downloaded.")
|
||||
} catch (error) {
|
||||
showSnackbar(error?.message || "Failed to create toggle backup.", "error")
|
||||
}
|
||||
}
|
||||
|
||||
function confirmResetDefault() {
|
||||
@@ -163,19 +191,6 @@ export function ToggleControl() {
|
||||
await fetch("/api/toggles/reset_default", { method: "POST" });
|
||||
}
|
||||
|
||||
function confirmResetStock() {
|
||||
state.showResetStockModal = true;
|
||||
}
|
||||
|
||||
async function resetTogglesToStock() {
|
||||
state.showResetStockModal = false;
|
||||
showSnackbar("Resetting toggles to stock openpilot values...");
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
showSnackbar("Rebooting...");
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
await fetch("/api/toggles/reset_stock", { method: "POST" });
|
||||
}
|
||||
|
||||
function triggerRestorePrompt() {
|
||||
fileInput.click()
|
||||
}
|
||||
@@ -229,16 +244,13 @@ export function ToggleControl() {
|
||||
</section>
|
||||
|
||||
<section class="toggle-control-widget" style="margin-left: 1.5rem">
|
||||
<div class="toggle-control-title">Reset Toggles to Default StarPilot</div>
|
||||
<div class="toggle-control-title">Reset Toggles to Default</div>
|
||||
<p class="toggle-control-text">
|
||||
Reset all toggles to default StarPilot settings.
|
||||
</p>
|
||||
<button class="toggle-control-button" @click="${confirmResetDefault}">
|
||||
Reset Toggles to Default
|
||||
</button>
|
||||
<button class="toggle-control-button" @click="${confirmResetStock}">
|
||||
Reset Toggles to Stock
|
||||
</button>
|
||||
<div class="toggle-control-danger-zone">
|
||||
<div class="toggle-control-danger-title">WARNING: Factory Reset</div>
|
||||
<p class="toggle-control-danger-text">
|
||||
@@ -277,13 +289,6 @@ export function ToggleControl() {
|
||||
onConfirm: resetTogglesToDefault,
|
||||
onCancel: () => { state.showResetDefaultModal = false; },
|
||||
confirmText: "Reset to Default"
|
||||
}) : ""}
|
||||
${() => state.showResetStockModal ? Modal({
|
||||
title: "Reset Toggles",
|
||||
message: "Are you sure you want to reset all toggles to stock openpilot values?",
|
||||
onConfirm: resetTogglesToStock,
|
||||
onCancel: () => { state.showResetStockModal = false; },
|
||||
confirmText: "Reset to Stock"
|
||||
}) : ""}
|
||||
${() => state.showSaveMeModal ? Modal({
|
||||
title: "SAVE ME",
|
||||
|
||||
@@ -96,7 +96,10 @@ def _install_server_import_stubs():
|
||||
)
|
||||
sys.modules["openpilot.system.hardware.hw"] = _simple_module(
|
||||
"openpilot.system.hardware.hw",
|
||||
Paths=SimpleNamespace(log_root=lambda **kwargs: "/tmp/dashboard-test-routes/"),
|
||||
Paths=SimpleNamespace(
|
||||
comma_home=lambda: "/tmp/dashboard-test-comma-home",
|
||||
log_root=lambda **kwargs: "/tmp/dashboard-test-routes/",
|
||||
),
|
||||
)
|
||||
sys.modules["openpilot.system.version"] = _simple_module("openpilot.system.version", get_build_metadata=lambda: SimpleNamespace())
|
||||
sys.modules["openpilot.tools.longitudinal_maneuvers.capabilities"] = _simple_module(
|
||||
@@ -183,6 +186,9 @@ def _install_server_import_stubs():
|
||||
"openpilot.starpilot.system.the_galaxy.factory_reset",
|
||||
remove_path=lambda *args, **kwargs: None,
|
||||
)
|
||||
sys.modules["openpilot.starpilot.system.the_galaxy.flm_workspace"] = _simple_module(
|
||||
"openpilot.starpilot.system.the_galaxy.flm_workspace",
|
||||
)
|
||||
sys.modules["openpilot.starpilot.system.the_galaxy.utilities"] = utilities
|
||||
|
||||
|
||||
@@ -1259,3 +1265,176 @@ def test_stats_endpoint_keeps_existing_keys_and_adds_dashboard(monkeypatch):
|
||||
assert include_response.status_code == 200
|
||||
assert ignored_calls == [route_name]
|
||||
assert included_calls == [route_name]
|
||||
|
||||
|
||||
def test_toggle_backup_restore_round_trip_filters_non_settings(monkeypatch):
|
||||
server = _load_server_module()
|
||||
assert server._import_galaxy_web_symbols()
|
||||
|
||||
persistent = server.ParamKeyFlag.PERSISTENT
|
||||
dont_log = server.ParamKeyFlag.DONT_LOG
|
||||
definitions = {
|
||||
"EnabledSetting": (True, server.ParamKeyType.BOOL, persistent),
|
||||
"AdbEnabled": (None, server.ParamKeyType.BOOL, persistent),
|
||||
"NumericSetting": (1.5, server.ParamKeyType.FLOAT, persistent),
|
||||
"JsonSetting": ({"mode": "default"}, server.ParamKeyType.JSON, persistent),
|
||||
"SensitiveSetting": ("", server.ParamKeyType.STRING, persistent | dont_log),
|
||||
"TransientSetting": (False, server.ParamKeyType.BOOL, server.ParamKeyFlag.CLEAR_ON_MANAGER_START),
|
||||
"NoDefaultSetting": (None, server.ParamKeyType.STRING, persistent),
|
||||
"StatsSetting": ({}, server.ParamKeyType.JSON, persistent),
|
||||
}
|
||||
|
||||
class ToggleParams:
|
||||
def __init__(self):
|
||||
self.values = {
|
||||
"EnabledSetting": False,
|
||||
"AdbEnabled": True,
|
||||
"NumericSetting": 2.75,
|
||||
"JsonSetting": {"mode": "custom"},
|
||||
"SensitiveSetting": "secret",
|
||||
"TransientSetting": True,
|
||||
"NoDefaultSetting": "runtime",
|
||||
"StatsSetting": {"drives": 10},
|
||||
}
|
||||
|
||||
def all_keys(self):
|
||||
return list(definitions)
|
||||
|
||||
def get(self, key, block=False):
|
||||
del block
|
||||
default = definitions[key][0]
|
||||
return self.values.get(key, default)
|
||||
|
||||
def get_default_value(self, key):
|
||||
return definitions[key][0]
|
||||
|
||||
def get_key_flag(self, key):
|
||||
return definitions[key][2]
|
||||
|
||||
def get_type(self, key):
|
||||
return definitions[key][1]
|
||||
|
||||
def get_tuning_level(self, key):
|
||||
del key
|
||||
return 0
|
||||
|
||||
def put(self, key, value):
|
||||
self.values[key] = value
|
||||
|
||||
raw_params = ToggleParams()
|
||||
server.starpilot_default_params = [
|
||||
(key, default, value_type, 0)
|
||||
for key, (default, value_type, _) in definitions.items()
|
||||
]
|
||||
server._cached_static_default_values = None
|
||||
monkeypatch.setattr(server, "_params_raw", raw_params)
|
||||
monkeypatch.setattr(server, "params", server.ParamsCompat(raw_params))
|
||||
monkeypatch.setattr(server, "EXCLUDED_KEYS", {"StatsSetting"})
|
||||
update_calls = []
|
||||
monkeypatch.setattr(server, "update_starpilot_toggles", lambda: update_calls.append(True))
|
||||
|
||||
app = server.Flask(
|
||||
"toggle_backup_test",
|
||||
template_folder=str(MODULE_DIR / "templates"),
|
||||
static_folder=str(MODULE_DIR / "assets"),
|
||||
)
|
||||
server.setup(app)
|
||||
client = app.test_client()
|
||||
|
||||
backup_response = client.post("/api/toggles/backup")
|
||||
backup_payload = json.loads(backup_response.data)
|
||||
backed_up_values = utilities.decode_parameters(backup_payload["data"])
|
||||
|
||||
assert backup_response.status_code == 200
|
||||
assert backup_payload["format"] == server.TOGGLE_BACKUP_FORMAT
|
||||
assert backup_payload["version"] == server.TOGGLE_BACKUP_VERSION
|
||||
assert backup_payload["settingsCount"] == 4
|
||||
assert backed_up_values == {
|
||||
"AdbEnabled": True,
|
||||
"EnabledSetting": False,
|
||||
"JsonSetting": {"mode": "custom"},
|
||||
"NumericSetting": 2.75,
|
||||
}
|
||||
|
||||
raw_params.values.update({
|
||||
"EnabledSetting": True,
|
||||
"AdbEnabled": False,
|
||||
"NumericSetting": 9.0,
|
||||
"JsonSetting": {"mode": "changed"},
|
||||
"SensitiveSetting": "new-secret",
|
||||
"TransientSetting": False,
|
||||
"NoDefaultSetting": "new-runtime",
|
||||
"StatsSetting": {"drives": 99},
|
||||
})
|
||||
restore_response = client.post("/api/toggles/restore", json={"data": backup_payload["data"]})
|
||||
restore_payload = restore_response.get_json()
|
||||
|
||||
assert restore_response.status_code == 200
|
||||
assert restore_payload["restoredCount"] == 4
|
||||
assert restore_payload["skippedCount"] == 0
|
||||
assert raw_params.values["AdbEnabled"] is True
|
||||
assert raw_params.values["EnabledSetting"] is False
|
||||
assert raw_params.values["NumericSetting"] == 2.75
|
||||
assert raw_params.values["JsonSetting"] == {"mode": "custom"}
|
||||
assert raw_params.values["SensitiveSetting"] == "new-secret"
|
||||
assert raw_params.values["TransientSetting"] is False
|
||||
assert raw_params.values["NoDefaultSetting"] == "new-runtime"
|
||||
assert raw_params.values["StatsSetting"] == {"drives": 99}
|
||||
assert update_calls == [True]
|
||||
|
||||
|
||||
def test_toggle_restore_reports_invalid_and_unavailable_settings(monkeypatch):
|
||||
server = _load_server_module()
|
||||
assert server._import_galaxy_web_symbols()
|
||||
|
||||
class ToggleParams:
|
||||
def __init__(self):
|
||||
self.values = {"EnabledSetting": False}
|
||||
|
||||
def get_key_flag(self, key):
|
||||
del key
|
||||
return server.ParamKeyFlag.PERSISTENT
|
||||
|
||||
def get_type(self, key):
|
||||
del key
|
||||
return server.ParamKeyType.BOOL
|
||||
|
||||
def put(self, key, value):
|
||||
self.values[key] = value
|
||||
|
||||
raw_params = ToggleParams()
|
||||
server.starpilot_default_params = [("EnabledSetting", True, server.ParamKeyType.BOOL, 0)]
|
||||
monkeypatch.setattr(server, "_params_raw", raw_params)
|
||||
monkeypatch.setattr(server, "EXCLUDED_KEYS", set())
|
||||
monkeypatch.setattr(server, "update_starpilot_toggles", lambda: None)
|
||||
|
||||
app = server.Flask(
|
||||
"toggle_restore_validation_test",
|
||||
template_folder=str(MODULE_DIR / "templates"),
|
||||
static_folder=str(MODULE_DIR / "assets"),
|
||||
)
|
||||
server.setup(app)
|
||||
client = app.test_client()
|
||||
|
||||
mixed_data = utilities.encode_parameters({
|
||||
"EnabledSetting": "true",
|
||||
"RemovedSetting": "1",
|
||||
})
|
||||
mixed_response = client.post("/api/toggles/restore", json={"data": mixed_data})
|
||||
mixed_payload = mixed_response.get_json()
|
||||
|
||||
assert mixed_response.status_code == 200
|
||||
assert mixed_payload["restoredCount"] == 1
|
||||
assert mixed_payload["skippedCount"] == 1
|
||||
assert raw_params.values["EnabledSetting"] is True
|
||||
|
||||
damaged_response = client.post("/api/toggles/restore", json={"data": "not-a-backup"})
|
||||
wrong_format_response = client.post("/api/toggles/restore", json={
|
||||
"format": "different-format",
|
||||
"data": mixed_data,
|
||||
})
|
||||
|
||||
assert damaged_response.status_code == 400
|
||||
assert damaged_response.get_json()["success"] is False
|
||||
assert wrong_format_response.status_code == 400
|
||||
assert wrong_format_response.get_json()["success"] is False
|
||||
|
||||
@@ -193,6 +193,38 @@ def test_galaxy_session_value_matches_cookie_format():
|
||||
) == f"testGalaxySlug01%3A{'a' * 64}"
|
||||
|
||||
|
||||
def test_configured_favorite_slot_values_only_reads_selected_keys(monkeypatch):
|
||||
fake_params = WritableFakeParams({
|
||||
"NavDesiresAllowed": False,
|
||||
"RedneckCruise": True,
|
||||
"UnusedToggle": True,
|
||||
})
|
||||
monkeypatch.setattr(the_galaxy, "params", fake_params)
|
||||
|
||||
values = the_galaxy._configured_favorite_slot_values([
|
||||
{"enabled": True, "key": "NavDesiresAllowed"},
|
||||
{"enabled": False, "key": "RedneckCruise"},
|
||||
{"enabled": False, "key": None},
|
||||
])
|
||||
|
||||
assert values == {"NavDesiresAllowed": False, "RedneckCruise": True}
|
||||
|
||||
|
||||
def test_favorite_values_endpoint_returns_current_selected_value(monkeypatch):
|
||||
client, _ = _params_client(monkeypatch, {"UseOldUI": False}, "tici")
|
||||
monkeypatch.setattr(the_galaxy, "_get_favorite_slot_options", lambda: [{"key": "UseOldUI"}])
|
||||
monkeypatch.setattr(
|
||||
the_galaxy,
|
||||
"normalize_favorite_slots",
|
||||
lambda *args, **kwargs: [{"enabled": True, "key": "UseOldUI"}],
|
||||
)
|
||||
|
||||
response = client.get("/api/favorites/values")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.get_json() == {"values": {"UseOldUI": False}}
|
||||
|
||||
|
||||
def test_use_old_ui_is_noop_on_c4_mici(monkeypatch):
|
||||
client, fake_params = _params_client(monkeypatch, {"UseOldUI": False, "IsOnroad": False}, "mici")
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from opendbc.car.gm.values import GMFlags
|
||||
from opendbc.car.toyota.carcontroller import LOCK_CMD, UNLOCK_CMD
|
||||
from opendbc.car.toyota.values import ToyotaStarPilotFlags
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import ParamKeyType, Params
|
||||
from openpilot.common.params import ParamKeyFlag, ParamKeyType, Params
|
||||
from openpilot.common.realtime import DT_HW
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
@@ -488,6 +488,100 @@ def _build_default_params():
|
||||
|
||||
starpilot_default_params = _build_default_params()
|
||||
|
||||
TOGGLE_BACKUP_FORMAT = "starpilot-toggle-backup"
|
||||
TOGGLE_BACKUP_VERSION = 1
|
||||
TOGGLE_BACKUP_MAX_ENCODED_BYTES = 2_000_000
|
||||
TOGGLE_BACKUP_NO_DEFAULT_KEYS = {
|
||||
"AdbEnabled",
|
||||
"AlphaLongitudinalEnabled",
|
||||
"AlwaysOnDM",
|
||||
"ExperimentalMode",
|
||||
"ExperimentalModeConfirmed",
|
||||
"IsLdwEnabled",
|
||||
"IsMetric",
|
||||
"IsRHD",
|
||||
"IsRHDOverride",
|
||||
"RecordAudio",
|
||||
"RecordFront",
|
||||
"SshEnabled",
|
||||
}
|
||||
|
||||
|
||||
def _get_toggle_backup_keys():
|
||||
keys = set()
|
||||
for key, default_value, _, _ in starpilot_default_params:
|
||||
if key in EXCLUDED_KEYS:
|
||||
continue
|
||||
if default_value is None and key not in TOGGLE_BACKUP_NO_DEFAULT_KEYS:
|
||||
continue
|
||||
|
||||
try:
|
||||
flags = _params_raw.get_key_flag(key)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not flags & ParamKeyFlag.PERSISTENT or flags & ParamKeyFlag.DONT_LOG:
|
||||
continue
|
||||
|
||||
keys.add(key)
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
def _coerce_toggle_restore_value(key, value):
|
||||
value_type = _get_param_key_type(_params_raw, key)
|
||||
|
||||
if value_type == ParamKeyType.BOOL:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, numbers.Real) and value in (0, 1):
|
||||
return bool(value)
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip().lower()
|
||||
if normalized in {"1", "true", "yes", "on"}:
|
||||
return True
|
||||
if normalized in {"0", "false", "no", "off", ""}:
|
||||
return False
|
||||
raise ValueError(f"Invalid boolean value for {key}")
|
||||
|
||||
if value_type == ParamKeyType.INT:
|
||||
if isinstance(value, bool):
|
||||
raise ValueError(f"Invalid integer value for {key}")
|
||||
return int(float(value))
|
||||
|
||||
if value_type == ParamKeyType.FLOAT:
|
||||
if isinstance(value, bool):
|
||||
raise ValueError(f"Invalid numeric value for {key}")
|
||||
result = float(value)
|
||||
if not math.isfinite(result):
|
||||
raise ValueError(f"Invalid numeric value for {key}")
|
||||
return result
|
||||
|
||||
if value_type == ParamKeyType.JSON:
|
||||
if isinstance(value, str):
|
||||
value = json.loads(value)
|
||||
if not isinstance(value, (dict, list)):
|
||||
raise ValueError(f"Invalid JSON value for {key}")
|
||||
return value
|
||||
|
||||
if value_type == ParamKeyType.BYTES:
|
||||
if isinstance(value, bytes):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return value.encode("utf-8")
|
||||
raise ValueError(f"Invalid byte value for {key}")
|
||||
|
||||
if value_type == ParamKeyType.TIME:
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return datetime.fromisoformat(value)
|
||||
raise ValueError(f"Invalid time value for {key}")
|
||||
|
||||
if value is None or isinstance(value, (dict, list)):
|
||||
raise ValueError(f"Invalid string value for {key}")
|
||||
return str(value)
|
||||
|
||||
params = ParamsCompat(_params_raw)
|
||||
params_memory = ParamsCompat(_params_memory_raw)
|
||||
STATS_RESPONSE_CACHE_SECONDS = 2.0
|
||||
@@ -2324,6 +2418,13 @@ def _favorite_slot_values(options):
|
||||
if option.get("key")
|
||||
}
|
||||
|
||||
def _configured_favorite_slot_values(slots):
|
||||
return {
|
||||
slot["key"]: _safe_params_get_bool(slot["key"])
|
||||
for slot in slots
|
||||
if slot.get("key")
|
||||
}
|
||||
|
||||
_cached_allowed_keys = None
|
||||
_cached_param_types = None
|
||||
_cached_default_values = None
|
||||
@@ -3789,6 +3890,7 @@ def setup(app):
|
||||
"/assets/components/tools/device_settings.js",
|
||||
"/assets/components/tools/device_settings.css",
|
||||
"/assets/components/tools/device_settings_layout.json",
|
||||
"/assets/components/tools/toggles.js",
|
||||
}:
|
||||
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
@@ -4140,6 +4242,13 @@ def setup(app):
|
||||
"values": _favorite_slot_values(options),
|
||||
}), 200
|
||||
|
||||
@app.route("/api/favorites/values", methods=["GET"])
|
||||
def favorite_values():
|
||||
options = _get_favorite_slot_options()
|
||||
eligible_keys = {option["key"] for option in options}
|
||||
slots = normalize_favorite_slots(params.get(FAVORITE_SLOTS_PARAM), params=params, eligible_keys=eligible_keys)
|
||||
return jsonify({"values": _configured_favorite_slot_values(slots)}), 200
|
||||
|
||||
@app.route("/api/params", methods=["GET", "PUT"])
|
||||
def get_param():
|
||||
if request.method == "PUT":
|
||||
@@ -7115,21 +7224,27 @@ def setup(app):
|
||||
@app.route("/api/toggles/backup", methods=["POST"])
|
||||
def backup_toggle_values():
|
||||
toggle_values = {}
|
||||
for key, _, _, _ in starpilot_default_params:
|
||||
if key in EXCLUDED_KEYS:
|
||||
default_values = _get_static_default_param_values()
|
||||
for key in sorted(_get_toggle_backup_keys()):
|
||||
raw_value = _params_raw.get(key)
|
||||
if raw_value is None:
|
||||
raw_value = default_values.get(key)
|
||||
if raw_value is None:
|
||||
continue
|
||||
|
||||
raw_value = params.get(key)
|
||||
value = _sanitize_json_value(raw_value)
|
||||
if value is None:
|
||||
value = "0"
|
||||
elif not isinstance(value, (str, int, float, bool, dict, list)):
|
||||
if not isinstance(value, (str, int, float, bool, dict, list)):
|
||||
value = str(value)
|
||||
|
||||
toggle_values[key] = value
|
||||
|
||||
encoded = utilities.encode_parameters(toggle_values)
|
||||
wrapped = json.dumps({"data": encoded}, indent=2)
|
||||
wrapped = json.dumps({
|
||||
"format": TOGGLE_BACKUP_FORMAT,
|
||||
"version": TOGGLE_BACKUP_VERSION,
|
||||
"createdAt": datetime.now(timezone.utc).isoformat(),
|
||||
"settingsCount": len(toggle_values),
|
||||
"data": encoded,
|
||||
}, indent=2)
|
||||
|
||||
buffer = BytesIO(wrapped.encode("utf-8"))
|
||||
buffer.seek(0)
|
||||
@@ -7138,30 +7253,78 @@ def setup(app):
|
||||
|
||||
@app.route("/api/toggles/restore", methods=["POST"])
|
||||
def restore_toggle_values():
|
||||
request_data = request.get_json()
|
||||
if not request_data or "data" not in request_data:
|
||||
return jsonify({"success": False, "message": "Missing 'data' in request."}), 400
|
||||
request_data = request.get_json(silent=True)
|
||||
if not isinstance(request_data, dict):
|
||||
return jsonify({"success": False, "message": "Invalid toggle backup file."}), 400
|
||||
|
||||
allowed_keys = {key for key, _, _, _ in starpilot_default_params if key not in EXCLUDED_KEYS}
|
||||
backup_format = request_data.get("format")
|
||||
if backup_format not in (None, TOGGLE_BACKUP_FORMAT):
|
||||
return jsonify({"success": False, "message": "This file is not a Galaxy toggle backup."}), 400
|
||||
|
||||
toggle_values = utilities.decode_parameters(request_data["data"])
|
||||
backup_version = request_data.get("version", 1)
|
||||
if not isinstance(backup_version, int) or backup_version > TOGGLE_BACKUP_VERSION:
|
||||
return jsonify({"success": False, "message": "This toggle backup requires a newer Galaxy version."}), 400
|
||||
|
||||
encoded_data = request_data.get("data")
|
||||
if not isinstance(encoded_data, str) or not encoded_data.strip():
|
||||
return jsonify({"success": False, "message": "Toggle backup data is missing."}), 400
|
||||
if len(encoded_data.encode("utf-8")) > TOGGLE_BACKUP_MAX_ENCODED_BYTES:
|
||||
return jsonify({"success": False, "message": "Toggle backup file is too large."}), 413
|
||||
|
||||
try:
|
||||
toggle_values = utilities.decode_parameters(encoded_data)
|
||||
except Exception:
|
||||
return jsonify({"success": False, "message": "Toggle backup data is damaged or invalid."}), 400
|
||||
if not isinstance(toggle_values, dict):
|
||||
return jsonify({"success": False, "message": "Toggle backup does not contain settings."}), 400
|
||||
|
||||
allowed_keys = _get_toggle_backup_keys()
|
||||
restored_count = 0
|
||||
skipped_count = 0
|
||||
for key, value in toggle_values.items():
|
||||
if not isinstance(key, str):
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
mapped_key = LEGACY_STARPILOT_PARAM_RENAMES.get(key, key)
|
||||
if mapped_key in allowed_keys:
|
||||
params.put(mapped_key, value)
|
||||
if mapped_key not in allowed_keys:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
_params_raw.put(mapped_key, _coerce_toggle_restore_value(mapped_key, value))
|
||||
restored_count += 1
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
skipped_count += 1
|
||||
|
||||
if restored_count == 0:
|
||||
return jsonify({"success": False, "message": "No compatible toggle settings were found in this backup."}), 400
|
||||
|
||||
update_starpilot_toggles()
|
||||
return jsonify({"success": True, "message": "Toggles restored!"})
|
||||
message = f"Restored {restored_count} toggle settings."
|
||||
if skipped_count:
|
||||
message += f" Skipped {skipped_count} incompatible or unavailable settings."
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": message,
|
||||
"restoredCount": restored_count,
|
||||
"skippedCount": skipped_count,
|
||||
})
|
||||
|
||||
@app.route("/api/toggles/reset_default", methods=["POST"])
|
||||
def reset_toggle_values():
|
||||
params.put_bool("DoToggleReset", True)
|
||||
HARDWARE.reboot()
|
||||
for raw_key in _params_raw.all_keys():
|
||||
key = raw_key.decode() if isinstance(raw_key, bytes) else str(raw_key)
|
||||
if key in EXCLUDED_KEYS:
|
||||
continue
|
||||
|
||||
@app.route("/api/toggles/reset_stock", methods=["POST"])
|
||||
def reset_toggle_values_to_stock():
|
||||
params.put_bool("DoToggleResetStock", True)
|
||||
default_value = _params_raw.get_default_value(raw_key)
|
||||
if default_value is not None:
|
||||
_params_raw.put(raw_key, default_value)
|
||||
|
||||
update_starpilot_toggles()
|
||||
HARDWARE.reboot()
|
||||
return jsonify({"success": True, "message": "Toggles reset to default StarPilot values. Rebooting..."})
|
||||
|
||||
@app.route("/mapbox-help/<path:filename>", methods=["GET"])
|
||||
def serve_mapbox_help(filename):
|
||||
|
||||
Reference in New Issue
Block a user