From 401319431a4993c7d8f69178e9140a51e63d9782 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:10:43 -0500 Subject: [PATCH] hi --- docs/CARS.md | 5 +- .../opendbc/car/hyundai/fingerprints.py | 5 + .../opendbc/car/hyundai/hyundaican.py | 3 + .../opendbc/car/hyundai/tests/test_hyundai.py | 21 ++++- opendbc_repo/opendbc/car/hyundai/values.py | 5 + opendbc_repo/opendbc/car/tests/routes.py | 1 + .../opendbc/car/torque_data/override.toml | 1 + starpilot/system/bluetooth/bluez.py | 22 +++-- starpilot/system/bluetooth/daemon.py | 13 ++- starpilot/system/bluetooth/protocol.py | 8 +- .../system/bluetooth/tests/test_bluetooth.py | 15 +++ .../the_galaxy/assets/components/router.js | 2 +- .../assets/components/tools/bluetooth.css | 60 ++++++++++++ .../assets/components/tools/bluetooth.js | 93 +++++++++++++------ .../system/the_galaxy/templates/index.html | 4 +- .../tests/test_frontend_module_graph.py | 2 +- 16 files changed, 210 insertions(+), 50 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 25cbaf957f..86ab8289e3 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,7 +4,7 @@ A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 452 Supported Cars +# 453 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -465,7 +465,7 @@ A supported vehicle is one that just works when you install a comma device. All These additional vehicle ports are maintained by StarPilot rather than upstream openpilot. -# 16 Community Cars +# 17 Community Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -478,6 +478,7 @@ These additional vehicle ports are maintained by StarPilot rather than upstream |Kia|Ceed Plug-in Hybrid Non-SCC 2022|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai I connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Forte Non-SCC 2019|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Forte Non-SCC 2021|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Kia|Ray EV 2025|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Seltos Non-SCC 2023-24|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Tesla|Model S (Pre-AP) 2012-14|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None||| |Toyota|Matrix Retrofit 2005|Custom retrofit|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| diff --git a/opendbc_repo/opendbc/car/hyundai/fingerprints.py b/opendbc_repo/opendbc/car/hyundai/fingerprints.py index 71320a3866..023d084c09 100644 --- a/opendbc_repo/opendbc/car/hyundai/fingerprints.py +++ b/opendbc_repo/opendbc/car/hyundai/fingerprints.py @@ -1696,4 +1696,9 @@ FW_VERSIONS = { b'\xf1\x00BC3 LKA AT EUR LHD 1.00 1.01 99211-Q0100 261', ], }, + CAR.KIA_RAY_EV: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00TAM MFC AT KOR LHD 1.00 1.02 99211-E2000 230901', + ], + }, } diff --git a/opendbc_repo/opendbc/car/hyundai/hyundaican.py b/opendbc_repo/opendbc/car/hyundai/hyundaican.py index c439711fad..22f63dd5af 100644 --- a/opendbc_repo/opendbc/car/hyundai/hyundaican.py +++ b/opendbc_repo/opendbc/car/hyundai/hyundaican.py @@ -80,6 +80,9 @@ def create_lkas11(packer, frame, CP, apply_torque, steer_req, # Genesis and Optima fault when forwarding while engaged values["CF_Lkas_LdwsActivemode"] = 2 + if CP.carFingerprint == CAR.KIA_RAY_EV: + values["CF_Lkas_LdwsOpt_USM"] = 0 + dat = packer.make_can_msg("LKAS11", 0, values)[1] if CP.flags & HyundaiFlags.CHECKSUM_CRC8: diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index 30b60141a1..3f87e9b225 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -79,6 +79,7 @@ HYUNDAI_NON_SCC_CARS = ( CAR.HYUNDAI_ELANTRA_HEV_2022_NON_SCC, CAR.HYUNDAI_KONA_NON_SCC, CAR.HYUNDAI_KONA_EV_NON_SCC, + CAR.KIA_RAY_EV, CAR.KIA_CEED_PHEV_2022_NON_SCC, CAR.KIA_FORTE_2019_NON_SCC, CAR.KIA_FORTE_2021_NON_SCC, @@ -709,6 +710,19 @@ class TestHyundaiFingerprint: assert parser.vl["LKAS11"]["CF_Lkas_LdwsActivemode"] == 0 assert parser.vl["LKAS11"]["CF_Lkas_FcwOpt_USM"] == 0 + def test_kia_ray_ev_preserves_stock_lkas_option(self): + CP = CarInterface.get_params(CAR.KIA_RAY_EV, gen_empty_fingerprint(), [], False, False, False, None) + packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) + parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS11", 0)], 0) + + msg = hyundaican.create_lkas11( + packer, 0, CP, 0, True, False, parser.vl["LKAS11"], False, 4, False, + True, True, 0, 0, 2, + ) + parser.update([(1, [msg])]) + + assert parser.vl["LKAS11"]["CF_Lkas_LdwsOpt_USM"] == 0 + @pytest.mark.parametrize("candidate", (CAR.HYUNDAI_ELANTRA_2024, CAR.HYUNDAI_ELANTRA_HEV_2024)) def test_hyundai_can_refresh_platforms_use_refresh_dbc_and_safety_param(self, candidate): CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], False, False, False, None) @@ -821,6 +835,7 @@ class TestHyundaiFingerprint: (CAR.HYUNDAI_ELANTRA_2022_NON_SCC, ("EMS16", "LVR12"), ()), (CAR.HYUNDAI_ELANTRA_HEV_2022_NON_SCC, ("E_CRUISE_CONTROL", "ELECT_GEAR"), ("EMS16",)), (CAR.HYUNDAI_KONA_EV_NON_SCC, ("LABEL11", "EMS12", "E_EMS11"), ()), + (CAR.KIA_RAY_EV, ("LABEL11", "EMS12", "E_EMS11"), ()), ]) def test_non_scc_cruise_message_selection(self, candidate, expected_msgs, unexpected_msgs): toggles = get_test_toggles() @@ -3511,7 +3526,9 @@ class TestHyundaiFingerprint: def test_platform_code_ecus_available(self, subtests): # TODO: add queries for these non-CAN FD cars to get EPS no_eps_platforms = CANFD_CAR | {CAR.KIA_SORENTO, CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.KIA_OPTIMA_H, - CAR.KIA_OPTIMA_H_G4_FL, CAR.HYUNDAI_SONATA_LF, CAR.HYUNDAI_TUCSON, CAR.GENESIS_G90, CAR.GENESIS_G80, CAR.HYUNDAI_ELANTRA} + CAR.KIA_OPTIMA_H_G4_FL, CAR.HYUNDAI_SONATA_LF, CAR.HYUNDAI_TUCSON, CAR.GENESIS_G90, CAR.GENESIS_G80, + CAR.HYUNDAI_ELANTRA, CAR.KIA_RAY_EV} + no_fwd_radar_platforms = {CAR.KIA_RAY_EV} # Asserts ECU keys essential for fuzzy fingerprinting are available on all platforms for car_model, ecus in FW_VERSIONS.items(): @@ -3519,6 +3536,8 @@ class TestHyundaiFingerprint: for platform_code_ecu in PLATFORM_CODE_ECUS: if platform_code_ecu in (Ecu.fwdRadar, Ecu.eps) and car_model == CAR.HYUNDAI_GENESIS: continue + if platform_code_ecu == Ecu.fwdRadar and car_model in no_fwd_radar_platforms: + continue if platform_code_ecu == Ecu.eps and car_model in no_eps_platforms: continue assert platform_code_ecu in [e[0] for e in ecus] diff --git a/opendbc_repo/opendbc/car/hyundai/values.py b/opendbc_repo/opendbc/car/hyundai/values.py index 46320a59ad..c46768ecaa 100644 --- a/opendbc_repo/opendbc/car/hyundai/values.py +++ b/opendbc_repo/opendbc/car/hyundai/values.py @@ -945,6 +945,11 @@ class CAR(Platforms): HYUNDAI_KONA_EV.specs, flags=HyundaiFlags.EV | HyundaiFlags.ALT_LIMITS, ) + KIA_RAY_EV = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Kia Ray EV 2025", car_parts=CarParts.common([CarHarness.hyundai_h]))], + CarSpecs(mass=1295, wheelbase=2.52, steerRatio=14.5), + flags=HyundaiFlags.EV | HyundaiFlags.CHECKSUM_CRC8, + ) KIA_CEED_PHEV_2022_NON_SCC = HyundaiNonSccPlatformConfig( [HyundaiNonSccCarDocs("Kia Ceed Plug-in Hybrid Non-SCC 2022", car_parts=CarParts.common([CarHarness.hyundai_i]))], CarSpecs(mass=1650, wheelbase=2.65, steerRatio=13.75, tireStiffnessFactor=0.5), diff --git a/opendbc_repo/opendbc/car/tests/routes.py b/opendbc_repo/opendbc/car/tests/routes.py index a6edb488c5..387b7c75dd 100644 --- a/opendbc_repo/opendbc/car/tests/routes.py +++ b/opendbc_repo/opendbc/car/tests/routes.py @@ -76,6 +76,7 @@ non_tested_cars = [ HYUNDAI.HYUNDAI_ELANTRA_HEV_2024, HYUNDAI.HYUNDAI_KONA_EV_NON_SCC, HYUNDAI.HYUNDAI_KONA_NON_SCC, + HYUNDAI.KIA_RAY_EV, HYUNDAI.HYUNDAI_PALISADE_2023, HYUNDAI.KIA_CEED_PHEV_2022_NON_SCC, HYUNDAI.KIA_FORTE_2019_NON_SCC, diff --git a/opendbc_repo/opendbc/car/torque_data/override.toml b/opendbc_repo/opendbc/car/torque_data/override.toml index 599ada1ef2..e9bffa1908 100644 --- a/opendbc_repo/opendbc/car/torque_data/override.toml +++ b/opendbc_repo/opendbc/car/torque_data/override.toml @@ -106,6 +106,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "KIA_CARNIVAL_4TH_GEN" = [1.75, 1.75, 0.15] "KIA_CARNIVAL_2025" = [1.75, 1.75, 0.15] "KIA_CARNIVAL_HEV_4TH_GEN" = [1.75, 1.75, 0.15] +"KIA_RAY_EV" = [1.8, 2.0, 0.15] "GMC_ACADIA" = [1.6, 1.6, 0.2] "LEXUS_IS_TSS2" = [2.0, 2.0, 0.1] "HYUNDAI_KONA_EV_2ND_GEN" = [2.5, 2.5, 0.1] diff --git a/starpilot/system/bluetooth/bluez.py b/starpilot/system/bluetooth/bluez.py index f897beb09b..e0692b422a 100644 --- a/starpilot/system/bluetooth/bluez.py +++ b/starpilot/system/bluetooth/bluez.py @@ -107,7 +107,11 @@ class BlueZClient: return reply.body def _register_agent(self) -> None: - self._call("/org/bluez", AGENT_MANAGER_IFACE, "RegisterAgent", "os", (AGENT_PATH, "KeyboardDisplay")) + try: + self._call("/org/bluez", AGENT_MANAGER_IFACE, "RegisterAgent", "os", (AGENT_PATH, "KeyboardDisplay")) + except RuntimeError as error: + if "alreadyexists" not in str(error).replace(" ", "").lower(): + raise self._call("/org/bluez", AGENT_MANAGER_IFACE, "RequestDefaultAgent", "o", (AGENT_PATH,)) def _agent_loop(self) -> None: @@ -163,7 +167,8 @@ class BlueZClient: return path, interfaces[ADAPTER_IFACE] raise RuntimeError("Bluetooth adapter is not available") - def devices(self, objects: dict[str, Any] | None = None) -> list[dict[str, Any]]: + def devices(self, objects: dict[str, Any] | None = None, include_hidden: bool = False, + include_discovering: bool = False) -> list[dict[str, Any]]: objects = self.managed_objects() if objects is None else objects devices = [] for path, interfaces in objects.items(): @@ -185,8 +190,8 @@ class BlueZClient: "audio": audio, "controller": controller, } - if show_pairing_device(device["address"], device["name"], device["paired"], device["trusted"], device["connected"], - device["blocked"], audio, controller): + if include_hidden or show_pairing_device(device["address"], device["name"], device["paired"], device["trusted"], device["connected"], + device["blocked"], audio, controller, include_discovering): devices.append(device) return sorted(devices, key=lambda device: (not device["connected"], not device["paired"], -(device["rssi"] or -127), device["name"].lower())) @@ -196,7 +201,7 @@ class BlueZClient: return { "powered": bool(adapter.get("Powered", False)), "discovering": bool(adapter.get("Discovering", False)), - "devices": self.devices(objects), + "devices": self.devices(objects, include_discovering=bool(adapter.get("Discovering", False))), "prompt": self.agent.prompt, } @@ -218,7 +223,7 @@ class BlueZClient: def device_for_address(self, address: str) -> dict[str, Any]: normalized = address.upper() - for device in self.devices(): + for device in self.devices(self.managed_objects(), include_hidden=True): if device["address"].upper() == normalized: return device raise RuntimeError(f"Bluetooth device {address} was not found") @@ -230,8 +235,9 @@ class BlueZClient: if reply.header.message_type == MessageType.error: raise RuntimeError(str(reply.body[0] if reply.body else f"Unable to set {name}")) - def pair(self, address: str) -> None: - device = self.device_for_address(address) + def pair(self, address: str, device_path: str | None = None) -> None: + self._register_agent() + device = {"path": device_path} if device_path else self.device_for_address(address) self._call(device["path"], DEVICE_IFACE, "Pair", timeout=90.0) self.set_device_property(address, "Trusted", "b", True) self.agent.clear() diff --git a/starpilot/system/bluetooth/daemon.py b/starpilot/system/bluetooth/daemon.py index 6ea8d5ae6f..daa8de3a0a 100644 --- a/starpilot/system/bluetooth/daemon.py +++ b/starpilot/system/bluetooth/daemon.py @@ -107,9 +107,9 @@ class BluetoothController: if command in OFFROAD_COMMANDS and not self._offroad(): raise RuntimeError("Bluetooth settings can only be changed offroad") - def _pair_worker(self, address: str) -> None: + def _pair_worker(self, address: str, device_path: str | None = None) -> None: try: - self._client().pair(address) + self._client().pair(address, device_path) status = self._client().device_for_address(address) if status.get("audio") and not self.params.get("BluetoothAudioAddress", encoding="utf-8"): self.params.put("BluetoothAudioAddress", address) @@ -172,6 +172,7 @@ class BluetoothController: elif command == "start_scan": if not self.params.get_bool("BluetoothEnabled"): raise RuntimeError("Enable Bluetooth before scanning") + self._pairing_error = "" self._client().start_discovery() self._scan_deadline = time.monotonic() + SCAN_DURATION elif command == "stop_scan": @@ -180,9 +181,15 @@ class BluetoothController: elif command == "pair": if self._pairing_address: raise RuntimeError("Another Bluetooth device is already pairing") + device = self._client().device_for_address(address) + try: + self._client().stop_discovery() + except Exception as error: + cloudlog.warning(f"Bluetooth discovery stop before pairing failed: {error}") + self._scan_deadline = 0.0 self._pairing_address = address self._pairing_error = "" - threading.Thread(target=self._pair_worker, args=(address,), daemon=True).start() + threading.Thread(target=self._pair_worker, args=(address, device["path"]), daemon=True).start() elif command == "connect": self._client().connect(address) elif command == "disconnect": diff --git a/starpilot/system/bluetooth/protocol.py b/starpilot/system/bluetooth/protocol.py index a7ce359f99..1594bba06c 100644 --- a/starpilot/system/bluetooth/protocol.py +++ b/starpilot/system/bluetooth/protocol.py @@ -95,10 +95,12 @@ def device_capabilities(uuids: list[str] | tuple[str, ...], bluetooth_class: int def show_pairing_device(address: str, name: str, paired: bool, trusted: bool, connected: bool, blocked: bool, - audio: bool, controller: bool) -> bool: + audio: bool, controller: bool, discovering: bool = False) -> bool: known = paired or trusted or connected - named = bool(name) and name not in {address, "Unknown device"} - return known or (named and not blocked and (audio or controller)) + normalized_address = "".join(character for character in address.upper() if character.isalnum()) + normalized_name = "".join(character for character in name.upper() if character.isalnum()) + named = bool(name) and name != "Unknown device" and normalized_name != normalized_address + return known or (named and not blocked and (audio or controller or discovering)) class _DesktopFakeBluetooth: diff --git a/starpilot/system/bluetooth/tests/test_bluetooth.py b/starpilot/system/bluetooth/tests/test_bluetooth.py index 0e61937e7d..0654cf5525 100644 --- a/starpilot/system/bluetooth/tests/test_bluetooth.py +++ b/starpilot/system/bluetooth/tests/test_bluetooth.py @@ -75,6 +75,7 @@ class FakeBlueZ: def stop_discovery(self): self.discovering = False + self.actions.append(("stop_scan", "")) def device_for_address(self, _address): return dict(self.device) @@ -393,6 +394,20 @@ def test_scan_stops_after_timeout(): assert not client.discovering and controller._scan_deadline == 0.0 +def test_pair_stops_discovery_before_starting_pair(): + params = FakeParams(IsOffroad=True, BluetoothEnabled=True) + client = FakeBlueZ() + controller = BluetoothController(params, lambda: client, FakeRadio()) + controller.handle({"command": "start_scan"}) + controller.handle({"command": "pair", "address": client.device["address"]}) + + deadline = time.monotonic() + 1.0 + while not any(action[0] == "pair" for action in client.actions) and time.monotonic() < deadline: + time.sleep(0.01) + + assert client.actions.index(("stop_scan", "")) < client.actions.index(("pair", client.device["address"])) + + def test_audio_queue_is_nonblocking_and_falls_back(): params = FakeParams(BluetoothEnabled=True, BluetoothAudioAddress="00:11:22:33:44:55") process = FakeProcess() diff --git a/starpilot/system/the_galaxy/assets/components/router.js b/starpilot/system/the_galaxy/assets/components/router.js index fdb2a693b5..f3506778bb 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-6" +import { Bluetooth } from "/assets/components/tools/bluetooth.js?v=bluetooth-9" 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.css b/starpilot/system/the_galaxy/assets/components/tools/bluetooth.css index 37a02e6ae7..8b2722f151 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/bluetooth.css +++ b/starpilot/system/the_galaxy/assets/components/tools/bluetooth.css @@ -69,6 +69,66 @@ color: #ffe2a3; } +.bluetoothPromptHeader, +.bluetoothPromptActions { + align-items: center; + display: flex; + gap: 10px; +} + +.bluetoothPromptHeader { + color: #ffe2a3; +} + +.bluetoothPromptHeader i { + font-size: 1.15rem; +} + +.bluetoothPrompt p { + margin: 8px 0 0; +} + +.bluetoothPromptPasskey { + font-size: 1.05rem; +} + +.bluetoothPromptInput { + background: var(--input-bg); + border: 1px solid var(--sidebar-border-color); + border-radius: var(--border-radius-md); + color: var(--text-color); + font: inherit; + margin-top: 12px; + max-width: 260px; + padding: 10px 12px; +} + +.bluetoothPromptActions { + justify-content: flex-end; + margin-top: 14px; +} + +.bluetoothPromptActions button { + background: linear-gradient(135deg, #765bb6, #9474ce); + border: 0; + border-radius: var(--border-radius-md); + color: #fff; + cursor: pointer; + font-weight: 700; + padding: 9px 14px; +} + +.bluetoothPromptActions button.bluetoothPromptReject { + background: var(--input-bg); + border: 1px solid var(--sidebar-border-color); + color: var(--text-color); +} + +.bluetoothPromptActions button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + .bluetoothError { color: #ff9ab3; } diff --git a/starpilot/system/the_galaxy/assets/components/tools/bluetooth.js b/starpilot/system/the_galaxy/assets/components/tools/bluetooth.js index afc55f94c3..0b8554706d 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/bluetooth.js +++ b/starpilot/system/the_galaxy/assets/components/tools/bluetooth.js @@ -20,7 +20,6 @@ const state = reactive({ }) let initialized = false -let lastPromptId = "" let audioTestTimer = null let pollTimer = null let refreshRequested = false @@ -29,7 +28,7 @@ let refreshPromise = null function bluetoothPageActive() { const currentPath = window.location.pathname.replace(/\/+$/, "") const bluetoothPath = galaxyPath("/bluetooth").replace(/\/+$/, "") - return currentPath === bluetoothPath + return document.querySelector(".bluetoothPage") !== null || currentPath === bluetoothPath } function pollDelay() { @@ -96,26 +95,6 @@ async function request(operation, body = {}) { } } -async function handlePrompt(prompt) { - if (!prompt || prompt.id === lastPromptId) return - lastPromptId = prompt.id - if (prompt.display_only) return - - let accepted = true - let value = "" - if (prompt.kind === "pin") { - value = window.prompt(`Enter the PIN for ${prompt.name || "Bluetooth device"}`) ?? "" - accepted = value.length > 0 - } else if (prompt.kind === "passkey") { - value = window.prompt(`Enter the passkey for ${prompt.name || "Bluetooth device"}`) ?? "" - accepted = /^\d{1,6}$/.test(value) - } else { - const suffix = prompt.value ? `\n\nPasskey: ${prompt.value}` : "" - accepted = window.confirm(`Allow ${prompt.name || "Bluetooth device"} to pair?${suffix}`) - } - await request("pairing_response", { prompt_id: prompt.id, accepted, value }) -} - async function refreshOnce() { const statusUrl = `${galaxyPath("/api/bluetooth/status")}?_=${Date.now()}` const response = await fetch(statusUrl, { cache: "no-store" }) @@ -130,7 +109,61 @@ async function refreshOnce() { state.devices = Array.isArray(payload.devices) ? payload.devices : [] state.prompt = payload.prompt || null state.error = payload.error || (response.ok ? "" : "Bluetooth service unavailable") - handlePrompt(state.prompt) +} + +function pairingPromptNeedsValue() { + return state.prompt && (state.prompt.kind === "pin" || state.prompt.kind === "passkey") +} + +function respondToPairingPrompt(accepted) { + const prompt = state.prompt + if (!prompt || state.busy === "pairing_response") return + const input = document.getElementById("bluetoothPairingValue") + const value = input?.value.trim() || "" + if (accepted && prompt.kind === "pin" && !value) { + state.error = "Enter the PIN to continue pairing." + input?.focus() + return + } + if (accepted && prompt.kind === "passkey" && !/^\d{1,6}$/.test(value)) { + state.error = "Enter the numeric passkey to continue pairing." + input?.focus() + return + } + request("pairing_response", { prompt_id: prompt.id, accepted, value }) +} + +function pairingPrompt() { + const prompt = state.prompt + if (!prompt) return "" + const name = prompt.name || "Bluetooth device" + const valuePrompt = pairingPromptNeedsValue() + const message = prompt.kind === "confirmation" + ? "Confirm the pairing request on this device." + : prompt.kind === "authorization" + ? "Allow this device to connect?" + : prompt.kind === "pin" + ? "Enter the PIN supplied by the device." + : prompt.kind === "passkey" + ? "Enter the device passkey." + : "Follow the instructions on the device." + return html` +
+
Bluetooth pairing request
+

${name} — ${message}

+ ${prompt.value ? html`

Passkey: ${prompt.value}

` : ""} + ${() => valuePrompt ? html` + + ` : ""} + ${prompt.display_only ? "" : html` +
+ + +
+ `} +
+ ` } async function refresh() { @@ -190,6 +223,7 @@ function deviceStatus(device) { if (isPairing(device)) return "Pairing…" if (device.connected) { const audioSelected = state.selectedAudio.toUpperCase() === normalizedAddress(device) + if (!device.paired) return audioSelected ? "Connected · Ready to pair · Audio output" : "Connected · Ready to pair" return audioSelected ? "Connected · Audio output" : "Connected" } return device.paired ? "Saved" : "Ready to pair" @@ -208,14 +242,17 @@ function deviceActions(device) { const pairing = () => isPairing(device) return html`
- ${!device.paired && !device.connected ? html` + ${!device.paired ? html` - ` : html` + ` : ""} + ${device.paired || device.connected ? html` + ` : ""} + ${device.paired || device.connected ? html` ${device.audio ? html` ` : ""} - `} + ` : ""}
` } @@ -292,9 +329,7 @@ export function Bluetooth() { ${() => !state.offroad ? html`
Scanning, pairing, and forgetting devices are available offroad only.
` : ""} ${() => state.error ? html`
${state.error}
` : ""} - ${() => state.prompt?.display_only ? html` -
${state.prompt.name || "Bluetooth device"}: ${state.prompt.value}
- ` : ""} + ${pairingPrompt} ${() => state.audioTestLabel ? html`
${state.audioTestLabel} diff --git a/starpilot/system/the_galaxy/templates/index.html b/starpilot/system/the_galaxy/templates/index.html index b26a3a6cca..30a1dd2b16 100644 --- a/starpilot/system/the_galaxy/templates/index.html +++ b/starpilot/system/the_galaxy/templates/index.html @@ -40,7 +40,7 @@ - + @@ -50,7 +50,7 @@