Skip to content

Add a repair action for unresponsive Bluetooth serial ports on macOS - #2229

Open
edwin-truthsearch-io wants to merge 1 commit into
artisan-roaster-scope:masterfrom
edwin-truthsearch-io:macos-bluetooth-repair
Open

edwin-truthsearch-io wants to merge 1 commit into
artisan-roaster-scope:masterfrom
edwin-truthsearch-io:macos-bluetooth-repair

Conversation

@edwin-truthsearch-io

Copy link
Copy Markdown

Follows #2228, which recognizes an unresponsive Bluetooth serial port and tells the user to re-pair the machine. This adds the action that does it for them.

Why re-pairing is the only remedy

Measured on macOS 26.6.2 (Apple Silicon) with a Kaleido on Bluetooth SPP, with the port in the stale state — /dev/cu.<machine> present, open() succeeding instantly, nothing sent or received:

attempted recovery result
reopening the port opens in 0.00s, silent at every baud rate, passively and after a ping
Bluetooth off/on device node survives with the same minor number
power-cycling the machine no change
IOBluetoothDevice.closeConnection() returns success, isConnected() stays true
IOBluetoothDevice.openConnection() no-op, returns instantly
restarting bluetoothd refused: Operation not permitted while System Integrity Protection is engaged
removing the pairing and pairing again works — unpairing deletes the node, the next connect succeeds in ~2s

The pairing record owns the serial binding, and nothing an application can reach rebuilds it — except removing that pairing.

The change

artisanlib/bluetooth_macos.py performs the repair through IOBluetooth: remove the pairing, rediscover the machine by an inquiry, pair it again. The inquiry is not optional — pairing a device built from a bare address fails with error 2; the device has to be discovered first, which is what the system Bluetooth settings do as well.

Verified end to end against a real stale port: the repair completed in about 60 seconds, without any system dialog appearing, and the Kaleido transport reconnected on its own immediately afterwards.

No address or device name is configured anywhere. The machine is derived from the port the user already configured: macOS names a Bluetooth serial port after the device name (spaces and invalid characters replaced, optionally suffixed by the serial service name), so the paired devices are matched against the configured port. On a machine with 12 paired devices — keyboard, mouse, AirPods, phones — the lookup matched exactly one and returned None for a USB serial port and for /dev/cu.Bluetooth-Incoming-Port. That precision is the safety property that matters, since a mismatch would unpair the wrong hardware.

Safety

Removing a pairing cannot be undone by the application if the machine is unreachable, so the repair is:

  • never automatic — only ever triggered by the user
  • behind a confirmation dialog naming the port
  • offered only where it applies: the button is enabled solely for a port served by a paired Bluetooth device, and the whole action is absent off macOS or without the IOBluetooth framework

The app already ships NSBluetoothAlwaysUsageDescription, which IOBluetooth requires; an unbundled process is killed by TCC without it.

Notes

🤖 Generated with Claude Code

On macOS a Bluetooth serial port is left behind stale if the machine
disappears while it is paired (it is switched off, unplugged, or the host is
suspended): /dev/cu.<machine> still exists and opens successfully, but there
is no serial connection behind it any longer, so nothing is sent or received.

Measured on macOS 26.6.2, neither reopening the port, toggling Bluetooth,
power-cycling the machine, nor IOBluetooth closeConnection()/openConnection()
rebuilds it, and the Bluetooth daemon cannot be restarted while System
Integrity Protection is engaged. Only removing the pairing destroys the stale
device, after which pairing the machine again rebuilds a working serial port.
Users currently have to do this by hand without any indication that it is what
their situation requires.

- adds artisanlib/bluetooth_macos.py performing that repair via IOBluetooth:
  it removes the pairing, rediscovers the machine by an inquiry (pairing a
  device that was not discovered first fails) and pairs it again
- the machine is derived from the configured port by matching the paired
  devices against the port name macOS derives from their device name, so
  neither an address nor a device name is configured anywhere
- adds a 'Repair' button to the serial port settings, enabled only for a port
  served by a paired Bluetooth device. As removing a pairing cannot be undone
  if the machine is unreachable, the repair is only ever triggered explicitly
  by the user, after a confirmation, and never automatically

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant