Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion bin/omarchy-bluetooth-device
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,30 @@ trust_device() {
bluetoothctl trust "$address" >/dev/null 2>&1 || true
}

adapter_pairable() {
[[ $(timeout 2s bluetoothctl show 2>/dev/null) == *"Pairable: yes"* ]]
}

# BlueZ only flags the adapter pairable while a default agent is registered,
# and the non-interactive bluetoothctl pair does not register one. Against a
# non-pairable adapter the kernel negotiates "no bonding": pairing succeeds and
# audio plays, but the link key never reaches /var/lib/bluetooth, so the next
# reconnect is refused and the device has to be forgotten and paired again.
# Raise the flag for the handshake and put it back the way it was.
pair_device() {
local raised=false
if ! adapter_pairable; then
bluetoothctl pairable on >/dev/null 2>&1 && raised=true
fi
timeout 20s bluetoothctl pair "$address" >/dev/null 2>&1 || true
[[ $raised == true ]] && bluetoothctl pairable off >/dev/null 2>&1
return 0
}

case "$action" in
pair)
power_on
timeout 20s bluetoothctl pair "$address" >/dev/null 2>&1 || true
pair_device
trust_device
timeout 20s bluetoothctl connect "$address" >/dev/null 2>&1 || true
;;
Expand Down
40 changes: 40 additions & 0 deletions test/shell.d/bluetooth-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -151,19 +151,22 @@ trap 'rm -rf "$device_tmp"' EXIT
mock_bin="$device_tmp/bin"
mkdir -p "$mock_bin"
export POWERED_FILE="$device_tmp/powered"
export PAIRABLE_FILE="$device_tmp/pairable"

cat >"$mock_bin/bluetoothctl" <<'SH'
#!/bin/bash

printf '%s\n' "$*" >>"$BLUETOOTHCTL_LOG"
[[ $1 == "power" && $2 == "on" ]] && echo yes >"$POWERED_FILE"
[[ $1 == "pairable" ]] && echo "$2" >"$PAIRABLE_FILE"
[[ $1 == "list" ]] &&
for c in ${MOCK_CONTROLLERS:-AA:BB:CC:DD:EE:FF}; do printf 'Controller %s mock\n' "$c"; done
# Per-controller state where a test set it, the shared file otherwise.
if [[ $1 == "show" ]]; then
state="$POWERED_FILE"
[[ -n ${2:-} && -f "$POWERED_FILE.$2" ]] && state="$POWERED_FILE.$2"
printf '\tPowered: %s\n' "$(cat "$state")"
printf '\tPairable: %s\n' "$(cat "$PAIRABLE_FILE" 2>/dev/null || echo no)"
fi
exit 0
SH
Expand Down Expand Up @@ -263,6 +266,43 @@ grep -qx "connect AA:BB:CC:DD:EE:FF" "$unpowered_log" ||
fail "bluetooth connects once the adapter is up" "$(cat "$unpowered_log")"
pass "bluetooth connects once the adapter is up"

# BlueZ only marks the adapter pairable while a default agent is registered, and
# bt-agent is skipped whenever bluetoothd or a USB dongle comes up after the
# session does. Pairing against a non-pairable adapter bonds without
# persistence: the link key never reaches /var/lib/bluetooth and the next
# reconnect is refused, so the device has to be forgotten and paired again.
bluetooth_pair_log() {
echo "$1" >"$PAIRABLE_FILE"
bluetooth_run yes "$ROOT/bin/omarchy-bluetooth-device" pair AA:BB:CC:DD:EE:FF
}

log_line() {
grep -nx "$2" "$1" | cut -d: -f1 | head -1
}

pair_log=$(bluetooth_pair_log no)
grep -qx "pairable on" "$pair_log" ||
fail "bluetooth makes the adapter pairable before pairing" "$(cat "$pair_log")"
pass "bluetooth makes the adapter pairable before pairing"

(( $(log_line "$pair_log" "pairable on") < $(log_line "$pair_log" "pair AA:BB:CC:DD:EE:FF") )) ||
fail "bluetooth raises pairable ahead of the pair handshake" "$(cat "$pair_log")"
pass "bluetooth raises pairable ahead of the pair handshake"

(( $(log_line "$pair_log" "pairable off") > $(log_line "$pair_log" "pair AA:BB:CC:DD:EE:FF") )) ||
fail "bluetooth lowers pairable again after the pair handshake" "$(cat "$pair_log")"
pass "bluetooth lowers pairable again after the pair handshake"

grep -qx "connect AA:BB:CC:DD:EE:FF" "$pair_log" ||
fail "bluetooth still connects after pairing" "$(cat "$pair_log")"
pass "bluetooth still connects after pairing"

# An adapter the agent already holds pairable is not ours to switch off.
agent_log=$(bluetooth_pair_log yes)
grep -q "pairable" "$agent_log" &&
fail "bluetooth leaves pairable alone when the agent already holds it" "$(cat "$agent_log")"
pass "bluetooth leaves pairable alone when the agent already holds it"

# Blocking hits every radio at once, so the read has to span them too. A bare
# bluetoothctl show reports the default controller and misses a powered dongle.
echo yes >"$POWERED_FILE.11:22:33:44:55:66"
Expand Down