mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-21 08:14:00 +08:00
Sentry and Tunes
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) {
|
||||
<option value="">Loading...</option>
|
||||
</select>
|
||||
`
|
||||
} else if (isText) {
|
||||
rowControl = html`
|
||||
<input
|
||||
type="${p.input_type || "text"}"
|
||||
class="ds-manual-input ds-text-input"
|
||||
id="ds-${p.key}"
|
||||
value="${() => toSelectValue(state.values[p.key])}"
|
||||
placeholder="${p.placeholder || ""}"
|
||||
disabled="${() => isLocked()}"
|
||||
@change="${() => updateParam(p.key, "text")}" />
|
||||
`
|
||||
} else if (p.ui_type === "color") {
|
||||
rowControl = html`
|
||||
<div style="display:flex; align-items:center; gap:0.75rem;">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() {
|
||||
<a class="galaxy-url" href="${state.url}" target="_blank" rel="noopener">
|
||||
${state.url}
|
||||
</a>
|
||||
<button
|
||||
class="galaxy-button"
|
||||
@click="${async () => {
|
||||
const permission = await requestSentryNotificationPermission()
|
||||
showSnackbar(permission === "granted" ? "Browser sentry notifications enabled." : "Browser notifications were not enabled.")
|
||||
}}"
|
||||
>
|
||||
Enable browser alerts
|
||||
</button>
|
||||
<button
|
||||
class="galaxy-button galaxy-button-danger"
|
||||
@click="${() => { state.showUnpairModal = true }}"
|
||||
|
||||
@@ -39,6 +39,7 @@ from opendbc.car.toyota.values import ToyotaStarPilotFlags
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import ParamKeyFlag, ParamKeyType, Params
|
||||
from openpilot.common.realtime import DT_HW
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
@@ -501,6 +502,85 @@ def _build_default_params():
|
||||
|
||||
starpilot_default_params = _build_default_params()
|
||||
|
||||
|
||||
def _sentry_event_roots() -> tuple[Path, ...]:
|
||||
roots = [Path("/data/media/0/sentryd")]
|
||||
if PC:
|
||||
roots.insert(0, Path(Paths.comma_home()) / "starpilot" / "data" / "sentryd")
|
||||
return tuple(root.resolve() for root in roots)
|
||||
|
||||
|
||||
def _safe_sentry_image_paths(raw_paths) -> list[str]:
|
||||
if not isinstance(raw_paths, list):
|
||||
return []
|
||||
|
||||
roots = _sentry_event_roots()
|
||||
safe_paths = []
|
||||
for raw_path in raw_paths:
|
||||
try:
|
||||
path = Path(str(raw_path)).resolve()
|
||||
if path.is_file() and any(path.is_relative_to(root) for root in roots):
|
||||
safe_paths.append(str(path))
|
||||
except (OSError, TypeError, ValueError):
|
||||
continue
|
||||
return safe_paths
|
||||
|
||||
|
||||
def _normalize_sentry_event(payload) -> dict | None:
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
|
||||
event_id = str(payload.get("eventId") or "").strip()
|
||||
kind = str(payload.get("kind") or "").strip().lower()
|
||||
if not event_id or kind not in {"warning", "alarm"}:
|
||||
return None
|
||||
|
||||
return {
|
||||
"eventId": event_id[:96],
|
||||
"kind": kind,
|
||||
"detectedAt": str(payload.get("detectedAt") or ""),
|
||||
"message": str(payload.get("message") or "Movement detected while parked.")[:500],
|
||||
"imagePaths": _safe_sentry_image_paths(payload.get("imagePaths")),
|
||||
}
|
||||
|
||||
|
||||
def _dispatch_sentry_event(event: dict) -> None:
|
||||
message = f"🚨 StarPilot Sentry Mode: {event['message']}"
|
||||
webhook = (params.get("SentryModeWebhook", encoding="utf-8") or "").strip()
|
||||
if webhook:
|
||||
files = []
|
||||
handles = []
|
||||
try:
|
||||
for image_path in event.get("imagePaths", []):
|
||||
handle = open(image_path, "rb")
|
||||
handles.append(handle)
|
||||
files.append(("file", (Path(image_path).name, handle, "image/jpeg")))
|
||||
|
||||
body = {"content": message, "event": json.dumps(event, separators=(",", ":"))}
|
||||
response = requests.post(webhook, data=body, files=files or None, timeout=10)
|
||||
response.raise_for_status()
|
||||
except Exception:
|
||||
cloudlog.exception("Galaxy: sentry webhook notification failed")
|
||||
finally:
|
||||
for handle in handles:
|
||||
try:
|
||||
handle.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
ntfy_url = (params.get("SentryModeNtfyUrl", encoding="utf-8") or "").strip()
|
||||
if ntfy_url:
|
||||
try:
|
||||
response = requests.post(
|
||||
ntfy_url,
|
||||
data=message.encode("utf-8"),
|
||||
headers={"Title": "StarPilot Sentry Mode", "Priority": "urgent", "Tags": "warning,car"},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except Exception:
|
||||
cloudlog.exception("Galaxy: ntfy notification failed")
|
||||
|
||||
TOGGLE_BACKUP_FORMAT = "starpilot-toggle-backup"
|
||||
TOGGLE_BACKUP_VERSION = 1
|
||||
TOGGLE_BACKUP_MAX_ENCODED_BYTES = 2_000_000
|
||||
@@ -4059,11 +4139,13 @@ def setup(app):
|
||||
def disable_device_settings_asset_cache(response):
|
||||
if request.path in {
|
||||
"/assets/components/router.js",
|
||||
"/assets/components/sentry_notifications.js",
|
||||
"/assets/components/home/home.js",
|
||||
"/assets/components/home/home.css",
|
||||
"/assets/components/tools/device_settings.js",
|
||||
"/assets/components/tools/device_settings.css",
|
||||
"/assets/components/tools/device_settings_layout.json",
|
||||
"/assets/components/tools/galaxy.js",
|
||||
"/assets/components/tools/v_asm.js",
|
||||
"/assets/components/tools/v_asm.css",
|
||||
"/assets/components/tools/pip_sidecam.js",
|
||||
@@ -6594,6 +6676,38 @@ def setup(app):
|
||||
"warning": "This wipes local params, backups, themes, models, maps, and route data.",
|
||||
}), 202
|
||||
|
||||
@app.route("/api/sentry/status", methods=["GET"])
|
||||
def sentry_status():
|
||||
raw_event = params.get("SentryModeLastEvent", encoding="utf-8") or "{}"
|
||||
raw_status = params.get("SentryModeStatus", encoding="utf-8") or "{}"
|
||||
try:
|
||||
last_event = json.loads(raw_event)
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
last_event = {}
|
||||
try:
|
||||
status = json.loads(raw_status)
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
status = {}
|
||||
|
||||
return jsonify({
|
||||
"enabled": params.get_bool("SentryModeEnabled"),
|
||||
"status": status if isinstance(status, dict) else {},
|
||||
"lastEvent": last_event if isinstance(last_event, dict) else {},
|
||||
})
|
||||
|
||||
@app.route("/api/sentry/events", methods=["POST"])
|
||||
def sentry_event():
|
||||
if request.remote_addr not in {None, "127.0.0.1", "::1"}:
|
||||
return jsonify({"error": "Sentry events must originate on the device."}), 403
|
||||
|
||||
event = _normalize_sentry_event(request.get_json(silent=True))
|
||||
if event is None:
|
||||
return jsonify({"error": "Invalid sentry event."}), 400
|
||||
|
||||
params.put("SentryModeLastEvent", json.dumps(event, separators=(",", ":")))
|
||||
threading.Thread(target=_dispatch_sentry_event, args=(event,), name="galaxy-sentry-notify", daemon=True).start()
|
||||
return jsonify({"accepted": True, "eventId": event["eventId"]}), 202
|
||||
|
||||
# ── Galaxy pairing (mirrors settings.cc L262-282) ──────────────────
|
||||
GALAXY_DIR = _get_galaxy_dir()
|
||||
GALAXY_AUTH_FILE = GALAXY_DIR / "glxyauth"
|
||||
|
||||
Reference in New Issue
Block a user