Compare commits

...

1 Commits

Author SHA1 Message Date
firestar5683 401319431a hi 2026-08-31 16:10:43 -05:00
16 changed files with 210 additions and 50 deletions
+3 -2
View File
@@ -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|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|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|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|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)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai I connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Ceed Plug-in Hybrid Non-SCC 2022">Buy Here</a></sub></details>|||
|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)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Forte Non-SCC 2019">Buy Here</a></sub></details>|||
|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)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Forte Non-SCC 2021">Buy Here</a></sub></details>|||
|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)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Ray EV 2025">Buy Here</a></sub></details>|||
|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)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Seltos Non-SCC 2023-24">Buy Here</a></sub></details>|||
|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)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Matrix Retrofit 2005">Buy Here</a></sub></details>|||
@@ -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',
],
},
}
@@ -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:
@@ -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]
@@ -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),
+1
View File
@@ -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,
@@ -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]
+14 -8
View File
@@ -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()
+10 -3
View File
@@ -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":
+5 -3
View File
@@ -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:
@@ -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()
@@ -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"
@@ -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;
}
@@ -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`
<div class="bluetoothPrompt">
<div class="bluetoothPromptHeader"><i class="bi bi-shield-check" aria-hidden="true"></i><strong>Bluetooth pairing request</strong></div>
<p><strong>${name}</strong> ${message}</p>
${prompt.value ? html`<p class="bluetoothPromptPasskey">Passkey: <strong>${prompt.value}</strong></p>` : ""}
${() => valuePrompt ? html`
<input id="bluetoothPairingValue" class="bluetoothPromptInput" inputmode="numeric" autocomplete="one-time-code"
placeholder="${() => prompt.kind === "pin" ? "PIN" : "Passkey"}" disabled="${() => state.busy === "pairing_response"}" />
` : ""}
${prompt.display_only ? "" : html`
<div class="bluetoothPromptActions">
<button class="bluetoothPromptReject" disabled="${() => state.busy === "pairing_response"}" @click="${() => respondToPairingPrompt(false)}">Cancel</button>
<button disabled="${() => state.busy === "pairing_response"}" @click="${() => respondToPairingPrompt(true)}">Allow</button>
</div>
`}
</div>
`
}
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`
<div class="bluetoothActions">
${!device.paired && !device.connected ? html`
${!device.paired ? html`
<button disabled="${() => !state.offroad || !!state.busy || pairing()}" @click="${() => request("pair", { address: device.address })}">
${() => pairing() ? "Pairing…" : "Pair"}
</button>
` : html`
` : ""}
${device.paired || device.connected ? html`
<button disabled="${() => !!state.busy}" @click="${() => request(device.connected ? "disconnect" : "connect", { address: device.address })}">
${device.connected ? "Disconnect" : "Connect"}
</button>
` : ""}
${device.paired || device.connected ? html`
${device.audio ? html`
<button class="${() => audioSelected() ? "selected" : ""}"
disabled="${() => !!state.busy}" @click="${() => request("select_audio", { address: audioSelected() ? "" : device.address })}">
@@ -231,7 +268,7 @@ function deviceActions(device) {
disabled="${() => !state.offroad || !!state.busy}" @click="${() => {
if (window.confirm(`Forget ${device.name}?`)) request("forget", { address: device.address })
}}"><i class="bi bi-trash3" aria-hidden="true"></i></button>` : ""}
`}
` : ""}
</div>
`
}
@@ -292,9 +329,7 @@ export function Bluetooth() {
${() => !state.offroad ? html`<div class="bluetoothNotice">Scanning, pairing, and forgetting devices are available offroad only.</div>` : ""}
${() => state.error ? html`<div class="bluetoothError">${state.error}</div>` : ""}
${() => state.prompt?.display_only ? html`
<div class="bluetoothPrompt">${state.prompt.name || "Bluetooth device"}: ${state.prompt.value}</div>
` : ""}
${pairingPrompt}
${() => state.audioTestLabel ? html`
<div class="bluetoothAudioCountdown">
<strong>${state.audioTestLabel}</strong>
@@ -40,7 +40,7 @@
<link rel="stylesheet" href="/assets/components/tools/toggles.css">
<link rel="stylesheet" href="/assets/components/tools/update_manager.css">
<link rel="stylesheet" href="/assets/components/tools/device_settings.css?v=favorite-actions-1">
<link rel="stylesheet" href="/assets/components/tools/bluetooth.css?v=bluetooth-4">
<link rel="stylesheet" href="/assets/components/tools/bluetooth.css?v=bluetooth-6">
<link rel="stylesheet" href="/assets/components/tools/wheel_controls.css?v=controllers-2">
<link rel="stylesheet" href="/assets/components/tools/galaxy.css">
<link rel="stylesheet" href="/assets/components/tools/sentry.css">
@@ -50,7 +50,7 @@
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
<script type="module">
import("/assets/components/router.js?v=router-cycle-fix-3").catch((err) => {
import("/assets/components/router.js?v=router-cycle-fix-4").catch((err) => {
console.error("[the_galaxy] bootstrap failed", err);
const target = document.getElementById("app") || document.body;
const pre = document.createElement("pre");
@@ -44,7 +44,7 @@ def test_bluetooth_actions_use_reactive_disabled_bindings():
assert "state.pairingAddress" in source
assert "state.busy !== \"power\"" in source
assert "Turning Bluetooth" in source
assert "!device.paired && !device.connected" in source
assert "!device.paired" in source
assert 'galaxyPath("/bluetooth")' in source
assert 'window.location.pathname === "/bluetooth"' not in source
assert "schedulePoll(250)" in source