master ohmu/fastfetch-openbsd / src / detection / bluetooth / bluetooth_linux.c
ohmu  ·  2026-10-15
  1#include "bluetooth.h"
  2#include "common/debug.h"
  3#include "common/percent.h"
  4#include "common/strutil.h"
  5
  6#ifdef FF_HAVE_DBUS
  7    #include "common/dbus.h"
  8    #include "common/io.h"
  9
 10/* Example dbus reply, striped to only the relevant parts:
 11array [                                                     //root
 12    dict entry(                                             //object
 13        object path "/org/bluez/hci0/dev_03_21_8B_91_16_4D"
 14        array [
 15           dict entry(                                      //property
 16              string "org.bluez.Device1"
 17              array [
 18                 dict entry(                                //value
 19                    string "Address"
 20                    variant string "03:21:8B:91:16:4D"
 21                 )
 22                 dict entry(                                //value
 23                    string "Name"
 24                    variant string "JBL TUNE160BT"
 25                 )
 26                 dict entry(                                //value
 27                    string "Icon"
 28                    variant string "audio-headset"
 29                 )
 30                 dict entry(                                //value
 31                    string "Connected"
 32                    variant boolean true
 33                 )
 34                 dict entry(                                //value
 35                    string "RSSI"
 36                    variant int16 -63
 37                 )
 38              ]
 39           )
 40           dict entry(                                      //property
 41              string "org.bluez.Battery1"
 42              array [
 43                 dict entry(                                //value
 44                    string "Percentage"
 45                    variant byte 100
 46                 )
 47              ]
 48           )
 49        ]
 50    )
 51]
 52*/
 53
 54/* How the two stack bits are inferred here, and why the LE one is mostly unattainable.
 55 *
 56 * `org.bluez.Device1` has no "does this device support LE" property, so both bits are read off
 57 * the only two stack-specific ones it has:
 58 *
 59 *   Class      - BR/EDR Class of Device, learned from BR/EDR EIR / inquiry data
 60 *                (BlueZ src/eir.c, EIR_CLASS_OF_DEV) and persisted in the device file.
 61 *   Appearance - GATT appearance, learned from LE advertising data only
 62 *                (BlueZ src/adapter.c -> src/eir.c, EIR_GAP_APPEARANCE, AD type 0x19).
 63 *
 64 * For a dual-mode device only the first one ever shows up, for two independent reasons:
 65 *
 66 * 1. BlueZ never publishes `Appearance` for a device that has a `Class`. Its getter short
 67 *    circuits on it, and the property table registers that getter as the `exists` callback,
 68 *    so the key is absent from `GetManagedObjects` altogether rather than present with a
 69 *    default value:
 70 *
 71 *        static gboolean get_appearance(...)
 72 *        {
 73 *            if (dev_property_exists_class(property, data))   // device->class != 0
 74 *                return FALSE;
 75 *            if (device->appearance) { ... return TRUE; }
 76 *            return FALSE;
 77 *        }
 78 *
 79 *    `btd_device_get_icon()` prefers `Class` the same way, which is why `Icon` describes a
 80 *    classic-seen device by its CoD. BlueZ treats the two as mutually exclusive descriptions
 81 *    of the device's *category*, not as a record of which radios it speaks. Checked against
 82 *    BlueZ 5.87, src/device.c.
 83 *
 84 * 2. Even without (1) the value is usually unknown: it is only learned if the device
 85 *    advertised AD type 0x19 while we were scanning (phones rarely do), a zero is never
 86 *    persisted (`/var/lib/bluetooth/<adapter>/<addr>/info` then has no `Appearance=` line),
 87 *    and that directory is mode 0700 so it could not be read here anyway.
 88 *
 89 * Consequence: a device ever seen over BR/EDR -- every paired phone, since all of them have a
 90 * CoD -- can only ever be reported as Classic, however LE capable it really is, and no amount
 91 * of extra data collected on this path can change that. Windows gets it right because it
 92 * enumerates the two stacks separately (bluetooth_windows.cpp walks the WinRT
 93 * BluetoothLEDevice endpoints and ORs in the LE bit); BlueZ has no equivalent enumeration --
 94 * one Device1 object per address, no per-stack capability -- and its own storage only records
 95 * `SupportedTechnologies=BR/EDR;LE` once a real LE connection or an LE sighting has happened.
 96 *
 97 * Signals that look like LE proof but are not, and are deliberately not used here:
 98 *   - GAP / GATT in `UUIDs` (0x1800 / 0x1801): BlueZ mirrors BR/EDR SDP records advertising
 99 *     the ATT bearer (L2CAP PSM 0x001F) into the same `attributes` cache it loads GATT
100 *     primaries from, so those UUIDs can come from a purely classic SDP browse.
101 *   - `AddressType`: a BR/EDR-only record reports "public" too -- `device->bdaddr_type` is
102 *     forced to `BDADDR_BREDR` when the record is loaded.
103 *   - `ManufacturerData` / `ServiceData`: BlueZ parses them out of BR/EDR EIR as well.
104 *
105 * The one durable LE-only signal BlueZ does publish, if this is ever revisited:
106 *   - `AdvertisingFlags` exists once the device has been seen in an LE advertising report.
107 *     It is written only from the `bdaddr_type != BDADDR_BREDR` branch of BlueZ's device-found
108 *     handler, starts out invalid (0xff) and is never cleared, so it survives the end of a
109 *     discovery session -- but it is not persisted, so a bluetoothd restart loses it again.
110 *     `PreferredBearer` would be even better (it exists exactly when both bearers are known),
111 *     yet it is flagged experimental and needs `Experimental=true` to be exported at all.
112 */
113
114static bool detectBluetoothValue(FFDBusData* dbus, DBusMessageIter* iter, FFBluetoothResult* device) {
115    if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) {
116        return true;
117    }
118
119    DBusMessageIter dictIter;
120    dbus->lib->ffdbus_message_iter_recurse(iter, &dictIter);
121
122    if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_STRING) {
123        return true;
124    }
125
126    const char* deviceProperty;
127    dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &deviceProperty);
128
129    dbus->lib->ffdbus_message_iter_next(&dictIter);
130
131    if (ffStrEquals(deviceProperty, "Address")) {
132        ffDBusGetString(dbus, &dictIter, &device->address);
133    } else if (ffStrEquals(deviceProperty, "Name")) {
134        ffDBusGetString(dbus, &dictIter, &device->name);
135    } else if (ffStrEquals(deviceProperty, "Icon")) {
136        // BlueZ derives this from the Class of Device whenever there is one and only falls back
137        // to the LE appearance otherwise, so `type` describes a classic-seen device by its CoD.
138        ffDBusGetString(dbus, &dictIter, &device->type);
139    } else if (ffStrEquals(deviceProperty, "Class")) {
140        // The BR/EDR Class of Device. Published iff BlueZ ever learned a CoD, which comes from
141        // BR/EDR EIR / inquiry data, so this means "seen over BR/EDR" -- not "classic only".
142        // Its mere presence is also what hides `Appearance` in the branch below.
143        device->deviceType |= FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT;
144    } else if (ffStrEquals(deviceProperty, "Appearance")) {
145        // The LE-only GATT appearance. Absence does *not* mean "not LE": BlueZ omits this
146        // property entirely for every device that has a `Class` (see the block comment above),
147        // and knows the value only if the device advertised AD type 0x19 while we were scanning.
148        // The two properties are mutually exclusive in BlueZ's view, so a dual-mode phone that
149        // was ever seen classically reports Classic only, whatever it is capable of.
150        device->deviceType |= FF_BLUETOOTH_DEVICE_TYPE_LE_BIT;
151    } else if (ffStrEquals(deviceProperty, "Percentage")) {
152        uint64_t percentage;
153        if (ffDBusGetUint(dbus, &dictIter, &percentage)) {
154            device->battery = (uint8_t) percentage;
155        }
156    } else if (ffStrEquals(deviceProperty, "Connected")) {
157        ffDBusGetBool(dbus, &dictIter, &device->connected);
158    } else if (ffStrEquals(deviceProperty, "RSSI")) {
159        // `int16` on the wire, in dBm. Written only from discovery reports and invalidated when
160        // the discovery session ends, so it says nothing about the connection state: a connected
161        // device that has not been scanned lately reports no RSSI at all.
162        int64_t rssi;
163        if (ffDBusGetInt(dbus, &dictIter, &rssi)) {
164            device->signalQuality = ffRssiToSignalQuality((int) rssi);
165        }
166    } else if (ffStrEquals(deviceProperty, "Paired")) {
167        bool paired = true;
168        ffDBusGetBool(dbus, &dictIter, &paired);
169        if (!paired) {
170            return false;
171        }
172    }
173    return true;
174}
175
176static void detectBluetoothProperty(FFDBusData* dbus, DBusMessageIter* iter, FFBluetoothResult* device) {
177    if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) {
178        return;
179    }
180
181    DBusMessageIter dictIter;
182    dbus->lib->ffdbus_message_iter_recurse(iter, &dictIter);
183
184    if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_STRING) {
185        return;
186    }
187
188    const char* propertyType;
189    dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &propertyType);
190
191    if (!ffStrContains(propertyType, ".Device") && !ffStrContains(propertyType, ".Battery")) {
192        return; // We don't care about other properties
193    }
194
195    dbus->lib->ffdbus_message_iter_next(&dictIter);
196
197    if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_ARRAY) {
198        return;
199    }
200
201    DBusMessageIter arrayIter;
202    dbus->lib->ffdbus_message_iter_recurse(&dictIter, &arrayIter);
203
204    do {
205        bool shouldContinue = detectBluetoothValue(dbus, &arrayIter, device);
206        if (!shouldContinue) {
207            ffStrbufClear(&device->name);
208            break;
209        }
210    } while (dbus->lib->ffdbus_message_iter_next(&arrayIter));
211}
212
213static FFBluetoothResult* detectBluetoothObject(FFlist* devices, FFDBusData* dbus, DBusMessageIter* iter) {
214    if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) {
215        return nullptr;
216    }
217
218    DBusMessageIter dictIter;
219    dbus->lib->ffdbus_message_iter_recurse(iter, &dictIter);
220
221    if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_OBJECT_PATH) {
222        return nullptr;
223    }
224
225    const char* objectPath;
226    dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &objectPath);
227
228    // We don't want adapter objects
229    if (!ffStrContains(objectPath, "/dev_")) {
230        return nullptr;
231    }
232
233    dbus->lib->ffdbus_message_iter_next(&dictIter);
234
235    if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_ARRAY) {
236        return nullptr;
237    }
238
239    DBusMessageIter arrayIter;
240    dbus->lib->ffdbus_message_iter_recurse(&dictIter, &arrayIter);
241
242    FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices);
243    ffStrbufInit(&device->name);
244    ffStrbufInit(&device->address);
245    ffStrbufInit(&device->type);
246    device->deviceType = FF_BLUETOOTH_DEVICE_TYPE_NONE;
247    device->battery = 0;
248    device->signalQuality = -DBL_MAX;
249    device->connected = false;
250
251    do {
252        detectBluetoothProperty(dbus, &arrayIter, device);
253    } while (dbus->lib->ffdbus_message_iter_next(&arrayIter));
254
255    return device;
256}
257
258static void detectBluetoothRoot(FFBluetoothOptions* options, FFlist* devices, FFDBusData* dbus, DBusMessageIter* iter, int32_t connectedCount) {
259    if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) {
260        return;
261    }
262
263    DBusMessageIter arrayIter;
264    dbus->lib->ffdbus_message_iter_recurse(iter, &arrayIter);
265
266    do {
267        FFBluetoothResult* device = detectBluetoothObject(devices, dbus, &arrayIter);
268
269        if (device) {
270            // `showType` selects which stacks are reported. Unlike the other backends there is no
271            // per-stack call to skip here: both bits are read off this one walk of Device1, so the
272            // filter lands on the result instead. That filter is necessarily one sided -- see the
273            // block comment above detectBluetoothValue(): `Class` and `Appearance` are mutually
274            // exclusive in BlueZ, so a dual-mode device that was ever seen classically carries only
275            // the classic bit, and `showType: le` drops it even though it does speak LE. A device
276            // whose stack could not be determined at all is kept -- BlueZ promises neither property,
277            // and dropping those would lose devices the default configuration reports.
278            bool deviceTypeWanted = device->deviceType == FF_BLUETOOTH_DEVICE_TYPE_NONE || (device->deviceType & options->showType);
279
280            if (!deviceTypeWanted || (!options->showDisconnected && !device->connected)) {
281                ffStrbufDestroy(&device->name);
282                ffStrbufDestroy(&device->address);
283                ffStrbufDestroy(&device->type);
284                --devices->length;
285                continue;
286            }
287
288            if (device->name.length == 0) {
289                ffStrbufSetStatic(&device->name, "Unknown Device");
290            }
291
292            if (device->connected && --connectedCount == 0) {
293                break;
294            }
295        }
296    } while (dbus->lib->ffdbus_message_iter_next(&arrayIter));
297}
298
299static const char* detectBluetooth(FFBluetoothOptions* options, FFlist* devices, int32_t connectedCount) {
300    FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {};
301    const char* error = ffDBusLoadData(DBUS_BUS_SYSTEM, &dbus);
302    if (error) {
303        return error;
304    }
305
306    DBusMessage* managedObjects = ffDBusGetMethodReply(&dbus, "org.bluez", "/", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", nullptr, nullptr);
307    if (!managedObjects) {
308        FF_DEBUG("Failed to call GetManagedObjects");
309        return "Failed to call GetManagedObjects";
310    }
311
312    DBusMessageIter rootIter;
313    if (!dbus.lib->ffdbus_message_iter_init(managedObjects, &rootIter)) {
314        FF_DEBUG("Failed to get root iterator of GetManagedObjects");
315        dbus.lib->ffdbus_message_unref(managedObjects);
316        return "Failed to get root iterator of GetManagedObjects";
317    }
318
319    detectBluetoothRoot(options, devices, &dbus, &rootIter, connectedCount);
320
321    dbus.lib->ffdbus_message_unref(managedObjects);
322    return nullptr;
323}
324
325static uint32_t connectedDevices(void) {
326    FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/bluetooth");
327    if (dirp == nullptr) {
328        return 0;
329    }
330
331    uint32_t result = 0;
332    struct dirent* entry;
333    while ((entry = readdir(dirp)) != nullptr) {
334        if (strchr(entry->d_name, ':') != nullptr) {
335            ++result;
336        }
337    }
338
339    return result;
340}
341
342#endif
343
344const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
345#ifdef FF_HAVE_DBUS
346    int32_t connectedCount = -1;
347    if (!options->showDisconnected) {
348        connectedCount = (int32_t) connectedDevices();
349        if (connectedCount == 0) {
350            return nullptr;
351        }
352    }
353
354    return detectBluetooth(options, devices, connectedCount);
355#else
356    FF_UNUSED(options, devices);
357    FF_DEBUG("Fastfetch was compiled without DBus support");
358    return "Fastfetch was compiled without DBus support";
359#endif
360}