mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-10 18:23:44 +08:00
that's not a power button
This commit is contained in:
+1
-1
@@ -21,7 +21,7 @@ fi
|
||||
export QCOM_PRIORITY=12
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="19.6.17"
|
||||
export AGNOS_VERSION="19.6.19"
|
||||
fi
|
||||
|
||||
if [ -z "$AGNOS_ACCEPTED_VERSIONS" ]; then
|
||||
|
||||
@@ -106,6 +106,8 @@ class PairingAgent:
|
||||
class BlueZClient:
|
||||
def __init__(self):
|
||||
self.router = DBusRouter(open_dbus_connection(bus="SYSTEM"))
|
||||
self._request_lock = threading.RLock()
|
||||
self._closed = threading.Event()
|
||||
self.agent = PairingAgent()
|
||||
self._agent_filter = self.router.filter(MatchRule(type="method_call", interface=AGENT_IFACE, path=AGENT_PATH), bufsize=20)
|
||||
self._agent_queue = self._agent_filter.__enter__()
|
||||
@@ -114,17 +116,28 @@ class BlueZClient:
|
||||
self._register_agent()
|
||||
|
||||
def close(self) -> None:
|
||||
if self._closed.is_set():
|
||||
return
|
||||
self._closed.set()
|
||||
self.agent.clear()
|
||||
try:
|
||||
self._call("/org/bluez", AGENT_MANAGER_IFACE, "UnregisterAgent", "o", (AGENT_PATH,))
|
||||
self._agent_queue.put_nowait(None)
|
||||
except Exception:
|
||||
pass
|
||||
with self._request_lock:
|
||||
try:
|
||||
self._call("/org/bluez", AGENT_MANAGER_IFACE, "UnregisterAgent", "o", (AGENT_PATH,))
|
||||
except Exception:
|
||||
pass
|
||||
self._agent_filter.__exit__(None, None, None)
|
||||
self.router.close()
|
||||
self._agent_thread.join(timeout=1.0)
|
||||
|
||||
def _call(self, path: str, interface: str, member: str, signature: str | None = None, body: tuple = (), timeout: float = 15.0):
|
||||
address = DBusAddress(path, bus_name=BLUEZ, interface=interface)
|
||||
message = new_method_call(address, member, signature, body) if signature is not None else new_method_call(address, member)
|
||||
reply = self.router.send_and_get_reply(message, timeout=timeout)
|
||||
with self._request_lock:
|
||||
reply = self.router.send_and_get_reply(message, timeout=timeout)
|
||||
if reply.header.message_type == MessageType.error:
|
||||
error_name = reply.header.fields.get(HeaderFields.error_name, "org.bluez.Error.Failed")
|
||||
detail = reply.body[0] if reply.body else error_name
|
||||
@@ -140,8 +153,10 @@ class BlueZClient:
|
||||
self._call("/org/bluez", AGENT_MANAGER_IFACE, "RequestDefaultAgent", "o", (AGENT_PATH,))
|
||||
|
||||
def _agent_loop(self) -> None:
|
||||
while True:
|
||||
while not self._closed.is_set():
|
||||
message = self._agent_queue.get()
|
||||
if message is None:
|
||||
break
|
||||
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"}:
|
||||
@@ -160,11 +175,17 @@ class BlueZClient:
|
||||
self.agent.clear()
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported pairing request: {member}")
|
||||
self.router.send(new_method_return(message, response_signature, response_body))
|
||||
self._send_agent_reply(new_method_return(message, response_signature, response_body))
|
||||
except PermissionError:
|
||||
self.router.send(new_error(message, "org.bluez.Error.Rejected", "s", ("Pairing rejected",)))
|
||||
self._send_agent_reply(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),)))
|
||||
self._send_agent_reply(new_error(message, "org.bluez.Error.Canceled", "s", (str(error),)))
|
||||
|
||||
def _send_agent_reply(self, message: Any) -> None:
|
||||
try:
|
||||
self.router.send(message)
|
||||
except Exception:
|
||||
self._closed.set()
|
||||
|
||||
def _handle_agent_request(self, message: Any, member: str, device_path: str) -> None:
|
||||
try:
|
||||
@@ -188,11 +209,11 @@ class BlueZClient:
|
||||
accepted, _ = self.agent.request("authorization", device_path)
|
||||
if not accepted:
|
||||
raise PermissionError
|
||||
self.router.send(new_method_return(message, response_signature, response_body))
|
||||
self._send_agent_reply(new_method_return(message, response_signature, response_body))
|
||||
except PermissionError:
|
||||
self.router.send(new_error(message, "org.bluez.Error.Rejected", "s", ("Pairing rejected",)))
|
||||
self._send_agent_reply(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),)))
|
||||
self._send_agent_reply(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")
|
||||
@@ -252,20 +273,24 @@ class BlueZClient:
|
||||
def set_powered(self, powered: bool) -> None:
|
||||
path, _ = self.adapter()
|
||||
address = DBusAddress(path, bus_name=BLUEZ, interface=ADAPTER_IFACE)
|
||||
reply = self.router.send_and_get_reply(Properties(address).set("Powered", "b", powered), timeout=10.0)
|
||||
with self._request_lock:
|
||||
reply = self.router.send_and_get_reply(Properties(address).set("Powered", "b", powered), timeout=10.0)
|
||||
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)
|
||||
with self._request_lock:
|
||||
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)
|
||||
with self._request_lock:
|
||||
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)
|
||||
with self._request_lock:
|
||||
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"))
|
||||
|
||||
@@ -288,7 +313,8 @@ class BlueZClient:
|
||||
def set_device_property(self, address: str, name: str, signature: str, value: Any) -> None:
|
||||
device = self.device_for_address(address)
|
||||
dbus_address = DBusAddress(device["path"], bus_name=BLUEZ, interface=DEVICE_IFACE)
|
||||
reply = self.router.send_and_get_reply(Properties(dbus_address).set(name, signature, value), timeout=10.0)
|
||||
with self._request_lock:
|
||||
reply = self.router.send_and_get_reply(Properties(dbus_address).set(name, signature, value), timeout=10.0)
|
||||
if reply.header.message_type == MessageType.error:
|
||||
raise RuntimeError(str(reply.body[0] if reply.body else f"Unable to set {name}"))
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ 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
|
||||
RECONNECT_INTERVAL_SECONDS = 15.0
|
||||
RECONNECT_MAX_BACKOFF_SECONDS = 300.0
|
||||
MANUAL_DISCONNECT_SUPPRESSION_SECONDS = 300.0
|
||||
|
||||
|
||||
class BluetoothController:
|
||||
@@ -31,6 +34,8 @@ class BluetoothController:
|
||||
self._pairing_address = ""
|
||||
self._pairing_error = ""
|
||||
self._last_reconnect = 0.0
|
||||
self._reconnect_backoff: dict[str, tuple[int, float]] = {}
|
||||
self._manual_disconnect_until: dict[str, float] = {}
|
||||
self._scan_deadline = 0.0
|
||||
self._audio_test_deadline = 0.0
|
||||
self._sleep = sleep
|
||||
@@ -201,15 +206,34 @@ class BluetoothController:
|
||||
raise RuntimeError("Another Bluetooth device is already pairing")
|
||||
self._client().device_for_address(address)
|
||||
self._scan_deadline = 0.0
|
||||
self._reconnect_backoff.pop(address.upper(), None)
|
||||
self._manual_disconnect_until.pop(address.upper(), None)
|
||||
self._pairing_address = address
|
||||
self._pairing_error = ""
|
||||
threading.Thread(target=self._pair_worker, args=(address,), daemon=True).start()
|
||||
elif command == "connect":
|
||||
self._client().connect(address)
|
||||
normalized_address = address.upper()
|
||||
self._reconnect_backoff.pop(normalized_address, None)
|
||||
self._manual_disconnect_until.pop(normalized_address, None)
|
||||
with self._lock:
|
||||
self._client().connect(normalized_address)
|
||||
elif command == "disconnect":
|
||||
self._client().disconnect(address)
|
||||
normalized_address = address.upper()
|
||||
# Mark this before issuing the D-Bus call. A disconnected device can
|
||||
# report NotConnected, and it must not immediately be auto-reconnected.
|
||||
self._manual_disconnect_until[normalized_address] = time.monotonic() + MANUAL_DISCONNECT_SUPPRESSION_SECONDS
|
||||
self._reconnect_backoff.pop(normalized_address, None)
|
||||
try:
|
||||
with self._lock:
|
||||
self._client().disconnect(normalized_address)
|
||||
except RuntimeError as error:
|
||||
if "notconnected" not in str(error).replace(" ", "").lower():
|
||||
self._manual_disconnect_until.pop(normalized_address, None)
|
||||
raise
|
||||
elif command == "forget":
|
||||
self._client().remove(address)
|
||||
self._reconnect_backoff.pop(address.upper(), None)
|
||||
self._manual_disconnect_until.pop(address.upper(), None)
|
||||
if (self.params.get("BluetoothAudioAddress", encoding="utf-8") or "").upper() == address.upper():
|
||||
self.params.remove("BluetoothAudioAddress")
|
||||
elif command == "select_audio":
|
||||
@@ -259,18 +283,36 @@ class BluetoothController:
|
||||
continue
|
||||
now = time.monotonic()
|
||||
self._maintain_scan(status, now)
|
||||
if self._pairing_address or now - self._last_reconnect < 15:
|
||||
if self._pairing_address or now - self._last_reconnect < RECONNECT_INTERVAL_SECONDS:
|
||||
continue
|
||||
self._last_reconnect = now
|
||||
selected = str(status["selected_audio"])
|
||||
candidates = [device for device in status["devices"] if device["paired"] and device["trusted"] and not device["connected"]]
|
||||
candidates.sort(key=lambda device: device["address"].upper() != selected.upper())
|
||||
candidate_addresses = {device["address"].upper() for device in candidates}
|
||||
for address in list(self._manual_disconnect_until):
|
||||
if address not in candidate_addresses or now >= self._manual_disconnect_until[address]:
|
||||
self._manual_disconnect_until.pop(address, None)
|
||||
for address in list(self._reconnect_backoff):
|
||||
if address not in candidate_addresses:
|
||||
self._reconnect_backoff.pop(address, None)
|
||||
for device in candidates:
|
||||
if device["audio"] or device["controller"]:
|
||||
address = device["address"].upper()
|
||||
if now < self._manual_disconnect_until.get(address, 0.0):
|
||||
continue
|
||||
_attempts, retry_after = self._reconnect_backoff.get(address, (0, 0.0))
|
||||
if now < retry_after:
|
||||
continue
|
||||
try:
|
||||
self._client().connect(device["address"])
|
||||
with self._lock:
|
||||
self._client().connect(address)
|
||||
self._reconnect_backoff.pop(address, None)
|
||||
except Exception:
|
||||
cloudlog.warning(f"Bluetooth reconnect failed for {device['address']}")
|
||||
attempts = _attempts + 1
|
||||
delay = min(RECONNECT_INTERVAL_SECONDS * (2 ** (attempts - 1)), RECONNECT_MAX_BACKOFF_SECONDS)
|
||||
self._reconnect_backoff[address] = (attempts, now + delay)
|
||||
cloudlog.warning(f"Bluetooth reconnect failed for {address}; retrying in {delay:.0f}s")
|
||||
except Exception:
|
||||
cloudlog.exception("Bluetooth connection maintenance failed")
|
||||
|
||||
|
||||
@@ -293,6 +293,19 @@ def test_power_pair_audio_and_offroad_enforcement():
|
||||
assert not params.get_bool("BluetoothEnabled") and radio.stops == 1 and clients[0].closed
|
||||
|
||||
|
||||
def test_disconnect_is_idempotent_and_suppresses_auto_reconnect():
|
||||
params = FakeParams(IsOffroad=False, BluetoothEnabled=True)
|
||||
client = FakeBlueZ()
|
||||
controller = BluetoothController(params, lambda: client, FakeRadio())
|
||||
|
||||
controller.handle({"command": "disconnect", "address": client.device["address"]})
|
||||
|
||||
address = client.device["address"].upper()
|
||||
assert client.actions == [("disconnect", client.device["address"])]
|
||||
assert address in controller._manual_disconnect_until
|
||||
assert controller._manual_disconnect_until[address] > time.monotonic()
|
||||
|
||||
|
||||
def test_power_off_preserves_saved_audio_selection():
|
||||
params = FakeParams(IsOffroad=True, BluetoothEnabled=False, BluetoothAudioAddress="00:11:22:33:44:55")
|
||||
controller = BluetoothController(params, FakeBlueZ, FakeRadio())
|
||||
|
||||
@@ -287,6 +287,22 @@ def test_only_key_down_is_dispatched(monkeypatch):
|
||||
daemon.close()
|
||||
|
||||
|
||||
def test_stale_selector_event_after_controller_disconnect_is_ignored():
|
||||
params = FakeParams({"IsOffroad": False})
|
||||
memory = FakeParams()
|
||||
daemon = wheel_controlsd.WheelControlsDaemon(params, memory)
|
||||
read_fd, write_fd = os.pipe()
|
||||
os.set_blocking(read_fd, False)
|
||||
os.write(write_fd, wheel_controlsd.INPUT_EVENT.pack(0, 0, wheel_controlsd.EV_KEY, 30, 1))
|
||||
|
||||
daemon._read_events(read_fd)
|
||||
|
||||
assert read_fd not in daemon.sources
|
||||
assert read_fd not in daemon.buffers
|
||||
os.close(write_fd)
|
||||
daemon.close()
|
||||
|
||||
|
||||
def test_learning_is_cancelled_onroad():
|
||||
params = FakeParams({"IsOffroad": False})
|
||||
memory = FakeParams({wheel_controlsd.LEARN_SLOT_PARAM: 1})
|
||||
|
||||
@@ -627,9 +627,11 @@ class WheelControlsDaemon:
|
||||
self._remove(fd)
|
||||
return
|
||||
|
||||
buffer = self.buffers[fd]
|
||||
buffer = self.buffers.get(fd)
|
||||
source = self.sources.get(fd)
|
||||
if buffer is None or source is None:
|
||||
return
|
||||
buffer.extend(chunk)
|
||||
source = self.sources[fd]
|
||||
while len(buffer) >= INPUT_EVENT.size:
|
||||
raw = bytes(buffer[:INPUT_EVENT.size])
|
||||
del buffer[:INPUT_EVENT.size]
|
||||
@@ -666,7 +668,10 @@ class WheelControlsDaemon:
|
||||
self._scan_devices()
|
||||
self.last_scan = now
|
||||
for key, _mask in self.selector.select(timeout=0.1):
|
||||
self._read_events(key.fd)
|
||||
try:
|
||||
self._read_events(key.fd)
|
||||
except (KeyError, OSError):
|
||||
self._remove(key.fd)
|
||||
now = time.monotonic()
|
||||
if now - self.last_status >= STATUS_INTERVAL_SECONDS:
|
||||
self._publish_status(now)
|
||||
|
||||
@@ -56,30 +56,30 @@
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://files.firestar.link/x/wqn7kn631kdx/boot27.img.xz",
|
||||
"url": "https://files.firestar.link/x/5gjbdh7zoxox/boot28.img.xz",
|
||||
"fallback_urls": [
|
||||
"https://files-east.firestar.link/x/0zzizffpkz3a/boot27.img.xz"
|
||||
"https://files-east.firestar.link/x/9mxc942n97e0/boot28.img.xz"
|
||||
],
|
||||
"hash": "173208aec963fa35a6d9a92c7ca77c5f2b49797def096f3cce05591e78b12460",
|
||||
"hash_raw": "173208aec963fa35a6d9a92c7ca77c5f2b49797def096f3cce05591e78b12460",
|
||||
"size": 48781312,
|
||||
"hash": "9d826668e81b51d493947f91bc436bb94d3f276364737760b59a6660dae366d6",
|
||||
"hash_raw": "9d826668e81b51d493947f91bc436bb94d3f276364737760b59a6660dae366d6",
|
||||
"size": 49043456,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "bc253532027b4756c09ea235b6fd794641d115b6ddbc05c417d6eb89475a22b8"
|
||||
"ondevice_hash": "d7b383535d9f8228e871710ff2973e3ccedace4e8a8b79e89d34fd0d0fde00a7"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://files.firestar.link/x/877rsvjzkmp8/system27.img.xz",
|
||||
"url": "https://files.firestar.link/x/pre1xtwygykq/system28.img.xz",
|
||||
"fallback_urls": [
|
||||
"https://files-east.firestar.link/x/5ni1fkd2k4hg/system27.img.xz"
|
||||
"https://files-east.firestar.link/x/hpfu9a8221uj/system28.img.xz"
|
||||
],
|
||||
"hash": "205dfc25b21eb236cefaabd081aa4ad6e15ccd1c9f9fc623ec29de0c1c70fb06",
|
||||
"hash_raw": "6f204321089c7ec4bae2e8b10eda827c502e6b516e6095a3b659016b4a646d38",
|
||||
"hash": "bb7fe46c2f1f49fefed7226218b8786e5f7e013cef1eaa5f2fb8ee044e087c3a",
|
||||
"hash_raw": "c967bcb9d5f7d2194a22de68d0ed66c0de44e9fd25cd650f99d9497d4d9a2a57",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "a8edcedcf4e6fb2a9af2e723a54f763f1f542fb63742cd1efc913ed30035c237"
|
||||
"ondevice_hash": "50e041fd2348dac61003f521f19b9311f0667d7bca443a12439dfd835bf5daa8"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -86,7 +86,8 @@ class BluetoothManager:
|
||||
|
||||
def worker():
|
||||
try:
|
||||
fn(*args)
|
||||
with self._client_lock:
|
||||
fn(*args)
|
||||
except Exception as error:
|
||||
with self._lock:
|
||||
self._operation_error = str(error)
|
||||
@@ -137,7 +138,8 @@ class BluetoothManager:
|
||||
def test_audio(self, address: str) -> None:
|
||||
def worker():
|
||||
try:
|
||||
delay = self._client.test_audio(address)
|
||||
with self._client_lock:
|
||||
delay = self._client.test_audio(address)
|
||||
with self._lock:
|
||||
self._audio_test_deadline = time.monotonic() + delay
|
||||
except Exception as error:
|
||||
|
||||
Reference in New Issue
Block a user