| 1 | # Transport: Linux Bluetooth sockets, SDP and BlueZ |
| 2 | |
| 3 | This file covers the parts of the link that are the same for every device: finding paired devices, finding the RFCOMM channel and opening sockets. Device protocols are in `<device>.md`. |
| 4 | |
| 5 | All of this is public, standard material: |
| 6 | - the Bluetooth Core Specification (SDP: Vol 3, Part B); |
| 7 | - the Linux kernel's Bluetooth socket headers (`include/net/bluetooth/{bluetooth,l2cap,rfcomm}.h`); |
| 8 | - BlueZ's D-Bus API documentation (`doc/org.bluez.Device.rst`, `doc/org.freedesktop.DBus.ObjectManager`). |
| 9 | |
| 10 | |
| 11 | |
| 12 | <a id="paired-devices"></a> |
| 13 | ## 1. Paired devices (BlueZ over D-Bus) |
| 14 | |
| 15 | - **Connection.** budctl talks to BlueZ only over the **system bus**. The D-Bus specification gives its default address as `unix:path=/var/run/dbus/system_bus_socket`. budctl connects to that path itself and hands the stream to zbus. It never reads `DBUS_SYSTEM_BUS_ADDRESS`, which could name a TCP address. |
| 16 | - **Listing devices.** Call `org.freedesktop.DBus.ObjectManager.GetManagedObjects` on service `org.bluez`, path `/`. Each object that has the interface `org.bluez.Device1` is a known device. |
| 17 | - **Device1 properties used:** |
| 18 | |
| 19 | | Property | Type | Use | |
| 20 | |---|---|---| |
| 21 | | `Address` | string `"AA:BB:CC:DD:EE:FF"` | the device to connect to | |
| 22 | | `Name` | string | the name the device reports. Recognition uses this, because the user can rename `Alias` | |
| 23 | | `Alias` | string | shown to the user | |
| 24 | | `Paired` | bool | only paired devices are listed | |
| 25 | | `Connected` | bool | budctl only opens a control link when the audio link is already up | |
| 26 | | `UUIDs` | array of strings | service UUIDs from the device's SDP record, as BlueZ caches them (lowercase) | |
| 27 | |
| 28 | - **Shutdown.** Drop the D-Bus connection before opening Bluetooth sockets. zbus runs an internal thread while a connection is alive. |
| 29 | |
| 30 | <a id="sockets"></a> |
| 31 | ## 2. Linux Bluetooth sockets |
| 32 | |
| 33 | **Constants** (from `<sys/socket.h>` and the kernel Bluetooth headers): |
| 34 | |
| 35 | | Name | Value | |
| 36 | |---|---| |
| 37 | | `AF_BLUETOOTH` | 31 | |
| 38 | | `BTPROTO_L2CAP` | 0 (used with `SOCK_SEQPACKET`, for SDP) | |
| 39 | | `BTPROTO_RFCOMM` | 3 (used with `SOCK_STREAM`, for the control link) | |
| 40 | |
| 41 | **Address byte order.** A `bdaddr_t` holds the address **reversed**: `AA:BB:CC:DD:EE:FF` is stored as bytes `FF EE DD CC BB AA`. |
| 42 | |
| 43 | `struct sockaddr_rc` is 10 bytes: |
| 44 | |
| 45 | | Offset | Field | Type | |
| 46 | |---|---|---| |
| 47 | | 0 | `rc_family` | u16, `AF_BLUETOOTH` | |
| 48 | | 2 | `rc_bdaddr` | 6 bytes, reversed address | |
| 49 | | 8 | `rc_channel` | u8 | |
| 50 | | 9 | padding | — | |
| 51 | |
| 52 | `struct sockaddr_l2` is 14 bytes: |
| 53 | |
| 54 | | Offset | Field | Type | |
| 55 | |---|---|---| |
| 56 | | 0 | `l2_family` | u16 | |
| 57 | | 2 | `l2_psm` | u16, little endian | |
| 58 | | 4 | `l2_bdaddr` | 6 bytes, reversed address | |
| 59 | | 10 | `l2_cid` | u16, little endian, 0 | |
| 60 | | 12 | `l2_bdaddr_type` | u8, 0 = BR/EDR | |
| 61 | | 13 | padding | — | |
| 62 | |
| 63 | The SDP server always listens on **PSM 1**. |
| 64 | |
| 65 | <a id="connect-timeout"></a> |
| 66 | **Connect timeout.** |
| 67 | - For Bluetooth sockets, a blocking `connect()` waits at most the socket's `SO_SNDTIMEO`. |
| 68 | - When that time runs out, the kernel returns **`EINPROGRESS`** (`bt_sock_wait_state`), not `ETIMEDOUT`. budctl treats both as a timeout. |
| 69 | |
| 70 | <a id="errno"></a> |
| 71 | **What budctl makes of errno.** This mapping is our own choice. It decides the exit code. |
| 72 | |
| 73 | | errno | Meaning for budctl | Exit | |
| 74 | |---|---|---| |
| 75 | | `EBUSY`, `EADDRINUSE`, `ECONNREFUSED` on the RFCOMM connect | another client holds the control channel | 3 | |
| 76 | | `EHOSTDOWN`, `EHOSTUNREACH`, `ENOTCONN`, `ECONNRESET`, `ECONNABORTED`, `EPIPE`, `ENODEV`, `ENETDOWN` | buds out of range, adapter off, or link dropped (for example, the case lid closed) | 2 | |
| 77 | | `ETIMEDOUT`, `EINPROGRESS`, `EAGAIN` | no answer in time | 4 | |
| 78 | | anything else | OS error, reported with its text | 2 | |
| 79 | |
| 80 | **Reads** use `poll(POLLIN)` with the time left until the deadline. A `read()` that returns 0 bytes means the buds closed the link. |
| 81 | |
| 82 | **Writes** use `send(..., MSG_NOSIGNAL)`, so a dead link gives `EPIPE` instead of killing the process with `SIGPIPE`. |
| 83 | |
| 84 | <a id="sdp"></a> |
| 85 | ## 3. Finding the RFCOMM channel with SDP |
| 86 | |
| 87 | The buds publish their control service in their SDP record. budctl asks for that record over L2CAP PSM 1 and reads the RFCOMM channel number out of it. It never uses a fixed channel. |
| 88 | |
| 89 | <a id="sdp-pdu"></a> |
| 90 | **PDU header** (Core Spec Vol 3 Part B §4.2). All multi-byte fields are **big endian**. |
| 91 | |
| 92 | | Field | Size | |
| 93 | |---|---| |
| 94 | | PDU id | 1 | |
| 95 | | transaction id | 2 | |
| 96 | | parameter length | 2 | |
| 97 | | parameters | n | |
| 98 | |
| 99 | | PDU id | Name | |
| 100 | |---|---| |
| 101 | | `0x01` | ErrorResponse; parameters: error code (u16) | |
| 102 | | `0x06` | ServiceSearchAttributeRequest | |
| 103 | | `0x07` | ServiceSearchAttributeResponse | |
| 104 | |
| 105 | <a id="sdp-request"></a> |
| 106 | **ServiceSearchAttributeRequest parameters** (§4.7.1): |
| 107 | 1. ServiceSearchPattern: a data-element sequence of UUIDs. budctl sends one 128-bit UUID: `35 11 1C <16 UUID bytes>`. |
| 108 | 2. MaximumAttributeByteCount (u16): `FF FF`. The server still splits a long answer with continuation state. |
| 109 | 3. AttributeIDList: a data-element sequence of attribute ids. budctl asks only for `0x0004`, the ProtocolDescriptorList: `35 03 09 00 04`. |
| 110 | 4. ContinuationState: 1 length byte followed by that many bytes. It is `00` on the first request, and on each later request it is copied from the previous response. |
| 111 | |
| 112 | <a id="sdp-response"></a> |
| 113 | **ServiceSearchAttributeResponse parameters** (§4.7.2): |
| 114 | 1. AttributeListsByteCount (u16). |
| 115 | 2. AttributeLists: that many bytes. This is one part of a data element. Joined across all responses, it forms the complete data element. |
| 116 | 3. ContinuationState. If its length byte is not 0, send the request again with this state. budctl gives up after 16 rounds. |
| 117 | |
| 118 | <a id="sdp-data-elements"></a> |
| 119 | **Data elements** (§3). |
| 120 | - **Header byte:** `type = byte >> 3`, `size index = byte & 7`. |
| 121 | - **Types used here:** 0 nil, 1 unsigned int, 2 signed int, 3 UUID, 4 text, 5 bool, 6 sequence, 7 alternative, 8 URL. |
| 122 | - **Size index:** |
| 123 | - 0 → 1 byte of data (nil: 0 bytes) |
| 124 | - 1 → 2 bytes |
| 125 | - 2 → 4 bytes |
| 126 | - 3 → 8 bytes |
| 127 | - 4 → 16 bytes |
| 128 | - 5, 6, 7 → the data length is in the next 1, 2 or 4 bytes (big endian), then the data follows. |
| 129 | - **UUIDs** are 2, 4 or 16 bytes. A short UUID `xxxx` or `xxxxxxxx` stands for `xxxxxxxx-0000-1000-8000-00805F9B34FB` (the Bluetooth base UUID). |
| 130 | |
| 131 | <a id="sdp-rfcomm-channel"></a> |
| 132 | **Reading the channel.** |
| 133 | - The joined AttributeLists is a sequence with one item per matching service record. Each item is a sequence of (attribute id, value) pairs. |
| 134 | - For attribute `0x0004`, the value is a sequence of protocol descriptors, each itself a sequence that starts with a UUID. |
| 135 | - The descriptor whose UUID is **RFCOMM (`0x0003`)** has the channel number next, as an unsigned 8-bit integer. |
| 136 | - Example: |
| 137 | `35 0C 35 03 19 01 00 35 05 19 00 03 08 0F` |
| 138 | - `35 03 19 01 00` is L2CAP (`0x0100`). |
| 139 | - `35 05 19 00 03 08 0F` is RFCOMM, channel 15. |
| 140 | - budctl walks the data elements properly. It does not search for a byte pattern. |