diff --git a/common/libcommon.a b/common/libcommon.a index 501db2771..170540b5a 100644 Binary files a/common/libcommon.a and b/common/libcommon.a differ diff --git a/common/params_keys.h b/common/params_keys.h index debfc0938..9bc6ef464 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -128,6 +128,12 @@ inline static std::unordered_map keys = { {"RecordAudioFeedback", {PERSISTENT, BOOL, "0"}}, {"RecordFront", {PERSISTENT, BOOL}}, {"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet + {"SentryModeEnabled", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}}, + {"SentryModeCapture", {CLEAR_ON_MANAGER_START, BOOL}}, + {"SentryModeLastEvent", {PERSISTENT, JSON, "{}", "{}"}}, + {"SentryModeNtfyUrl", {PERSISTENT, STRING}}, + {"SentryModeStatus", {CLEAR_ON_MANAGER_START | DONT_LOG, JSON}}, + {"SentryModeWebhook", {PERSISTENT, STRING}}, {"SecOCKey", {PERSISTENT | DONT_LOG, STRING}}, {"ShowDebugInfo", {PERSISTENT, BOOL}}, {"ShowAllToggles", {PERSISTENT, BOOL, "0", "0", 3}}, diff --git a/common/params_pyx.so b/common/params_pyx.so index 32b58d246..3df86bc4e 100755 Binary files a/common/params_pyx.so and b/common/params_pyx.so differ diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 770c0ce3a..2f04e899c 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -404,6 +404,8 @@ class LatControlTorque(LatControl): get_ioniq_6_directional_taper_scale(setpoint, desired_lateral_jerk, CS.vEgo)) ff *= get_ioniq_6_ff_scale(setpoint, desired_lateral_jerk, CS.vEgo, directional_taper_scale=ioniq_6_directional_taper) * ioniq_6_center_taper + if not self.is_ioniq_6_2025: + ff *= get_ioniq_6_2023_unwind_ff_scale(setpoint, measurement, desired_lateral_jerk, CS.vEgo) friction_threshold = get_ioniq_6_friction_threshold(CS.vEgo, setpoint, desired_lateral_jerk) / max(ioniq_6_center_taper, 1e-3) friction_scale = get_ioniq_6_friction_scale(CS.vEgo, setpoint, desired_lateral_jerk) friction_scale = 1.0 + ((friction_scale - 1.0) * ioniq_6_center_taper) diff --git a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py index def8ca945..2bb83b635 100644 --- a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py +++ b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py @@ -836,6 +836,15 @@ IONIQ_6_CURVY_TURN_IN_TRIM_LAT_START = 1.0 IONIQ_6_CURVY_TURN_IN_TRIM_LAT_END = 2.5 IONIQ_6_CURVY_TURN_IN_TRIM_LAT_ONSET_WIDTH = 0.18 IONIQ_6_CURVY_TURN_IN_TRIM_LAT_CUTOFF_WIDTH = 0.30 +IONIQ_6_2023_UNWIND_FF_REDUCTION_MAX = 0.18 +IONIQ_6_2023_UNWIND_FF_OVERSHOOT = 0.15 +IONIQ_6_2023_UNWIND_FF_OVERSHOOT_WIDTH = 0.18 +IONIQ_6_2023_UNWIND_FF_JERK = 0.10 +IONIQ_6_2023_UNWIND_FF_JERK_WIDTH = 0.10 +IONIQ_6_2023_UNWIND_FF_SPEED_ONSET = 8.0 +IONIQ_6_2023_UNWIND_FF_SPEED_ONSET_WIDTH = 2.5 +IONIQ_6_2023_UNWIND_FF_SPEED_CUTOFF = 23.5 +IONIQ_6_2023_UNWIND_FF_SPEED_CUTOFF_WIDTH = 2.0 IONIQ_6_LOW_SPEED_PID_RESET_SPEED = 0.1 * CV.MPH_TO_MS # Friction compensation near zero lateral accel amplifies planner jerk noise into a slow # (~0.5 Hz) weave on straights: the 0.09/0.39 small-signal slope plus the jerk feed acts as @@ -3070,6 +3079,29 @@ def get_ioniq_6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo (extra_scale * turn_in_boost * max(unwind_taper, 0.0))) * directional_taper_scale +def get_ioniq_6_2023_unwind_ff_scale(setpoint: float, measured_lateral_accel: float, + desired_lateral_jerk: float, v_ego: float) -> float: + """Trim residual curve feedforward when the 2023 car is already over-rotated.""" + if setpoint * desired_lateral_jerk >= 0.0 or setpoint * measured_lateral_accel <= 0.0: + return 1.0 + + overshoot = max(abs(measured_lateral_accel) - abs(setpoint), 0.0) + if overshoot <= 0.0: + return 1.0 + + overshoot_weight = _ioniq_6_sigmoid((overshoot - IONIQ_6_2023_UNWIND_FF_OVERSHOOT) / + IONIQ_6_2023_UNWIND_FF_OVERSHOOT_WIDTH) + jerk_weight = _ioniq_6_sigmoid((abs(desired_lateral_jerk) - IONIQ_6_2023_UNWIND_FF_JERK) / + IONIQ_6_2023_UNWIND_FF_JERK_WIDTH) + speed_onset = _ioniq_6_sigmoid((v_ego - IONIQ_6_2023_UNWIND_FF_SPEED_ONSET) / + IONIQ_6_2023_UNWIND_FF_SPEED_ONSET_WIDTH) + speed_cutoff = _ioniq_6_sigmoid((IONIQ_6_2023_UNWIND_FF_SPEED_CUTOFF - v_ego) / + IONIQ_6_2023_UNWIND_FF_SPEED_CUTOFF_WIDTH) + reduction = (IONIQ_6_2023_UNWIND_FF_REDUCTION_MAX * overshoot_weight * jerk_weight * + speed_onset * speed_cutoff) + return 1.0 - reduction + + def get_ioniq_6_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0, desired_lateral_jerk: float = 0.0) -> float: base_threshold = max(get_hkg_canfd_base_friction_threshold(v_ego), IONIQ_6_BASE_FRICTION_THRESHOLD) transition_envelope = _ioniq_6_transition_envelope(v_ego, desired_lateral_accel, desired_lateral_jerk) diff --git a/selfdrive/controls/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py index d327a3af0..e19eee148 100644 --- a/selfdrive/controls/tests/test_latcontrol.py +++ b/selfdrive/controls/tests/test_latcontrol.py @@ -116,6 +116,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import ( get_ioniq_6_directional_taper_scale, get_ioniq_6_output_taper_scale, get_ioniq_6_ff_scale, + get_ioniq_6_2023_unwind_ff_scale, get_ioniq_6_friction_center_fade_scale, get_ioniq_6_friction_scale, get_ioniq_6_friction_threshold, @@ -1149,6 +1150,12 @@ class TestLatControl: assert get_ioniq_6_ff_scale(0.30, 0.60, 6.0) > get_ioniq_6_ff_scale(0.30, 0.60, 12.0) assert get_ioniq_6_ff_scale(0.30, -0.60, 3.0) < get_ioniq_6_ff_scale(0.30, 0.60, 3.0) + def test_ioniq_6_2023_unwind_ff_scale_only_trims_measured_overshoot(self): + assert get_ioniq_6_2023_unwind_ff_scale(-1.2, -2.0, 1.0, 18.0) < 1.0 + assert get_ioniq_6_2023_unwind_ff_scale(-1.2, -1.0, 1.0, 18.0) == 1.0 + assert get_ioniq_6_2023_unwind_ff_scale(-1.2, -2.0, -1.0, 18.0) == 1.0 + assert get_ioniq_6_2023_unwind_ff_scale(-1.2, -2.0, 1.0, 30.0) > get_ioniq_6_2023_unwind_ff_scale(-1.2, -2.0, 1.0, 18.0) + def test_ioniq_6_low_speed_angle_assist_curve(self): base = 0.05 boosted = get_ioniq_6_low_speed_angle_assist_torque(22.0, 0.0, base, 1.0) diff --git a/starpilot/system/the_galaxy/assets/components/router.js b/starpilot/system/the_galaxy/assets/components/router.js index a876c7ba0..f01314895 100644 --- a/starpilot/system/the_galaxy/assets/components/router.js +++ b/starpilot/system/the_galaxy/assets/components/router.js @@ -26,6 +26,7 @@ import { ToggleControl } from "/assets/components/tools/toggles.js" import { VASMAnnotations } from "/assets/components/tools/v_asm.js" import { PipSideCamera } from "/assets/components/tools/pip_sidecam.js" import { UpdateManager } from "/assets/components/tools/update_manager.js" +import { startSentryNotifications } from "/assets/components/sentry_notifications.js" let router, routerState @@ -187,3 +188,5 @@ if (document.readyState === "loading") { } else { mountRouterWhenReady() } + +startSentryNotifications() diff --git a/starpilot/system/the_galaxy/assets/components/sentry_notifications.js b/starpilot/system/the_galaxy/assets/components/sentry_notifications.js new file mode 100644 index 000000000..19bddef4c --- /dev/null +++ b/starpilot/system/the_galaxy/assets/components/sentry_notifications.js @@ -0,0 +1,57 @@ +const STORAGE_KEY = "starpilot.sentry.last-event" +const POLL_INTERVAL_MS = 5000 + +let started = false +let initialized = false + +function lastSeenEventId() { + try { + return window.localStorage.getItem(STORAGE_KEY) || "" + } catch { + return "" + } +} + +function rememberEvent(eventId) { + try { + window.localStorage.setItem(STORAGE_KEY, eventId) + } catch { + } +} + +async function pollSentryEvent() { + try { + const response = await fetch("/api/sentry/status", { cache: "no-store" }) + if (!response.ok) return + const payload = await response.json() + const event = payload?.lastEvent + const eventId = String(event?.eventId || "") + if (!eventId) return + + const previous = lastSeenEventId() + rememberEvent(eventId) + if (!initialized || eventId === previous) return + if (typeof Notification === "undefined" || Notification.permission !== "granted") return + + new Notification("StarPilot Sentry Mode", { + body: String(event.message || "Movement detected while parked."), + tag: `starpilot-sentry-${eventId}`, + }) + } catch (error) { + console.debug("Sentry notification poll failed:", error) + } finally { + initialized = true + } +} + +export async function requestSentryNotificationPermission() { + if (typeof Notification === "undefined") return "unsupported" + return Notification.requestPermission() +} + +export function startSentryNotifications() { + if (started) return + started = true + pollSentryEvent() + window.setInterval(pollSentryEvent, POLL_INTERVAL_MS) +} diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js index 555dc7eb1..1306c7b54 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js @@ -272,6 +272,11 @@ function syncInputs() { el.value = resolveColorInputValue(param) } + for (const el of document.querySelectorAll("input.ds-text-input[id^='ds-']")) { + if (document.activeElement === el) continue + el.value = toSelectValue(state.values[el.id.slice(3)]) + } + // Sync selects — hydrate options + set value for (const el of document.querySelectorAll("select.ds-select[id^='ds-']")) { const key = el.id.slice(3) @@ -1475,6 +1480,7 @@ function renderSettingRow(p) { const isNumeric = p.ui_type === "numeric" const isSlider = isNumeric && p.control === "slider" + const isText = p.ui_type === "text" const isColor = p.ui_type === "color" const isAction = p.ui_type === "action" const isGroup = isGroupParam(p) @@ -1600,6 +1606,17 @@ function renderSettingRow(p) { ` + } else if (isText) { + rowControl = html` + + ` } else if (p.ui_type === "color") { rowControl = html`
diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json b/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json index de847635f..9d47c5bcb 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json @@ -13,6 +13,40 @@ } ] }, + { + "name": "Sentry Mode", + "icon": "bi-shield-exclamation", + "params": [ + { + "key": "SentryModeEnabled", + "label": "Sentry Mode", + "description": "Detect movement while parked and send alerts through Galaxy.", + "data_type": "bool", + "ui_type": "toggle", + "settings_tier": "simple" + }, + { + "key": "SentryModeWebhook", + "label": "Sentry Webhook", + "description": "Optional webhook that Galaxy will POST to when sentry mode detects movement. Discord webhooks are supported.", + "data_type": "string", + "ui_type": "text", + "input_type": "url", + "placeholder": "https://…", + "settings_tier": "simple" + }, + { + "key": "SentryModeNtfyUrl", + "label": "Sentry ntfy URL", + "description": "Optional ntfy topic URL, for example https://ntfy.sh/my-starpilot-topic.", + "data_type": "string", + "ui_type": "text", + "input_type": "url", + "placeholder": "https://ntfy.sh/…", + "settings_tier": "simple" + } + ] + }, { "name": "Lateral (Steering)", "icon": "bi-arrows-move", diff --git a/starpilot/system/the_galaxy/assets/components/tools/galaxy.js b/starpilot/system/the_galaxy/assets/components/tools/galaxy.js index e6c53c84b..674d49fcf 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/galaxy.js +++ b/starpilot/system/the_galaxy/assets/components/tools/galaxy.js @@ -1,6 +1,7 @@ import { html, reactive } from "/assets/vendor/arrow-core.js" import { isGalaxyTunnel } from "/assets/js/utils.js" import { Modal } from "/assets/components/modal.js" +import { requestSentryNotificationPermission } from "/assets/components/sentry_notifications.js" const state = reactive({ paired: false, @@ -114,6 +115,15 @@ export function GalaxyPairing() { ${state.url} +