Skip to content

Update Bluetooth to v3.0.0 - #1106

Open
Outpox wants to merge 1 commit into
SteamDeckHomebrew:mainfrom
Outpox:main
Open

Update Bluetooth to v3.0.0#1106
Outpox wants to merge 1 commit into
SteamDeckHomebrew:mainfrom
Outpox:main

Conversation

@Outpox

@Outpox Outpox commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Update Bluetooth to v3.0.0

bluethoothctl replacement with D-Bus

Plugin used subprocess calls to bluetoothctl since the beginning but it was always a bit unreliable because it conflicted with Steam. Then Steam shared SteamClient.System.Bluetooth which was much more reliable but it did not last long as they removed it.
Fed up with this and getting issues opened I finally took some time and switched the project to using dbus with the jeepney python library instead.
Devices now display properly and also got the added bonus to retrieve the current battery level if available.

image

Full changelog

  • rewrote the backend to talk to BlueZ directly over D-Bus, no more bluetoothctl subprocess calls
  • show battery level next to the device status when the device reports one
  • reworked the device row, connect/disconnect is now a proper focusable item
  • new generic controller icon
  • fix long device names shifting the device list
  • timeout and retries on every backend call
  • dropped jest, moved to eslint 9 + prettier, dependencies updates

Task Checklist

Developer

  • I am the original author or an authorized maintainer of this plugin.
  • I have abided by the licenses of the libraries I am utilizing, including attaching license notices where appropriate.
  • Generative AI was NOT used to write a majority of the code I am submitting.

Plugin

  • I have verified that my plugin works properly on the Stable and Beta update channels of SteamOS.
  • I have verified my plugin is unique or provides more/alternative functionality to a plugin already on the store.

Backend

  • No: I am using a custom backend other than Python.
  • No: I am using a tool or software from a 3rd party FOSS project that does not have it's dependencies statically linked.
  • No: I am using a custom binary that has all of it's dependencies statically linked.

Community

  • I have tested and left feedback on two other pull requests for new or updating plugins.
  • I have commented links to my testing report in this PR.

Testing

  • Tested by a third party on SteamOS Stable or Beta update channel.

@Outpox
Outpox requested a review from a team as a code owner August 13, 2026 19:39
@github-actions github-actions Bot added the plugin-update Updating a plugin on the Plugin Store label Aug 13, 2026
@github-actions

Copy link
Copy Markdown

Issues Found

No issues with your PR description were found.

Next Steps

  1. If we found any issues above, please edit your pull request description to resolve them and leave a comment saying you've done so.
  2. For the quickest review, please see the Community section of the pull request template for how you can help other developers.
  3. Once your description is correct, a maintainer will review your pull request as soon as possible.

Thank you for your contribution! If you need any help, please reach out on our Discord server. ❤️

@jwhitlow45

jwhitlow45 commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Plugin Testing Report

I manually installed the plugin at the commit referenced by this PR's submodule.

Installed Plugins

  • Game Theme Music - 1.7.1-1
  • ProtonDB Badges - 1.2.0
  • Game Sync - 1.1.2
  • Tab Master - 2.15.0
  • Tailscale Control - 0.1.4
  • Audio Loader - 1.6.1
  • Free Loader - 1.5.2
  • CSS Loader - 2.1.2
  • PowerTools - 2.0.3
  • Animation Changer - 1.3.2
  • AutoFlatpaks - 1.6.8
  • SteamGridDB - 1.7.1
  • vibrantDeck - 2.0.1
  • Volume Mixer - 1.2.1-1

Specifications

  • SteamOS 3.8.16 (Stable)
  • Steam 1785799196 (Stable)
  • Decky v3.2.6 (Stable)

Issues

Has the following major blocking issue(s): N/A
Has the following minor non-blocking issue(s): Not an issue per se, but a quick nice-to-have would be for the text for the primary select button (A w/ Steam Deck controls) changed to be CONNECT/DISCONNECT depending on the connection state of the focused device in the "Bluetooth Devices" list. This would make it more apparent that the primary select action on a given device does something. At the moment it just says "SELECT".

Summary

Tested with a few different Bluetooth devices that report their battery life

  • Connected properly and device sound/inputs worked as expected
    • Additionally, when initially fighting my phone to connect to my headphones the plugin properly showed them as "NOT CONNECTED" after the failed attempt to connect
  • Connecting/disconnecting by clicking on a device works as expected
  • Correct battery life displays in plugin UI, cross-referenced with my phone
  • Generic controller icon looks good

Devices tested with:

  • Sony WH1000-XM4
  • OpenSwim Pro
  • DualSense Wireless Controller

@zomars

zomars commented Aug 18, 2026

Copy link
Copy Markdown

Testing report — Bluetooth v3.0.0

Hardware: Steam Machine (not a Deck), SteamOS Stable, Decky Loader 3.2.6, BlueZ 5.83, single adapter hci0.
Build: built from the submodule commit 95e7ddfb with ghcr.io/steamdeckhomebrew/builder:latest. The resulting dist/index.js is byte-identical to the v3.0.0 release asset apart from the build stamp, and main.py matches by sha256, so this is the same artifact users will get.
Test devices: a DualSense Wireless Controller (input-gaming) and a pair of BT earbuds (audio-headphones).

