mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-12 11:13:46 +08:00
weevil
This commit is contained in:
@@ -36,6 +36,9 @@ class PairingAgent:
|
||||
self._condition = threading.Condition()
|
||||
self._prompt: dict[str, Any] | None = None
|
||||
self._response: tuple[bool, str] | None = None
|
||||
self._generation = 0
|
||||
self._auto_accept_paths: set[str] = set()
|
||||
self._auto_accept_incoming = False
|
||||
|
||||
@property
|
||||
def prompt(self) -> dict[str, Any] | None:
|
||||
@@ -44,26 +47,34 @@ class PairingAgent:
|
||||
|
||||
def clear(self) -> None:
|
||||
with self._condition:
|
||||
self._generation += 1
|
||||
self._prompt = None
|
||||
self._response = None
|
||||
self._condition.notify_all()
|
||||
|
||||
def display(self, kind: str, device_path: str, value: str) -> None:
|
||||
with self._condition:
|
||||
self._generation += 1
|
||||
self._prompt = {"id": uuid.uuid4().hex, "kind": kind, "device_path": device_path, "value": value, "display_only": True}
|
||||
|
||||
def request(self, kind: str, device_path: str, value: str = "", timeout: float = 60.0) -> tuple[bool, str]:
|
||||
if self.auto_accept(kind, device_path):
|
||||
return True, ""
|
||||
prompt_id = uuid.uuid4().hex
|
||||
with self._condition:
|
||||
self._generation += 1
|
||||
generation = self._generation
|
||||
self._response = None
|
||||
self._prompt = {"id": prompt_id, "kind": kind, "device_path": device_path, "value": value, "display_only": False}
|
||||
deadline = time.monotonic() + timeout
|
||||
while self._response is None:
|
||||
while self._response is None and self._generation == generation:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
self._prompt = None
|
||||
return False, ""
|
||||
self._condition.wait(remaining)
|
||||
if self._generation != generation:
|
||||
return False, ""
|
||||
response = self._response
|
||||
self._response = None
|
||||
self._prompt = None
|
||||
@@ -77,6 +88,21 @@ class PairingAgent:
|
||||
self._condition.notify_all()
|
||||
return True
|
||||
|
||||
def set_auto_accept(self, device_path: str, enabled: bool) -> None:
|
||||
with self._condition:
|
||||
if enabled:
|
||||
self._auto_accept_paths.add(device_path)
|
||||
else:
|
||||
self._auto_accept_paths.discard(device_path)
|
||||
|
||||
def set_auto_accept_incoming(self, enabled: bool) -> None:
|
||||
with self._condition:
|
||||
self._auto_accept_incoming = enabled
|
||||
|
||||
def auto_accept(self, kind: str, device_path: str) -> bool:
|
||||
with self._condition:
|
||||
return kind in {"confirmation", "authorization"} and (self._auto_accept_incoming or device_path in self._auto_accept_paths)
|
||||
|
||||
|
||||
class BlueZClient:
|
||||
def __init__(self):
|
||||
@@ -118,34 +144,19 @@ class BlueZClient:
|
||||
while True:
|
||||
message = self._agent_queue.get()
|
||||
member = message.header.fields.get(HeaderFields.member, "")
|
||||
device_path = str(message.body[0]) if message.body else ""
|
||||
if member in {"RequestPinCode", "RequestPasskey", "RequestConfirmation", "RequestAuthorization", "AuthorizeService"}:
|
||||
threading.Thread(target=self._handle_agent_request, args=(message, member, device_path), daemon=True).start()
|
||||
continue
|
||||
try:
|
||||
response_signature = None
|
||||
response_body: tuple = ()
|
||||
device_path = str(message.body[0]) if message.body else ""
|
||||
if member == "Release":
|
||||
self.agent.clear()
|
||||
elif member == "RequestPinCode":
|
||||
accepted, value = self.agent.request("pin", device_path)
|
||||
if not accepted:
|
||||
raise PermissionError
|
||||
response_signature, response_body = "s", (value,)
|
||||
elif member == "DisplayPinCode":
|
||||
self.agent.display("display_pin", device_path, str(message.body[1]))
|
||||
elif member == "RequestPasskey":
|
||||
accepted, value = self.agent.request("passkey", device_path)
|
||||
if not accepted:
|
||||
raise PermissionError
|
||||
response_signature, response_body = "u", (int(value),)
|
||||
elif member == "DisplayPasskey":
|
||||
self.agent.display("display_passkey", device_path, f"{int(message.body[1]):06d}")
|
||||
elif member == "RequestConfirmation":
|
||||
accepted, _ = self.agent.request("confirmation", device_path, f"{int(message.body[1]):06d}")
|
||||
if not accepted:
|
||||
raise PermissionError
|
||||
elif member in ("RequestAuthorization", "AuthorizeService"):
|
||||
accepted, _ = self.agent.request("authorization", device_path)
|
||||
if not accepted:
|
||||
raise PermissionError
|
||||
elif member == "Cancel":
|
||||
self.agent.clear()
|
||||
else:
|
||||
@@ -156,6 +167,34 @@ class BlueZClient:
|
||||
except Exception as error:
|
||||
self.router.send(new_error(message, "org.bluez.Error.Canceled", "s", (str(error),)))
|
||||
|
||||
def _handle_agent_request(self, message: Any, member: str, device_path: str) -> None:
|
||||
try:
|
||||
response_signature = None
|
||||
response_body: tuple = ()
|
||||
if member == "RequestPinCode":
|
||||
accepted, value = self.agent.request("pin", device_path)
|
||||
if not accepted:
|
||||
raise PermissionError
|
||||
response_signature, response_body = "s", (value,)
|
||||
elif member == "RequestPasskey":
|
||||
accepted, value = self.agent.request("passkey", device_path)
|
||||
if not accepted:
|
||||
raise PermissionError
|
||||
response_signature, response_body = "u", (int(value),)
|
||||
elif member == "RequestConfirmation":
|
||||
accepted, _ = self.agent.request("confirmation", device_path, f"{int(message.body[1]):06d}")
|
||||
if not accepted:
|
||||
raise PermissionError
|
||||
else:
|
||||
accepted, _ = self.agent.request("authorization", device_path)
|
||||
if not accepted:
|
||||
raise PermissionError
|
||||
self.router.send(new_method_return(message, response_signature, response_body))
|
||||
except PermissionError:
|
||||
self.router.send(new_error(message, "org.bluez.Error.Rejected", "s", ("Pairing rejected",)))
|
||||
except Exception as error:
|
||||
self.router.send(new_error(message, "org.bluez.Error.Canceled", "s", (str(error),)))
|
||||
|
||||
def managed_objects(self) -> dict[str, dict[str, dict[str, Any]]]:
|
||||
body = self._call("/", OBJECT_MANAGER, "GetManagedObjects")
|
||||
return unwrap_variant(body[0]) if body else {}
|
||||
@@ -198,11 +237,17 @@ class BlueZClient:
|
||||
def status(self) -> dict[str, Any]:
|
||||
objects = self.managed_objects()
|
||||
_, adapter = self.adapter(objects)
|
||||
prompt = self.agent.prompt
|
||||
if prompt is not None:
|
||||
prompt = dict(prompt)
|
||||
device = objects.get(prompt.get("device_path", ""), {}).get(DEVICE_IFACE, {})
|
||||
prompt["address"] = str(device.get("Address", ""))
|
||||
prompt["name"] = str(device.get("Alias") or device.get("Name") or prompt["address"] or "Bluetooth device")
|
||||
return {
|
||||
"powered": bool(adapter.get("Powered", False)),
|
||||
"discovering": bool(adapter.get("Discovering", False)),
|
||||
"devices": self.devices(objects, include_discovering=bool(adapter.get("Discovering", False))),
|
||||
"prompt": self.agent.prompt,
|
||||
"prompt": prompt,
|
||||
}
|
||||
|
||||
def set_powered(self, powered: bool) -> None:
|
||||
@@ -212,6 +257,19 @@ class BlueZClient:
|
||||
if reply.header.message_type == MessageType.error:
|
||||
raise RuntimeError(str(reply.body[0] if reply.body else "Unable to change Bluetooth power"))
|
||||
|
||||
def set_discoverable(self, discoverable: bool) -> None:
|
||||
path, _ = self.adapter()
|
||||
address = DBusAddress(path, bus_name=BLUEZ, interface=ADAPTER_IFACE)
|
||||
reply = self.router.send_and_get_reply(Properties(address).set("Pairable", "b", True), timeout=10.0)
|
||||
if reply.header.message_type == MessageType.error:
|
||||
raise RuntimeError(str(reply.body[0] if reply.body else "Unable to enable Bluetooth pairing"))
|
||||
reply = self.router.send_and_get_reply(Properties(address).set("DiscoverableTimeout", "u", 0), timeout=10.0)
|
||||
if reply.header.message_type == MessageType.error:
|
||||
raise RuntimeError(str(reply.body[0] if reply.body else "Unable to configure Bluetooth discoverability"))
|
||||
reply = self.router.send_and_get_reply(Properties(address).set("Discoverable", "b", discoverable), timeout=10.0)
|
||||
if reply.header.message_type == MessageType.error:
|
||||
raise RuntimeError(str(reply.body[0] if reply.body else "Unable to change Bluetooth discoverability"))
|
||||
|
||||
def start_discovery(self) -> None:
|
||||
path, _ = self.adapter()
|
||||
self._call(path, ADAPTER_IFACE, "StartDiscovery")
|
||||
@@ -238,7 +296,11 @@ class BlueZClient:
|
||||
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.agent.set_auto_accept(device["path"], True)
|
||||
try:
|
||||
self._call(device["path"], DEVICE_IFACE, "Pair", timeout=90.0)
|
||||
finally:
|
||||
self.agent.set_auto_accept(device["path"], False)
|
||||
self.set_device_property(address, "Trusted", "b", True)
|
||||
self.agent.clear()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ OFFROAD_COMMANDS = {"set_power", "start_scan", "stop_scan", "pair", "forget", "t
|
||||
SCAN_DURATION = 20.0
|
||||
AUDIO_TEST_START_DELAY = 3.0
|
||||
AUDIO_TEST_HOLD_TIME = 3.0
|
||||
SCAN_RESULT_TTL = 30.0
|
||||
|
||||
|
||||
class BluetoothController:
|
||||
@@ -32,6 +33,7 @@ class BluetoothController:
|
||||
self._pairing_error = ""
|
||||
self._last_reconnect = 0.0
|
||||
self._scan_deadline = 0.0
|
||||
self._recent_devices: dict[str, tuple[dict[str, Any], float]] = {}
|
||||
self._audio_test_deadline = 0.0
|
||||
self._sleep = sleep
|
||||
self.params.remove("BluetoothAudioTestActive")
|
||||
@@ -58,6 +60,11 @@ class BluetoothController:
|
||||
self._radio.start()
|
||||
self._bluez = self._bluez_factory()
|
||||
self._bluez.set_powered(True)
|
||||
self._bluez.agent.set_auto_accept_incoming(self._offroad())
|
||||
try:
|
||||
self._bluez.set_discoverable(True)
|
||||
except Exception as error:
|
||||
cloudlog.warning(f"Bluetooth discoverability setup failed: {error}")
|
||||
return self._bluez
|
||||
|
||||
def _reset_client(self) -> None:
|
||||
@@ -72,6 +79,24 @@ class BluetoothController:
|
||||
def _offroad(self) -> bool:
|
||||
return self.params.get_bool("IsOffroad")
|
||||
|
||||
def _merge_recent_devices(self, result: dict[str, Any]) -> None:
|
||||
now = time.monotonic()
|
||||
current = {str(device.get("address", "")).upper() for device in result["devices"]}
|
||||
if result["discovering"]:
|
||||
for device in result["devices"]:
|
||||
address = str(device.get("address", "")).upper()
|
||||
if address:
|
||||
self._recent_devices[address] = (dict(device), now + SCAN_RESULT_TTL)
|
||||
return
|
||||
|
||||
for address, (device, expires) in list(self._recent_devices.items()):
|
||||
if expires <= now:
|
||||
self._recent_devices.pop(address, None)
|
||||
elif address not in current:
|
||||
result["devices"].append(dict(device))
|
||||
result["devices"].sort(key=lambda device: (not device["connected"], not device["paired"],
|
||||
-(device["rssi"] or -127), device["name"].lower()))
|
||||
|
||||
def status(self) -> dict[str, Any]:
|
||||
# Status lazily initializes the radio, so serialize it with power changes.
|
||||
with self._lock:
|
||||
@@ -92,6 +117,8 @@ class BluetoothController:
|
||||
try:
|
||||
result.update(self._client().status())
|
||||
result["available"] = True
|
||||
self._bluez.agent.set_auto_accept_incoming(result["offroad"])
|
||||
self._merge_recent_devices(result)
|
||||
prompt = result.get("prompt")
|
||||
if prompt is not None and self._pairing_address:
|
||||
prompt["address"] = self._pairing_address
|
||||
@@ -169,6 +196,7 @@ class BluetoothController:
|
||||
self._radio.stop()
|
||||
self.params.put_bool("BluetoothEnabled", False)
|
||||
self._scan_deadline = 0.0
|
||||
self._recent_devices.clear()
|
||||
elif command == "start_scan":
|
||||
if not self.params.get_bool("BluetoothEnabled"):
|
||||
raise RuntimeError("Enable Bluetooth before scanning")
|
||||
@@ -196,6 +224,7 @@ class BluetoothController:
|
||||
self._client().disconnect(address)
|
||||
elif command == "forget":
|
||||
self._client().remove(address)
|
||||
self._recent_devices.pop(address.upper(), None)
|
||||
if (self.params.get("BluetoothAudioAddress", encoding="utf-8") or "").upper() == address.upper():
|
||||
self.params.remove("BluetoothAudioAddress")
|
||||
elif command == "select_audio":
|
||||
|
||||
@@ -39,6 +39,9 @@ class FakeAgent:
|
||||
def __init__(self):
|
||||
self.responses = []
|
||||
|
||||
def set_auto_accept_incoming(self, _enabled):
|
||||
pass
|
||||
|
||||
def respond(self, prompt_id, accepted, value):
|
||||
self.responses.append((prompt_id, accepted, value))
|
||||
return prompt_id == "prompt"
|
||||
@@ -48,6 +51,7 @@ class FakeBlueZ:
|
||||
def __init__(self):
|
||||
self.agent = FakeAgent()
|
||||
self.powered = False
|
||||
self.discoverable = False
|
||||
self.discovering = False
|
||||
self.closed = False
|
||||
self.actions = []
|
||||
@@ -67,6 +71,9 @@ class FakeBlueZ:
|
||||
def set_powered(self, powered):
|
||||
self.powered = powered
|
||||
|
||||
def set_discoverable(self, discoverable):
|
||||
self.discoverable = discoverable
|
||||
|
||||
def status(self):
|
||||
return {"powered": self.powered, "discovering": self.discovering, "devices": [dict(self.device)], "prompt": None}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user