diff --git a/starpilot/system/the_galaxy/assets/components/router.js b/starpilot/system/the_galaxy/assets/components/router.js index dc2950798..bae0db76b 100644 --- a/starpilot/system/the_galaxy/assets/components/router.js +++ b/starpilot/system/the_galaxy/assets/components/router.js @@ -2,7 +2,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=favorite-c4-hint-1" -import { Bluetooth } from "/assets/components/tools/bluetooth.js?v=bluetooth-13" +import { Bluetooth } from "/assets/components/tools/bluetooth.js?v=bluetooth-live-15" import { WheelControls } from "/assets/components/tools/wheel_controls.js?v=controllers-2" import { ErrorLogs } from "/assets/components/tools/error_logs.js" import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js" diff --git a/starpilot/system/the_galaxy/assets/components/tools/bluetooth.js b/starpilot/system/the_galaxy/assets/components/tools/bluetooth.js index 95a9aa1d5..d588df721 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/bluetooth.js +++ b/starpilot/system/the_galaxy/assets/components/tools/bluetooth.js @@ -1,5 +1,5 @@ import { html, reactive } from "/assets/vendor/arrow-core.js" -import { galaxyPath } from "/assets/js/utils.js" +import { escapeHtml, galaxyPath } from "/assets/js/utils.js" const state = reactive({ loading: true, @@ -14,6 +14,9 @@ const state = reactive({ pairingAddress: "", devices: [], revision: 0, + deviceSignature: "", + refreshing: false, + lastUpdated: 0, prompt: null, audioTestAddress: "", audioTestLabel: "", @@ -25,6 +28,8 @@ let audioTestTimer = null let pollTimer = null let refreshRequested = false let refreshPromise = null +const POLL_INTERVAL_MS = 750 +const ACTIVE_POLL_INTERVAL_MS = 250 function bluetoothPageActive() { const currentPath = window.location.pathname.replace(/\/+$/, "") @@ -32,11 +37,22 @@ function bluetoothPageActive() { return document.querySelector(".bluetoothPage") !== null || currentPath === bluetoothPath } -function schedulePoll() { - if (pollTimer !== null) return - pollTimer = setInterval(() => { - if (bluetoothPageActive() && state.busy !== "power") refresh() - }, 750) +function pollDelay() { + return state.busy || state.discovering || state.pairingAddress ? ACTIVE_POLL_INTERVAL_MS : POLL_INTERVAL_MS +} + +function schedulePoll(delay = pollDelay()) { + if (pollTimer !== null) clearTimeout(pollTimer) + pollTimer = setTimeout(async () => { + pollTimer = null + try { + if (bluetoothPageActive() && document.visibilityState !== "hidden" && state.busy !== "power") { + await refresh() + } + } finally { + schedulePoll() + } + }, delay) } function startAudioTestCountdown(address, delayMs, requestStartedAt) { @@ -101,8 +117,20 @@ async function refreshOnce() { state.offroad = !!payload.offroad state.selectedAudio = String(payload.selected_audio || "") state.pairingAddress = String(payload.pairing_address || "") - state.devices = Array.isArray(payload.devices) ? payload.devices : [] - state.revision++ + const devices = Array.isArray(payload.devices) ? payload.devices : [] + const deviceSignature = JSON.stringify({ + enabled: state.enabled, + discovering: state.discovering, + selectedAudio: state.selectedAudio, + pairingAddress: state.pairingAddress, + devices, + }) + state.devices = devices + if (state.deviceSignature !== deviceSignature) { + state.deviceSignature = deviceSignature + state.revision++ + } + state.lastUpdated = Date.now() state.prompt = payload.prompt || null state.error = payload.error || (response.ok ? "" : "Bluetooth service unavailable") } @@ -166,6 +194,7 @@ async function refresh() { refreshRequested = true if (refreshPromise !== null) return refreshPromise + state.refreshing = true refreshPromise = (async () => { while (refreshRequested) { refreshRequested = false @@ -184,6 +213,7 @@ async function refresh() { await refreshPromise } finally { refreshPromise = null + state.refreshing = false } } @@ -191,11 +221,12 @@ function initialize() { if (initialized) return initialized = true window.addEventListener("focus", refresh) + window.addEventListener("pageshow", refresh) document.addEventListener("visibilitychange", () => { - if (!document.hidden && bluetoothPageActive()) refresh() + if (document.visibilityState !== "hidden" && bluetoothPageActive()) refresh() }) refresh() - schedulePoll() + schedulePoll(0) } function normalizedAddress(device) { @@ -291,9 +322,10 @@ function deviceRow(device) { ` } -function deviceSection(title, icon, devices, emptyText = "") { +function deviceSection(title, icon, devices, emptyText = "", revision = null) { + const revisionAttribute = revision === null ? "" : ` data-revision="${revision}"` return html` -
+

${title}

${devices.length} @@ -307,6 +339,77 @@ function deviceSection(title, icon, devices, emptyText = "") { ` } +function renderDisabledAttribute(disabled) { + return disabled ? " disabled" : "" +} + +function renderDeviceActions(device) { + const selected = state.selectedAudio.toUpperCase() === device.address.toUpperCase() + const pairing = isPairing(device) + const address = escapeHtml(device.address) + const name = escapeHtml(device.name) + const actions = [] + if (!device.paired) { + actions.push("") + } + if (device.paired || device.connected) { + const operation = device.connected ? "disconnect" : "connect" + actions.push("") + if (device.audio) { + actions.push("") + if (device.connected) { + const testing = state.audioTestAddress.toUpperCase() === device.address.toUpperCase() && !!state.audioTestLabel + actions.push("") + } + } + if (device.paired) { + actions.push("") + } + } + return "
" + actions.join("") + "
" +} + +function renderDeviceRow(device) { + const name = escapeHtml(device.name) + return "
" + + "
" + + "

" + name + + "

" + (device.connected ? "" : "") + + "

" + deviceCapabilities(device) + "

" + + escapeHtml(deviceStatus(device)) + "
" + renderDeviceActions(device) + "
" +} + +function renderDeviceSection(title, icon, devices, emptyText = "", revision = null) { + const revisionAttribute = revision === null ? "" : " data-revision=\"" + revision + "\"" + const rows = devices.length + ? devices.map(renderDeviceRow).join("") + : "
" + escapeHtml(emptyText) + "
" + return html(["
" + + "

" + title + + "

" + devices.length + "
" + rows + + "
"]) +} + +function handleDeviceListClick(event) { + const target = event.target + const button = target && typeof target.closest === "function" + ? target.closest("button[data-bluetooth-operation]") + : null + if (!button) return + const operation = button.dataset.bluetoothOperation + const address = button.dataset.address || "" + if (operation === "forget" && !window.confirm("Forget " + (button.dataset.deviceName || "this device") + "?")) return + request(operation, { address }) +} + export function Bluetooth() { initialize() return html` @@ -348,10 +451,11 @@ export function Bluetooth() { + ${() => state.busy ? "Updating…" : state.lastUpdated ? "Live" : ""} Put a device in pairing mode before searching.
-
+
${() => state.loading ? html`
` : ""} ${() => !state.loading && !state.enabled ? html`
@@ -360,14 +464,12 @@ export function Bluetooth() {

Turn it on to reconnect saved devices or find something new.

` : ""} - ${() => { - if (state.loading || !state.enabled) return "" - void state.revision - return html` - ${deviceSection("My Devices", "bi-check2-circle", knownDevices(), "No saved devices yet.")} - ${deviceSection("Available Devices", "bi-radar", availableDevices(), state.discovering ? "Searching for nearby devices…" : "No nearby devices found. Start a search to try again.")} - ` - }} + ${() => !state.loading && state.enabled + ? renderDeviceSection("My Devices", "bi-check2-circle", knownDevices(), "No saved devices yet.", state.revision) + : ""} + ${() => !state.loading && state.enabled + ? renderDeviceSection("Available Devices", "bi-radar", availableDevices(), state.discovering ? "Searching for nearby devices…" : "No nearby devices found. Start a search to try again.") + : ""}
` diff --git a/starpilot/system/the_galaxy/templates/index.html b/starpilot/system/the_galaxy/templates/index.html index 2c363c229..f2717549b 100644 --- a/starpilot/system/the_galaxy/templates/index.html +++ b/starpilot/system/the_galaxy/templates/index.html @@ -50,7 +50,7 @@