Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

bluetooth

BlueZ: adapter power, discovery, connected, paired and discovered devices, and pairing prompts.

list {
    items = mantle.bluetooth:map(function(bluetooth)
        return bluetooth.connected_devices
    end),
    key = function(device) return device.mac end,
    each = function(device)
        local battery = device.battery and string.format(" %d%%", device.battery) or ""
        return rect {
            on_click = function() mantle.bluetooth:disconnect(device.mac) end,
            children = { text { content = device.name .. battery } },
        }
    end,
}

State

mantle.bluetooth:get() returns BluetoothState, nil before the first push. A field marked ? may be absent.

FieldTypeDescription
availablebooleanBlueZ has an adapter; false without one or without bluetoothd.
connected_devicesConnectedDevice[]Paired, connected devices. Unordered and may reshuffle on any push: sort before drawing.
discoverablebooleanOther devices can find this adapter. BlueZ turns it off after DiscoverableTimeout (180 s by default).
discovered_devicesDiscoveredDevice[]Unpaired devices BlueZ knows, unordered. Kept after stop_discovery; BlueZ expires unseen temporary ones after TemporaryTimeout (30 s by default).
discoveringbooleanThe adapter is scanning, whichever client started it.
enabledbooleanThe adapter is powered.
paired_devicesPairedDevice[]Paired devices that are not connected. Unordered like connected_devices.
pairing_request?PairingRequestThe pairing question to show, or nil. Entry requests use secure_submit; confirmation, authorization and service requests use answer_pairing.

ConnectedDevice

FieldTypeDescription
battery?integerBattery percentage, or nil when the device reports none.
busy?DeviceActionSame as DiscoveredDevice.busy.
categoryDeviceCategory
macstringMAC address, e.g. "00:1A:7D:DA:71:11"; every bluetooth action takes it.
namestringThe device’s advertised name, or empty.

DeviceAction

What the shell is doing to a device, as its busy.

One of "pairing", "connecting", "disconnecting".

DeviceCategory

A device’s kind, read from its BlueZ class of device.

ValueDescription
"keyboard"
"mouse"
"headphones"
"headset"
"phone"
"computer"
"generic"Any class not listed.

DiscoveredDevice

FieldTypeDescription
blockedbooleanBlueZ refuses to pair with or connect to the device until it is unblocked.
busy?DeviceActionThe action this shell is running on the device, or nil; another client’s never shows.
macstringMAC address, the argument of pair.
namestringAdvertised name, often empty when the device broadcasts only an address.

PairedDevice

FieldTypeDescription
blockedbooleanBlueZ refuses every connection to or from the device until it is unblocked.
busy?DeviceActionSame as DiscoveredDevice.busy.
categoryDeviceCategory
macstringMAC address, the argument of connect and forget.
namestringThe device’s advertised name, or empty.

PairingKind

What a pairing_request asks; see PairingRequest.kind.

One of "confirm", "authorize", "service", "display", "pin_entry", "passkey_entry".

PairingRequest

What the pairing agent is asking the user.

FieldTypeDescription
code?stringSix-digit passkey for "confirm", passkey or PIN for "display", else nil.
idstringUnique for this Supervisor session. Include it with the device MAC in a secure entry target.
kindPairingKind"confirm": does the device show code? "authorize": may it pair? "service": may a paired, untrusted device connect? "display": type code on the device; nothing to answer. "pin_entry" and "passkey_entry": type a secret in a Bluetooth secure_submit field.
macstringThe device’s MAC address.
namestringThe device’s advertised name, or empty.

Actions

Call each as mantle.bluetooth:<action>(arguments...); ? marks an argument you may omit.

ActionArgumentsDescription
set_enabledenabled: booleanPowers the adapter on or off.
set_discoverablediscoverable: booleanMakes the adapter findable by other devices, or not.
start_discoveryClears discovered_devices and scans. The request holds, so a scan starts once the adapter powers on and pauses while a pair runs.
stop_discoveryStops discovery; discovered_devices stays.
pairmac: stringPairs a discovered device, then trusts and connects it.
connectmac: stringTrusts and connects a paired device.
disconnectmac: stringDisconnects a connected device.
forgetmac: stringRemoves a device from BlueZ, unpairing it.
answer_pairingmac: string, accept: booleanAccepts or rejects a confirmation, authorization or service pairing_request for mac; a yes within 750 ms of it appearing is ignored. Entry requests use secure_submit.

