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|[](##)|[](##)|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|[](##)|[](##)|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|[](##)|[](##)|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|[](##)|[](##)|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|[](##)|[](##)|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|[](##)|[](##)|None|||
|Toyota|Matrix Retrofit 2005|Custom retrofit|Stock|19 mph|0 mph|[](##)|[](##)|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`
+
+
+
${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 @@