mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-30 20:53:42 +08:00
bingo
This commit is contained in:
@@ -588,14 +588,14 @@ KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.18
|
||||
KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_JERK = 0.65
|
||||
KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_JERK_WIDTH = 0.25
|
||||
KIA_CARNIVAL_UNWIND_FF_REDUCTION_MAX = 0.45
|
||||
KIA_CARNIVAL_UNWIND_FF_SPEED = 15.0
|
||||
KIA_CARNIVAL_UNWIND_FF_SPEED = 9.0
|
||||
KIA_CARNIVAL_UNWIND_FF_SPEED_WIDTH = 2.0
|
||||
KIA_CARNIVAL_UNWIND_FF_SPEED_CUTOFF = 23.0
|
||||
KIA_CARNIVAL_UNWIND_FF_SPEED_CUTOFF_WIDTH = 2.0
|
||||
KIA_CARNIVAL_UNWIND_FF_OVERSHOOT = 0.20
|
||||
KIA_CARNIVAL_UNWIND_FF_OVERSHOOT_WIDTH = 0.12
|
||||
KIA_CARNIVAL_UNWIND_FF_JERK = 0.65
|
||||
KIA_CARNIVAL_UNWIND_FF_JERK_WIDTH = 0.25
|
||||
KIA_CARNIVAL_UNWIND_FF_OVERSHOOT = 0.08
|
||||
KIA_CARNIVAL_UNWIND_FF_OVERSHOOT_WIDTH = 0.06
|
||||
KIA_CARNIVAL_UNWIND_FF_JERK = 0.45
|
||||
KIA_CARNIVAL_UNWIND_FF_JERK_WIDTH = 0.20
|
||||
|
||||
TUCSON_4TH_GEN_CENTER_TAPER_MAX = 0.44
|
||||
TUCSON_4TH_GEN_CENTER_TAPER_LAT = 0.28
|
||||
|
||||
@@ -26,6 +26,7 @@ from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import (
|
||||
get_honda_accord_stop_go_accel_cap,
|
||||
get_honda_accord_stop_go_accel_rise_rate,
|
||||
get_toyota_rav4_tss2_lead_departure_tune,
|
||||
get_toyota_rav4_tss2_lead_creep_tune,
|
||||
get_force_stop_distance_bias,
|
||||
get_force_stop_handoff_distance,
|
||||
get_stop_sign_low_speed_hold,
|
||||
@@ -1323,11 +1324,17 @@ class LongitudinalPlanner:
|
||||
lead_gap = float(getattr(lead, "dRel", 0.0))
|
||||
lead_speed = max(float(getattr(lead, "vLead", 0.0)), 0.0)
|
||||
lead_accel = float(getattr(lead, "aLeadK", 0.0))
|
||||
creep_tune = get_toyota_rav4_tss2_lead_creep_tune(self.CP)
|
||||
min_lead_speed, min_lead_accel = (
|
||||
(STANDSTILL_LEAD_CREEP_RELEASE_MIN_LEAD_SPEED,
|
||||
STANDSTILL_LEAD_CREEP_RELEASE_MIN_LEAD_ACCEL)
|
||||
if creep_tune is None else creep_tune
|
||||
)
|
||||
return bool(
|
||||
float(v_ego) <= STANDSTILL_LEAD_DEPART_MAX_EGO_SPEED and
|
||||
lead_gap >= standstill_nudge_gap + STANDSTILL_LEAD_CREEP_RELEASE_MIN_GAP_MARGIN and
|
||||
lead_speed >= STANDSTILL_LEAD_CREEP_RELEASE_MIN_LEAD_SPEED and
|
||||
lead_accel >= STANDSTILL_LEAD_CREEP_RELEASE_MIN_LEAD_ACCEL
|
||||
lead_speed >= min_lead_speed and
|
||||
lead_accel >= min_lead_accel
|
||||
)
|
||||
|
||||
def get_safe_depart_release_hold_lead(self, v_ego):
|
||||
|
||||
@@ -22,7 +22,7 @@ HONDA_ACCORD_STOP_GO_ACCEL_RISE_RATE = 4.0
|
||||
HYUNDAI_ELANTRA_LEAD_FOLLOW_JERK_SCALE = 1.25
|
||||
GENESIS_GV70_ELECTRIFIED_LEAD_FOLLOW_JERK_SCALE = 1.35
|
||||
FORD_LIGHTNING_LEAD_FOLLOW_JERK_SCALE = 1.35
|
||||
HONDA_CRV_5G_LEAD_FOLLOW_JERK_SCALE = 1.20
|
||||
HONDA_CRV_5G_LEAD_FOLLOW_JERK_SCALE = 1.35
|
||||
GM_SILVERADO_EARLY_FOLLOW_MIN_EGO_SPEED = 18.0
|
||||
GM_SILVERADO_EARLY_FOLLOW_MAX_DISTANCE = 130.0
|
||||
GM_SILVERADO_EARLY_FOLLOW_MIN_MODEL_PROB = 0.85
|
||||
@@ -73,6 +73,8 @@ TOYOTA_RAV4_TSS2_FAR_FOLLOW_BRAKE_SLEW_RATE = 2.5
|
||||
TOYOTA_RAV4_TSS2_FAR_FOLLOW_RELEASE_SLEW_RATE = 1.75
|
||||
TOYOTA_RAV4_TSS2_LEAD_DEPART_ACCEL_HOLD_MAX_ACCEL = 0.70
|
||||
TOYOTA_RAV4_TSS2_LEAD_DEPART_ACCEL_ASSIST = 0.20
|
||||
TOYOTA_RAV4_TSS2_LEAD_CREEP_MIN_LEAD_SPEED = 0.15
|
||||
TOYOTA_RAV4_TSS2_LEAD_CREEP_MIN_LEAD_ACCEL = -0.05
|
||||
TOYOTA_PRIUS_STOPPED_LEAD_OBSTACLE_BIAS_M = 1.5
|
||||
TOYOTA_PRIUS_STOPPED_LEAD_MAX_EGO_SPEED = 22.0
|
||||
TOYOTA_PRIUS_STOPPED_LEAD_MAX_SPEED = 1.0
|
||||
@@ -356,6 +358,16 @@ def get_toyota_rav4_tss2_lead_departure_tune(CP):
|
||||
return None
|
||||
|
||||
|
||||
def get_toyota_rav4_tss2_lead_creep_tune(CP):
|
||||
"""Allow a credible RAV4 vision lead to release the standstill hold early."""
|
||||
if is_toyota_rav4_tss2_post_departure_tune(CP):
|
||||
return (
|
||||
TOYOTA_RAV4_TSS2_LEAD_CREEP_MIN_LEAD_SPEED,
|
||||
TOYOTA_RAV4_TSS2_LEAD_CREEP_MIN_LEAD_ACCEL,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def get_toyota_rav4_tss2_early_lead_cap(CP, lead, v_ego, accel_min):
|
||||
"""Start a mild RAV4 coast/brake response before a hard lead approach."""
|
||||
if (
|
||||
|
||||
@@ -772,6 +772,9 @@ class TestLatControl:
|
||||
assert overshooting_unwind < 0.70
|
||||
assert highway_overshoot > overshooting_unwind
|
||||
|
||||
low_speed_exit = get_kia_carnival_unwind_ff_scale(0.31, 0.43, -0.88, 11.0)
|
||||
assert low_speed_exit < 0.90
|
||||
|
||||
def test_genesis_g90_ff_scale_curve(self):
|
||||
assert get_genesis_g90_ff_scale(0.0, 0.0, 20.0) == 1.0
|
||||
assert get_genesis_g90_ff_scale(0.5, 0.0, 20.0) > get_genesis_g90_ff_scale(-0.5, 0.0, 20.0)
|
||||
|
||||
@@ -48,6 +48,7 @@ from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import (
|
||||
get_tracked_lead_catchup_speed_range,
|
||||
get_toyota_prius_stopped_lead_obstacle_bias,
|
||||
get_toyota_rav4_tss2_lead_departure_tune,
|
||||
get_toyota_rav4_tss2_lead_creep_tune,
|
||||
get_toyota_rav4_tss2_early_lead_cap,
|
||||
get_toyota_sienna_post_departure_restop_cap,
|
||||
is_toyota_rav4_tss2_radar_follow_lead,
|
||||
@@ -3335,6 +3336,26 @@ def test_rav4_tss2_variants_use_the_car_specific_post_departure_tune():
|
||||
assert not is_toyota_rav4_tss2_post_departure_tune(other_cp)
|
||||
|
||||
|
||||
def test_rav4_tss2_lead_creep_tune_is_vehicle_specific():
|
||||
rav4 = ToyotaCarInterface.get_non_essential_params(TOYOTA_CAR.TOYOTA_RAV4_TSS2_2023)
|
||||
other = ToyotaCarInterface.get_non_essential_params(TOYOTA_CAR.TOYOTA_RAV4_TSS2_2022)
|
||||
|
||||
assert get_toyota_rav4_tss2_lead_creep_tune(rav4) == pytest.approx((0.15, -0.05))
|
||||
assert get_toyota_rav4_tss2_lead_creep_tune(other) is None
|
||||
|
||||
|
||||
def test_rav4_tss2_standstill_lead_creep_does_not_wait_for_lead_acceleration():
|
||||
rav4 = ToyotaCarInterface.get_non_essential_params(TOYOTA_CAR.TOYOTA_RAV4_TSS2_2023)
|
||||
civic = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
||||
rav4_planner = LongitudinalPlanner(rav4, init_v=0.0)
|
||||
civic_planner = LongitudinalPlanner(civic, init_v=0.0)
|
||||
lead = make_lead(status=True, d_rel=6.4, v_lead=0.2, a_lead=0.0, model_prob=1.0)
|
||||
gap = longitudinal_planner_module.STOP_DISTANCE - 0.5
|
||||
|
||||
assert rav4_planner.is_slow_creep_lead_depart(lead, 0.0, gap)
|
||||
assert not civic_planner.is_slow_creep_lead_depart(lead, 0.0, gap)
|
||||
|
||||
|
||||
def test_rav4_tss2_early_lead_cap_starts_a_mild_response():
|
||||
CP = ToyotaCarInterface.get_non_essential_params(TOYOTA_CAR.TOYOTA_RAV4_TSS2_2023)
|
||||
lead = make_lead(status=True, d_rel=100.0, v_lead=13.0, a_lead=-1.1, model_prob=0.9)
|
||||
|
||||
@@ -44,7 +44,7 @@ def test_lead_follow_jerk_scale_is_platform_specific():
|
||||
assert get_lead_follow_jerk_scale(SimpleNamespace(brand="hyundai", carFingerprint="HYUNDAI_ELANTRA_2021")) == 1.25
|
||||
assert get_lead_follow_jerk_scale(SimpleNamespace(brand="hyundai", carFingerprint="GENESIS_GV70_ELECTRIFIED_1ST_GEN")) == 1.35
|
||||
assert get_lead_follow_jerk_scale(SimpleNamespace(brand="ford", carFingerprint="FORD_F_150_LIGHTNING_MK1")) == 1.35
|
||||
assert get_lead_follow_jerk_scale(SimpleNamespace(brand="honda", carFingerprint="HONDA_CRV_5G")) == 1.20
|
||||
assert get_lead_follow_jerk_scale(SimpleNamespace(brand="honda", carFingerprint="HONDA_CRV_5G")) == 1.35
|
||||
assert get_lead_follow_jerk_scale(SimpleNamespace(brand="other", carFingerprint="OTHER_CAR")) == 1.0
|
||||
|
||||
|
||||
|
||||
@@ -21,26 +21,54 @@ class BluetoothDeviceButton(BigButton):
|
||||
self._offroad = offroad
|
||||
self._selected_audio = selected_audio
|
||||
self._check_txt = gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 32, 32)
|
||||
self._forget_btn = ForgetButton(lambda: self._manager.forget(self.device.address))
|
||||
self._forget_btn = ForgetButton(self._forget_device)
|
||||
self.update_device(device, selected_audio, offroad)
|
||||
|
||||
def _forget_device(self):
|
||||
self._manager.forget(self.device.address)
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 48
|
||||
|
||||
@property
|
||||
def _show_forget_btn(self):
|
||||
return self.device.paired and self._offroad
|
||||
return self.device.paired and self._offroad and self._manager.operation_for(self.device.address) != "forgetting"
|
||||
|
||||
def update_device(self, device, selected_audio: str, offroad: bool):
|
||||
self.device = device
|
||||
self._selected_audio = selected_audio
|
||||
self._offroad = offroad
|
||||
states = ["connected" if device.connected else "paired" if device.paired else "pair"]
|
||||
if device.audio:
|
||||
states.append("audio selected" if selected_audio.upper() == device.address.upper() else "audio")
|
||||
if device.controller:
|
||||
states.append("controller")
|
||||
self.set_value(" / ".join(states))
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
operation = self._manager.operation_for(self.device.address)
|
||||
pairing = self._manager.status.pairing_address.upper() == self.device.address.upper()
|
||||
audio_selected = self._selected_audio.upper() == self.device.address.upper()
|
||||
|
||||
if operation or pairing:
|
||||
self.set_value(operation or "pairing")
|
||||
self.set_enabled(False)
|
||||
elif self.device.connected:
|
||||
capabilities = []
|
||||
if audio_selected:
|
||||
capabilities.append("audio output")
|
||||
elif self.device.audio:
|
||||
capabilities.append("audio")
|
||||
if self.device.controller:
|
||||
capabilities.append("controller")
|
||||
self.set_value("connected" + (f" / {' / '.join(capabilities)}" if capabilities else ""))
|
||||
self.set_enabled(True)
|
||||
elif self.device.paired:
|
||||
self.set_value("connect")
|
||||
self.set_enabled(True)
|
||||
else:
|
||||
capabilities = []
|
||||
if self.device.audio:
|
||||
capabilities.append("audio")
|
||||
if self.device.controller:
|
||||
capabilities.append("controller")
|
||||
self.set_value("pair" + (f" / {' / '.join(capabilities)}" if capabilities else ""))
|
||||
self.set_enabled(self._offroad)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if self._show_forget_btn and rl.check_collision_point_rec(mouse_pos, self._forget_btn.rect):
|
||||
@@ -117,20 +145,28 @@ class BluetoothLayoutMici(NavScroller):
|
||||
self._scan_btn = BigButton("scan for devices", "scan", self._dialog_icon, scroll=True)
|
||||
self._scan_btn.set_click_callback(lambda: self._manager.set_scanning(True))
|
||||
self._scanning_btn = BluetoothScanningButton()
|
||||
self._device_buttons = {}
|
||||
self._scan_on_ready = False
|
||||
self._scroller.add_widgets([self._power_btn, self._scan_btn, self._scanning_btn])
|
||||
self._rebuild()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._manager.set_active(True)
|
||||
self._scan_on_ready = True
|
||||
gui_app.add_nav_stack_tick(self._tick)
|
||||
|
||||
def hide_event(self):
|
||||
if self._manager.status.discovering and self._manager.status.offroad:
|
||||
self._manager.set_scanning(False)
|
||||
self._manager.set_active(False)
|
||||
gui_app.remove_nav_stack_tick(self._tick)
|
||||
super().hide_event()
|
||||
|
||||
def _toggle_power(self):
|
||||
self._manager.set_power(not self._manager.status.enabled)
|
||||
enabled = not self._manager.status.enabled
|
||||
self._scan_on_ready = enabled
|
||||
self._manager.set_power(enabled)
|
||||
|
||||
def _rebuild(self):
|
||||
status = self._manager.status
|
||||
@@ -139,35 +175,43 @@ class BluetoothLayoutMici(NavScroller):
|
||||
self._scan_btn.set_enabled(status.enabled and status.offroad)
|
||||
items = [self._power_btn]
|
||||
for device in status.devices:
|
||||
button = BluetoothDeviceButton(device, self._manager, self._bluetooth_icon, status.selected_audio, status.offroad)
|
||||
button.set_enabled(status.offroad or device.paired)
|
||||
button.set_click_callback(lambda selected=device: self._device_actions(selected))
|
||||
button = self._device_buttons.get(device.address)
|
||||
if button is None:
|
||||
button = BluetoothDeviceButton(device, self._manager, self._bluetooth_icon, status.selected_audio, status.offroad)
|
||||
button.set_click_callback(lambda address=device.address: self._device_selected(address))
|
||||
self._device_buttons[device.address] = button
|
||||
self._scroller.add_widget(button)
|
||||
else:
|
||||
button.update_device(device, status.selected_audio, status.offroad)
|
||||
items.append(button)
|
||||
if status.enabled:
|
||||
items.append(self._scanning_btn if status.discovering else self._scan_btn)
|
||||
self._scroller.items.clear()
|
||||
self._scroller.add_widgets(items)
|
||||
self._device_buttons = {device.address: self._device_buttons[device.address] for device in status.devices}
|
||||
self._scroller.items[:] = items
|
||||
|
||||
def _device_actions(self, device):
|
||||
def _device_selected(self, address: str):
|
||||
device = next((device for device in self._manager.status.devices if device.address == address), None)
|
||||
if device is None:
|
||||
return
|
||||
if not device.paired:
|
||||
self._manager.pair(device.address)
|
||||
return
|
||||
elif not device.connected:
|
||||
self._manager.connect(device.address)
|
||||
else:
|
||||
self._device_actions(device)
|
||||
|
||||
options = ["disconnect" if device.connected else "connect"]
|
||||
def _device_actions(self, device):
|
||||
options = ["disconnect"]
|
||||
if device.audio:
|
||||
selected = self._manager.status.selected_audio.upper() == device.address.upper()
|
||||
options.append("stop using for audio" if selected else "use for audio")
|
||||
if device.connected and self._manager.status.offroad:
|
||||
options.append("test audio")
|
||||
if self._manager.status.offroad:
|
||||
options.append("forget")
|
||||
dialog_holder = {}
|
||||
|
||||
def apply():
|
||||
action = dialog_holder["dialog"].get_selected_option()
|
||||
if action == "connect":
|
||||
self._manager.connect(device.address)
|
||||
elif action == "disconnect":
|
||||
if action == "disconnect":
|
||||
self._manager.disconnect(device.address)
|
||||
elif action == "use for audio":
|
||||
self._manager.select_audio(device.address)
|
||||
@@ -176,8 +220,6 @@ class BluetoothLayoutMici(NavScroller):
|
||||
elif action == "test audio":
|
||||
self._manager.test_audio(device.address)
|
||||
gui_app.push_widget(BluetoothAudioTestDialog(self._manager, self._dialog_icon))
|
||||
elif action == "forget":
|
||||
self._manager.forget(device.address)
|
||||
|
||||
dialog = BigMultiOptionDialog(options=options, default=options[0], right_btn_callback=apply)
|
||||
dialog_holder["dialog"] = dialog
|
||||
@@ -217,12 +259,18 @@ class BluetoothLayoutMici(NavScroller):
|
||||
status.discovering,
|
||||
status.offroad,
|
||||
status.selected_audio,
|
||||
status.pairing_address,
|
||||
tuple((device.address, device.name, device.paired, device.connected, device.audio, device.controller) for device in status.devices),
|
||||
)
|
||||
if signature != self._last_signature:
|
||||
self._last_signature = signature
|
||||
self._rebuild()
|
||||
if self._scan_on_ready and status.available and status.enabled:
|
||||
self._scan_on_ready = False
|
||||
if status.offroad and not status.discovering:
|
||||
self._manager.set_scanning(True)
|
||||
error = self._manager.consume_error()
|
||||
if error:
|
||||
self._scan_on_ready = False
|
||||
gui_app.push_widget(BigDialog("Bluetooth", error))
|
||||
self._handle_prompt()
|
||||
|
||||
@@ -63,6 +63,7 @@ class BluetoothStatus:
|
||||
discovering: bool = False
|
||||
offroad: bool = False
|
||||
selected_audio: str = ""
|
||||
pairing_address: str = ""
|
||||
devices: tuple[BluetoothDevice, ...] = ()
|
||||
prompt: dict[str, Any] | None = None
|
||||
error: str = ""
|
||||
@@ -76,6 +77,7 @@ class BluetoothStatus:
|
||||
discovering=bool(value.get("discovering", False)),
|
||||
offroad=bool(value.get("offroad", False)),
|
||||
selected_audio=str(value.get("selected_audio", "")),
|
||||
pairing_address=str(value.get("pairing_address", "")),
|
||||
devices=tuple(BluetoothDevice.from_dict(device) for device in value.get("devices", ())),
|
||||
prompt=value.get("prompt"),
|
||||
error=str(value.get("error", "")),
|
||||
|
||||
@@ -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-3"
|
||||
import { Bluetooth } from "/assets/components/tools/bluetooth.js?v=bluetooth-4"
|
||||
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"
|
||||
|
||||
@@ -1,55 +1,67 @@
|
||||
.bluetoothPage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 18px;
|
||||
margin: 0 auto;
|
||||
max-width: var(--width-xxxl);
|
||||
padding: var(--padding-base) var(--padding-lg) var(--padding-xxl);
|
||||
}
|
||||
|
||||
.bluetoothHeader,
|
||||
.bluetoothTitle,
|
||||
.bluetoothDeviceHeader,
|
||||
.bluetoothToolbar,
|
||||
.bluetoothActions,
|
||||
.bluetoothBadges {
|
||||
display: flex;
|
||||
.bluetoothDeviceName,
|
||||
.bluetoothSectionHeader,
|
||||
.bluetoothSectionHeader > div {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bluetoothHeader,
|
||||
.bluetoothDeviceHeader {
|
||||
.bluetoothHeader {
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.bluetoothTitle {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.bluetoothTitle > i {
|
||||
color: #a98ce5;
|
||||
font-size: 2.5rem;
|
||||
align-items: center;
|
||||
background: rgba(139, 108, 197, 0.16);
|
||||
border: 1px solid rgba(169, 140, 229, 0.34);
|
||||
border-radius: 16px;
|
||||
color: #b99bec;
|
||||
display: flex;
|
||||
font-size: 2rem;
|
||||
height: 58px;
|
||||
justify-content: center;
|
||||
width: 58px;
|
||||
}
|
||||
|
||||
.bluetoothHeader h2,
|
||||
.bluetoothHeader p,
|
||||
.bluetoothCard h3 {
|
||||
.bluetoothDeviceRow h3,
|
||||
.bluetoothDeviceRow p,
|
||||
.bluetoothEmptyPage h3,
|
||||
.bluetoothEmptyPage p,
|
||||
.bluetoothSectionHeader h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bluetoothHeader p {
|
||||
margin-top: 6px;
|
||||
opacity: 0.8;
|
||||
margin-top: 4px;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.bluetoothCard,
|
||||
.bluetoothNotice,
|
||||
.bluetoothError,
|
||||
.bluetoothPrompt {
|
||||
background: var(--sidebar-bg);
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.bluetoothDeviceList {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.bluetoothNotice,
|
||||
@@ -57,83 +69,329 @@
|
||||
color: #ffe2a3;
|
||||
}
|
||||
|
||||
.bluetoothAudioCountdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 16px 20px;
|
||||
border: 1px solid rgba(64, 201, 124, 0.45);
|
||||
border-radius: var(--border-radius-lg);
|
||||
background: rgba(64, 201, 124, 0.12);
|
||||
}
|
||||
|
||||
.bluetoothAudioCountdown strong {
|
||||
min-width: 72px;
|
||||
color: #b8ffd4;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bluetoothError {
|
||||
color: #ff9ab3;
|
||||
}
|
||||
|
||||
.bluetoothAddress {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
font-family: monospace;
|
||||
opacity: 0.65;
|
||||
.bluetoothAudioCountdown {
|
||||
align-items: center;
|
||||
background: rgba(64, 201, 124, 0.12);
|
||||
border: 1px solid rgba(64, 201, 124, 0.45);
|
||||
border-radius: var(--border-radius-lg);
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.bluetoothBadge {
|
||||
border-radius: 999px;
|
||||
padding: 4px 9px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bluetoothBadgePaired {
|
||||
color: #d9c9ff;
|
||||
background: rgba(139, 108, 197, 0.22);
|
||||
}
|
||||
|
||||
.bluetoothBadgeConnected {
|
||||
.bluetoothAudioCountdown strong {
|
||||
color: #b8ffd4;
|
||||
background: rgba(64, 201, 124, 0.2);
|
||||
font-size: 2rem;
|
||||
min-width: 72px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bluetoothSwitch,
|
||||
.bluetoothToolbar button,
|
||||
.bluetoothActions button,
|
||||
.bluetoothSwitch {
|
||||
.bluetoothActions button {
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #765bb6, #9474ce);
|
||||
border: 0;
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(135deg, #7a62b8, #8b6cc5);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-weight: 700;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
padding: 10px 14px;
|
||||
transition: filter 140ms ease, opacity 140ms ease, transform 140ms ease;
|
||||
}
|
||||
|
||||
.bluetoothSwitch:hover,
|
||||
.bluetoothToolbar button:hover:not(:disabled),
|
||||
.bluetoothActions button:hover:not(:disabled) {
|
||||
filter: brightness(1.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.bluetoothSwitch input {
|
||||
accent-color: #c4a7ff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bluetoothToolbar {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bluetoothToolbar button.scanning i {
|
||||
animation: bluetoothSpin 1s linear infinite;
|
||||
}
|
||||
|
||||
.bluetoothToolbar .bluetoothSecondaryButton {
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.bluetoothScanHint {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.86rem;
|
||||
margin-left: auto;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.bluetoothDeviceList {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bluetoothSection {
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bluetoothSectionHeader {
|
||||
background: var(--input-bg);
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.bluetoothSectionHeader > div {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bluetoothSectionHeader i {
|
||||
color: #b99bec;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.bluetoothSectionHeader h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.bluetoothSectionHeader > span {
|
||||
background: rgba(169, 140, 229, 0.16);
|
||||
border-radius: 999px;
|
||||
color: #d9c9ff;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
min-width: 26px;
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bluetoothSectionBody > .bluetoothDeviceRow + .bluetoothDeviceRow {
|
||||
border-top: 1px solid var(--sidebar-border-color);
|
||||
}
|
||||
|
||||
.bluetoothDeviceRow {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: 48px minmax(180px, 1fr) auto;
|
||||
padding: 15px 16px;
|
||||
transition: background-color 140ms ease;
|
||||
}
|
||||
|
||||
.bluetoothDeviceRow:hover {
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
.bluetoothDeviceRow.connected {
|
||||
box-shadow: inset 3px 0 0 #40c97c;
|
||||
}
|
||||
|
||||
.bluetoothDeviceIcon {
|
||||
align-items: center;
|
||||
background: rgba(169, 140, 229, 0.12);
|
||||
border: 1px solid rgba(169, 140, 229, 0.24);
|
||||
border-radius: 50%;
|
||||
color: #cbb2fa;
|
||||
display: flex;
|
||||
font-size: 1.25rem;
|
||||
height: 44px;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.bluetoothDeviceDetails {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bluetoothDeviceName {
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.bluetoothDeviceName h3 {
|
||||
font-size: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bluetoothConnectedDot {
|
||||
background: #40c97c;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 3px rgba(64, 201, 124, 0.13);
|
||||
flex: 0 0 auto;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.bluetoothDeviceRow p {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 3px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.bluetoothDeviceStatus {
|
||||
color: #cbb2fa;
|
||||
display: inline-block;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.bluetoothActions {
|
||||
margin-top: 14px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bluetoothActions button.selected {
|
||||
background: linear-gradient(135deg, #258b58, #40c97c);
|
||||
}
|
||||
|
||||
.bluetoothActions button.danger {
|
||||
background: linear-gradient(135deg, #b14a6b, #d95a7b);
|
||||
.bluetoothActions .bluetoothIconButton {
|
||||
min-width: 42px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.bluetoothActions .bluetoothForgetButton {
|
||||
background: rgba(217, 90, 123, 0.13);
|
||||
border: 1px solid rgba(217, 90, 123, 0.34);
|
||||
color: #ff9ab3;
|
||||
}
|
||||
|
||||
.bluetoothToolbar button:disabled,
|
||||
.bluetoothActions button:disabled,
|
||||
.bluetoothSwitch:has(input:disabled) {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
opacity: 0.45;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.bluetoothSwitch input {
|
||||
margin: 0;
|
||||
.bluetoothEmptyState,
|
||||
.bluetoothEmptyPage,
|
||||
.bluetoothLoading {
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bluetoothEmptyState {
|
||||
padding: 28px 18px;
|
||||
}
|
||||
|
||||
.bluetoothEmptyPage {
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 46px 20px;
|
||||
}
|
||||
|
||||
.bluetoothEmptyPage > i {
|
||||
color: #b99bec;
|
||||
display: block;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bluetoothEmptyPage p {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.bluetoothLoading {
|
||||
padding: 36px;
|
||||
}
|
||||
|
||||
.bluetoothLoading span {
|
||||
animation: bluetoothBounce 1.1s infinite ease-in-out;
|
||||
background: #b99bec;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
margin: 0 4px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.bluetoothLoading span:nth-child(2) {
|
||||
animation-delay: 0.14s;
|
||||
}
|
||||
|
||||
.bluetoothLoading span:nth-child(3) {
|
||||
animation-delay: 0.28s;
|
||||
}
|
||||
|
||||
@keyframes bluetoothSpin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes bluetoothBounce {
|
||||
0%, 60%, 100% { opacity: 0.35; transform: translateY(0); }
|
||||
30% { opacity: 1; transform: translateY(-7px); }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.bluetoothPage {
|
||||
padding-left: var(--padding-base);
|
||||
padding-right: var(--padding-base);
|
||||
}
|
||||
|
||||
.bluetoothHeader,
|
||||
.bluetoothToolbar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.bluetoothHeader {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bluetoothSwitch {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.bluetoothScanHint {
|
||||
flex-basis: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.bluetoothDeviceRow {
|
||||
grid-template-columns: 44px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.bluetoothActions {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: flex-start;
|
||||
padding-left: 58px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.bluetoothToolbar button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bluetoothActions {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.bluetoothActions button:not(.bluetoothIconButton) {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const state = reactive({
|
||||
discovering: false,
|
||||
offroad: false,
|
||||
selectedAudio: "",
|
||||
pairingAddress: "",
|
||||
devices: [],
|
||||
prompt: null,
|
||||
audioTestAddress: "",
|
||||
@@ -96,6 +97,7 @@ async function refresh() {
|
||||
state.discovering = !!payload.discovering
|
||||
state.offroad = !!payload.offroad
|
||||
state.selectedAudio = String(payload.selected_audio || "")
|
||||
state.pairingAddress = String(payload.pairing_address || "")
|
||||
state.devices = Array.isArray(payload.devices) ? payload.devices : []
|
||||
state.prompt = payload.prompt || null
|
||||
state.error = payload.error || (response.ok ? "" : "Bluetooth service unavailable")
|
||||
@@ -117,21 +119,54 @@ function initialize() {
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function capabilityBadges(device) {
|
||||
const badges = []
|
||||
if (device.audio) badges.push(html`<span class="bluetoothBadge">Audio</span>`)
|
||||
if (device.controller) badges.push(html`<span class="bluetoothBadge">Controller</span>`)
|
||||
if (device.paired) badges.push(html`<span class="bluetoothBadge bluetoothBadgePaired">Paired</span>`)
|
||||
if (device.connected) badges.push(html`<span class="bluetoothBadge bluetoothBadgeConnected">Connected</span>`)
|
||||
return badges
|
||||
function normalizedAddress(device) {
|
||||
return String(device.address || "").toUpperCase()
|
||||
}
|
||||
|
||||
function isPairing(device) {
|
||||
return !!state.pairingAddress && state.pairingAddress.toUpperCase() === normalizedAddress(device)
|
||||
}
|
||||
|
||||
function deviceIcon(device) {
|
||||
if (device.audio && device.controller) return "bi-headset"
|
||||
if (device.audio) return "bi-headphones"
|
||||
if (device.controller) return "bi-controller"
|
||||
return "bi-bluetooth"
|
||||
}
|
||||
|
||||
function deviceCapabilities(device) {
|
||||
const capabilities = []
|
||||
if (device.audio) capabilities.push("Audio")
|
||||
if (device.controller) capabilities.push("Controller")
|
||||
return capabilities.join(" · ") || "Bluetooth device"
|
||||
}
|
||||
|
||||
function deviceStatus(device) {
|
||||
if (isPairing(device)) return "Pairing…"
|
||||
if (device.connected) {
|
||||
const audioSelected = state.selectedAudio.toUpperCase() === normalizedAddress(device)
|
||||
return audioSelected ? "Connected · Audio output" : "Connected"
|
||||
}
|
||||
return device.paired ? "Saved" : "Ready to pair"
|
||||
}
|
||||
|
||||
function knownDevices() {
|
||||
return state.devices.filter((device) => device.paired || device.trusted || device.connected)
|
||||
}
|
||||
|
||||
function availableDevices() {
|
||||
return state.devices.filter((device) => !device.paired && !device.trusted && !device.connected)
|
||||
}
|
||||
|
||||
function deviceActions(device) {
|
||||
const audioSelected = () => state.selectedAudio.toUpperCase() === device.address.toUpperCase()
|
||||
const pairing = () => isPairing(device)
|
||||
return html`
|
||||
<div class="bluetoothActions">
|
||||
${!device.paired ? html`
|
||||
<button disabled="${() => !state.offroad || !!state.busy}" @click="${() => request("pair", { address: device.address })}">Pair</button>
|
||||
<button disabled="${() => !state.offroad || !!state.busy || pairing()}" @click="${() => request("pair", { address: device.address })}">
|
||||
${() => pairing() ? "Pairing…" : "Pair"}
|
||||
</button>
|
||||
` : html`
|
||||
<button disabled="${() => !!state.busy}" @click="${() => request(device.connected ? "disconnect" : "connect", { address: device.address })}">
|
||||
${device.connected ? "Disconnect" : "Connect"}
|
||||
@@ -147,14 +182,48 @@ function deviceActions(device) {
|
||||
</button>
|
||||
` : ""}
|
||||
` : ""}
|
||||
<button class="danger" disabled="${() => !state.offroad || !!state.busy}" @click="${() => {
|
||||
<button class="bluetoothIconButton bluetoothForgetButton" title="Forget device" aria-label="Forget ${device.name}"
|
||||
disabled="${() => !state.offroad || !!state.busy}" @click="${() => {
|
||||
if (window.confirm(`Forget ${device.name}?`)) request("forget", { address: device.address })
|
||||
}}">Forget</button>
|
||||
}}"><i class="bi bi-trash3" aria-hidden="true"></i></button>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function deviceRow(device) {
|
||||
return html`
|
||||
<div class="${() => `bluetoothDeviceRow ${device.connected ? "connected" : ""}`}">
|
||||
<div class="bluetoothDeviceIcon"><i class="bi ${deviceIcon(device)}" aria-hidden="true"></i></div>
|
||||
<div class="bluetoothDeviceDetails">
|
||||
<div class="bluetoothDeviceName">
|
||||
<h3>${device.name}</h3>
|
||||
${device.connected ? html`<span class="bluetoothConnectedDot" title="Connected"></span>` : ""}
|
||||
</div>
|
||||
<p>${deviceCapabilities(device)}</p>
|
||||
<span class="bluetoothDeviceStatus">${() => deviceStatus(device)}</span>
|
||||
</div>
|
||||
${deviceActions(device)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function deviceSection(title, icon, devices, emptyText = "") {
|
||||
return html`
|
||||
<section class="bluetoothSection">
|
||||
<div class="bluetoothSectionHeader">
|
||||
<div><i class="bi ${icon}" aria-hidden="true"></i><h3>${title}</h3></div>
|
||||
<span>${devices.length}</span>
|
||||
</div>
|
||||
<div class="bluetoothSectionBody">
|
||||
${devices.length ? devices.map(deviceRow) : html`
|
||||
<div class="bluetoothEmptyState">${emptyText}</div>
|
||||
`}
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
|
||||
export function Bluetooth() {
|
||||
initialize()
|
||||
return html`
|
||||
@@ -164,7 +233,7 @@ export function Bluetooth() {
|
||||
<i class="bi bi-bluetooth" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h2>Bluetooth</h2>
|
||||
<p>Connect audio devices and controllers.</p>
|
||||
<p>Connect speakers, headphones, media controls, and controllers.</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="bluetoothSwitch">
|
||||
@@ -188,28 +257,30 @@ export function Bluetooth() {
|
||||
|
||||
<div class="bluetoothToolbar">
|
||||
<button disabled="${() => !state.offroad || !state.enabled || !!state.busy}"
|
||||
class="${() => state.discovering ? "scanning" : ""}"
|
||||
@click="${() => request(state.discovering ? "stop_scan" : "scan")}">
|
||||
${() => state.discovering ? "Stop Scanning" : "Scan for Devices"}
|
||||
<i class="${() => `bi ${state.discovering ? "bi-arrow-repeat" : "bi-search"}`}" aria-hidden="true"></i>
|
||||
${() => state.discovering ? "Searching…" : "Search for Devices"}
|
||||
</button>
|
||||
<button disabled="${() => !!state.busy}" @click="${refresh}">Refresh</button>
|
||||
<button class="bluetoothSecondaryButton" disabled="${() => !!state.busy}" @click="${refresh}">
|
||||
<i class="bi bi-arrow-clockwise" aria-hidden="true"></i> Refresh
|
||||
</button>
|
||||
<span class="bluetoothScanHint">Put a device in pairing mode before searching.</span>
|
||||
</div>
|
||||
|
||||
<div class="bluetoothDeviceList">
|
||||
${() => state.loading ? html`<div class="bluetoothCard">Loading...</div>` : ""}
|
||||
${() => !state.loading && state.devices.length === 0 ? html`
|
||||
<div class="bluetoothCard">${state.enabled ? "No Bluetooth devices found." : "Enable Bluetooth to find devices."}</div>
|
||||
` : ""}
|
||||
${() => state.devices.map((device) => html`
|
||||
<div class="bluetoothCard">
|
||||
<div class="bluetoothDeviceHeader">
|
||||
<div>
|
||||
<h3>${device.name}</h3>
|
||||
</div>
|
||||
<div class="bluetoothBadges">${capabilityBadges(device)}</div>
|
||||
</div>
|
||||
${deviceActions(device)}
|
||||
${() => state.loading ? html`<div class="bluetoothLoading"><span></span><span></span><span></span></div>` : ""}
|
||||
${() => !state.loading && !state.enabled ? html`
|
||||
<div class="bluetoothEmptyPage">
|
||||
<i class="bi bi-bluetooth" aria-hidden="true"></i>
|
||||
<h3>Bluetooth is off</h3>
|
||||
<p>Turn it on to reconnect saved devices or find something new.</p>
|
||||
</div>
|
||||
`)}
|
||||
` : ""}
|
||||
${() => !state.loading && state.enabled ? html`
|
||||
${deviceSection("My Devices", "bi-check2-circle", knownDevices(), "No saved devices yet.")}
|
||||
${deviceSection("Available Devices", "bi-radar", availableDevices(), state.discovering ? "Searching for nearby devices…" : "No nearby devices found. Start a search to try again.")}
|
||||
` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -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-3">
|
||||
<link rel="stylesheet" href="/assets/components/tools/bluetooth.css?v=bluetooth-4">
|
||||
<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">
|
||||
|
||||
@@ -37,6 +37,11 @@ def test_bluetooth_actions_use_reactive_disabled_bindings():
|
||||
assert 'request("test_audio", { address: device.address })' in source
|
||||
assert "startAudioTestCountdown" in source
|
||||
assert "The test sound is sent at NOW" in source
|
||||
assert 'deviceSection("My Devices"' in source
|
||||
assert 'deviceSection("Available Devices"' in source
|
||||
assert "bluetoothForgetButton" in source
|
||||
assert "bi-trash3" in source
|
||||
assert "state.pairingAddress" in source
|
||||
|
||||
|
||||
def test_controller_test_mode_has_explicit_start_and_stop():
|
||||
|
||||
@@ -149,6 +149,7 @@ def test_bluetooth_status_api(monkeypatch):
|
||||
"enabled": True,
|
||||
"error": "",
|
||||
"offroad": True,
|
||||
"pairing_address": "",
|
||||
"powered": True,
|
||||
"prompt": None,
|
||||
"selected_audio": "",
|
||||
|
||||
@@ -13,6 +13,7 @@ class BluetoothManager:
|
||||
self._active = False
|
||||
self._exit = False
|
||||
self._operation_error = ""
|
||||
self._operations = {}
|
||||
self._audio_test_deadline = 0.0
|
||||
self._thread = threading.Thread(target=self._poll, daemon=True)
|
||||
self._thread.start()
|
||||
@@ -34,6 +35,10 @@ class BluetoothManager:
|
||||
self._operation_error = ""
|
||||
return error
|
||||
|
||||
def operation_for(self, address: str) -> str:
|
||||
with self._lock:
|
||||
return self._operations.get(address.upper(), "")
|
||||
|
||||
def audio_test_phase(self) -> str:
|
||||
with self._lock:
|
||||
deadline = self._audio_test_deadline
|
||||
@@ -58,13 +63,23 @@ class BluetoothManager:
|
||||
self._status = BluetoothStatus(error=str(error))
|
||||
time.sleep(1.0 if self._active else 2.0)
|
||||
|
||||
def _run(self, fn, *args) -> None:
|
||||
def _run(self, fn, *args, operation: str = "", address: str = "") -> None:
|
||||
normalized_address = address.upper()
|
||||
if normalized_address:
|
||||
with self._lock:
|
||||
self._operations[normalized_address] = operation
|
||||
|
||||
def worker():
|
||||
try:
|
||||
fn(*args)
|
||||
except Exception as error:
|
||||
with self._lock:
|
||||
self._operation_error = str(error)
|
||||
finally:
|
||||
if normalized_address:
|
||||
with self._lock:
|
||||
if self._operations.get(normalized_address) == operation:
|
||||
self._operations.pop(normalized_address, None)
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def set_power(self, enabled: bool) -> None:
|
||||
@@ -74,16 +89,16 @@ class BluetoothManager:
|
||||
self._run(self._client.start_scan if scanning else self._client.stop_scan)
|
||||
|
||||
def pair(self, address: str) -> None:
|
||||
self._run(self._client.pair, address)
|
||||
self._run(self._client.pair, address, operation="pairing", address=address)
|
||||
|
||||
def connect(self, address: str) -> None:
|
||||
self._run(self._client.connect, address)
|
||||
self._run(self._client.connect, address, operation="connecting", address=address)
|
||||
|
||||
def disconnect(self, address: str) -> None:
|
||||
self._run(self._client.disconnect, address)
|
||||
self._run(self._client.disconnect, address, operation="disconnecting", address=address)
|
||||
|
||||
def forget(self, address: str) -> None:
|
||||
self._run(self._client.forget, address)
|
||||
self._run(self._client.forget, address, operation="forgetting", address=address)
|
||||
|
||||
def select_audio(self, address: str) -> None:
|
||||
self._run(self._client.select_audio, address)
|
||||
|
||||
Reference in New Issue
Block a user