Backend

BlueZ on the system bus.

ContractBehavior
Stateorg.bluez’s ObjectManager plus property changes. An adapter added later is picked up; if the tracked one disappears, another present adapter takes over. A bluetoothd that starts late or restarts is read afresh and gets the pairing agent again; what the old one reported, and its open pairing_request, are dropped. Without BlueZ, available is false, the lists stay empty and every action does nothing
AgentMantle registers the default KeyboardDisplay agent at /org/mantle/Bluez/Agent1. A confirmation, authorization, displayed code, PIN entry or passkey entry becomes pairing_request only while the adapter is discoverable or Mantle is pairing that device. A "service" request asks only for a paired device. One request shows at a time: a second is rejected, unless the first only displays a code and the second needs an answer
Discoverystart_discovery clears discovered_devices, but the next device change refills it with every unpaired device BlueZ still holds, earlier scans’ included; stop_discovery keeps it
Battery, categoryBattery1 gives battery; the Class major and minor bits give category
CodecsPipeWire owns them: audio.cards lists each device’s profiles, joined here by mac, each with its codec when it names one, and set_card_profile switches one

How do I…

Pair a new device

Scan, pair a discovered device, then answer the pairing_request it raises. A "display" request only shows a code to type on the device. Confirm, authorize and service requests take a yes or no; a yes within 750 ms of the prompt appearing is ignored. For "pin_entry" or "passkey_entry", submit a masked field with name = request.id .. "/" .. request.mac. The PIN must have 1–16 ASCII letters or digits; a passkey must have 1–6 digits (0–999999). A stale id or device is refused. Use No to decline an entry request. The typed value never enters Lua:

local function answer(accept)
    return function()
        local bluetooth = mantle.bluetooth:get()
        local request = bluetooth and bluetooth.pairing_request
        if request then
            mantle.bluetooth:answer_pairing(request.mac, accept)
        end
    end
end

local function button(label, on_click, visible)
    return rect { padding = 4, on_click = on_click, visible = visible, children = { text { content = label } } }
end

return column {
    spacing = 6,
    children = {
        button("Scan", function() mantle.bluetooth:start_discovery() end),
        list {
            items = mantle.bluetooth:map(function(bluetooth)
                return bluetooth.discovered_devices
            end),
            key = function(device) return device.mac end,
            each = function(device)
                return button(device.name ~= "" and device.name or device.mac, function()
                    mantle.bluetooth:pair(device.mac)
                end)
            end,
        },
        text {
            content = mantle.bluetooth:map(function(bluetooth)
                local request = bluetooth.pairing_request
                if request == nil then
                    return ""
                end
                return string.format("%s (%s) %s", request.name, request.kind, request.code or "")
            end, ""),
        },
        row {
            visible = mantle.bluetooth:map(function(bluetooth)
                local request = bluetooth.pairing_request
                return request ~= nil and
                    (request.kind == "confirm" or request.kind == "authorize" or request.kind == "service")
            end, false),
            spacing = 6,
            children = { button("Yes", answer(true)), button("No", answer(false)) },
        },
        textfield {
            visible = mantle.bluetooth:map(function(bluetooth)
                local request = bluetooth.pairing_request
                return request ~= nil and (request.kind == "pin_entry" or request.kind == "passkey_entry")
            end, false),
            width = "fill",
            height = 28,
            placeholder = "PIN or passkey",
            accessible_name = "Bluetooth PIN or passkey",
            secure_submit = mantle.bluetooth:map(function(bluetooth)
                local request = bluetooth.pairing_request
                if request and (request.kind == "pin_entry" or request.kind == "passkey_entry") then
                    return { capability = "bluetooth", action = "pair", name = request.id .. "/" .. request.mac }
                end
            end),
        },
        button("No", answer(false), mantle.bluetooth:map(function(bluetooth)
            local request = bluetooth.pairing_request
            return request ~= nil and (request.kind == "pin_entry" or request.kind == "passkey_entry")
        end, false)),
    },
}

Gotchas

TrapFix
A device pairing from its own side gets rejectedMantle only prompts for invited devices. Set set_discoverable to true while pairing

Source: supervisor/src/capabilities/bluetooth/