mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-01 21:53:42 +08:00
Compare commits
1 Commits
OBDysseyDuo
...
Dom
| Author | SHA1 | Date | |
|---|---|---|---|
| 401319431a |
+3
-2
@@ -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> |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> |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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|None|||
|
||||
|Toyota|Matrix Retrofit 2005|Custom retrofit|Stock|19 mph|0 mph|[](##)|[](##)|<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),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user