Krolyxon
Commit 904ab9a

Branches

Refs →

budctldocsprotocoltransport.md

140 lines · 6.7 KB · Markdown

1# Transport: Linux Bluetooth sockets, SDP and BlueZ
2
3This 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
5All 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 (CLAUDE.md rule 1).
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
63The 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
87The 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):
1071. ServiceSearchPattern: a data-element sequence of UUIDs. budctl sends one 128-bit UUID: `35 11 1C <16 UUID bytes>`.
1082. MaximumAttributeByteCount (u16): `FF FF`. The server still splits a long answer with continuation state.
1093. AttributeIDList: a data-element sequence of attribute ids. budctl asks only for `0x0004`, the ProtocolDescriptorList: `35 03 09 00 04`.
1104. 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):
1141. AttributeListsByteCount (u16).
1152. AttributeLists: that many bytes. This is one part of a data element. Joined across all responses, it forms the complete data element.
1163. 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.