Works

  • Installs and loads cleanly, no errors on load.
  • Bluetooth toggle works in both directions; verified against bluetoothctl show each time.
  • Paired device list is correct — name, icon and connected state all right — and fast: get_paired_devices_with_info returns in 1–4 ms.
  • The new controller icon renders. BlueZ reports the DualSense as input-gaming and the gamepad glyph shows in the row.
  • Activating a device row does connect and disconnect the device at the BlueZ level. Connecting an awake device is quick — 25 ms for the DualSense.
  • Malformed input doesn't crash the backend (get_device_info with a non-MAC string falls back cleanly and logs).

Issues

1. The device row doesn't update after you connect or disconnect it. In src/server.ts:

export async function toggleDeviceConnection(device: Device): Promise<void> {
  controller.toggleDeviceConnection(device.mac, device.connected).catch(console.error);
}

The inner promise isn't returned, so the async wrapper resolves immediately and void toggleDeviceConnection(device).then(refresh) in device.tsx:45 refreshes before the D-Bus call has done anything. Measured: activating the DualSense row disconnected it — bluetoothctl confirms Connected: no — but the row still read CONNECTED 12 s later and stayed stale indefinitely, since nothing polls. Hitting the refresh button corrected it instantly. toggleBluetooth in the same file has the identical missing return, feeding handleToggleBluetooth's .finally(() => refreshStatus(0)).

2. D-Bus error replies are never checked. router.send_and_get_reply() returns the error message rather than raising — its own docstring says "Returns the reply message (method return or error message type)" — and nothing in main.py inspects reply.header.message_type. Reproduced: with the earbuds asleep, BlueZ answers org.bluez.Error.Failed br-connection-page-timeout after 5.14 s, and toggle_device_connection returns None, logs nothing, and the UI gives no feedback at all. toggle_bluetooth has the same shape. jeepney.wrappers.unwrap_msg(reply) raises DBusErrorResponse and would fix all of these in one line.

3. reply.body[0][1] silently yields a character on an error reply. An error body is a 1-tuple holding the message string, so indexing [1] returns its second character instead of failing:

  • get_device_info() returns "battery": "o" for any device without org.bluez.Battery1 — reproduced on both test devices. 'o' is the second character of No such interface 'org.bluez.Battery1'. It doesn't reach the panel today, since the panel uses get_paired_devices_with_info, which handles this correctly — but it is live in an exported method.
  • get_bluetooth_status() has no try/except, so against an adapter path that doesn't resolve it returns 'e' — truthy, so the toggle would read as permanently ON. Verified by pointing the same code at /org/bluez/hci9.

4. The battery readout won't fire for PlayStation controllers. org.bluez.Battery1 only exists for devices that report battery over GATT. The DualSense is HID over BR/EDR (UUIDs 0x1124 and 0x1200 only) and BlueZ exposes no Battery1 for it — GetAll returns No such interface. The plugin correctly shows no battery, but on this setup it won't show one for probably the most common device people pair to a Deck. The level is available, from the hid-playstation kernel driver:

/sys/class/power_supply/ps-controller-battery-XX:XX:XX:XX:XX:XX/capacity → 100

A /sys/class/power_supply/* fallback keyed on the MAC would cover DualShock/DualSense, and it reads without root so it fits the plugin's current permissions.

5. retryWithTO doesn't cover the call that most needs it. The changelog says "timeout and retries on every backend call", but retryWithTO wraps only getStatus, getPairedDevicesWithInfo, getDeviceInfo and toggleBluetooth — all of which return in single-digit milliseconds here. toggleDeviceConnection is passed straight through, and that's the one that can sit for 5.14 s on a sleeping device. Worth either covering it with a suitably long timeout or dropping the claim.

6. Hardcoded ADAPTER_PATH = '/org/bluez/hci0' (the existing TODO). Note it also feeds _device_addr(), so it isn't only the adapter status that breaks on a box whose adapter isn't hci0 — connect and disconnect go too, because BlueZ scopes device objects under their adapter (/org/bluez/hciN/dev_..., and a wrong N gives org.freedesktop.DBus.Error.UnknownObject), which issue 2 then swallows. get_paired_devices_with_info is unaffected since it goes through GetManagedObjects. I only have one adapter here, so this part is read from the code plus the object-path behaviour above, not reproduced on a real second adapter.

7. Minor: with no paired devices the list renders as an empty gap — no "no devices" message.

One loose end: this turned up once in the plugin log during connect testing and I could not establish what produced it, so take it as an observation rather than a diagnosis.

[ERROR]: Task was destroyed but it is pending!
task: <Task cancelling name='Task-483' coro=<DBusRouter._receiver() done,
      defined at .../py_modules/jeepney/io/asyncio.py:174> wait_for=<Future cancelled>>

Not tested

A device that actually exposes org.bluez.Battery1 — neither test device does, so the battery rendering path itself is unexercised. Also the long-name layout fix, and plugin unload.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plugin-update Updating a plugin on the Plugin Store

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants