master ohmu/fastfetch-openbsd / src / detection / bluetooth / bluetooth_windows.cpp
ohmu  ·  2026-10-15
  1extern "C" {
  2#include "bluetooth.h"
  3#include "common/debug.h"
  4#include "common/mallocHelper.h"
  5#include "common/percent.h"
  6#include "common/strutil.h"
  7}
  8
  9#if FF_HAVE_WINRT
 10
 11    #include "common/windows/winrt.hpp"
 12
 13    #define INITGUID
 14    #include <windows.h>
 15    #include <cfgmgr32.h>
 16    #include <devpkey.h>
 17
 18    #include <winrt/Windows.Devices.Bluetooth.h>
 19    #include <winrt/Windows.Devices.Enumeration.h>
 20
 21// Same definition as the one in bluetooth_windows.c; both are `selectany` and identical, so the
 22// linker keeps either. It is not in the SDK headers, hence the hand-written GUID.
 23/* DEVPROP_TYPE_STRING */
 24DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_DeviceAddress, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a, 1);
 25
 26#define GUID_DEVCLASS_BLUETOOTH_STRING L"{e0cbf06c-cd8b-4647-bb8a-263b43f0f974}" // Found in <devguid.h>
 27
 28using FFBluetoothDeviceInformation = winrt::Windows::Devices::Enumeration::DeviceInformation;
 29using FFBluetoothDeviceInformationItf = abi_t<winrt::Windows::Devices::Enumeration::IDeviceInformation>;
 30using FFBluetoothDeviceList = abi_t<winrt::Windows::Foundation::Collections::IVectorView<FFBluetoothDeviceInformation>>;
 31using FFBluetoothProperties = abi_t<winrt::Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::Windows::Foundation::IInspectable>>;
 32using FFBluetoothPropertyValue = abi_t<winrt::Windows::Foundation::IPropertyValue>;
 33
 34// The AEP properties this file asks for. `FindAllAsync` rejects the whole call with
 35// `0x8002802B` (property key syntax error) if any name is not canonical, so every one of these was
 36// checked against the fast selector on a machine with paired LE devices; the ones that look
 37// plausible but do not exist (`...Bluetooth.Cod.Services`, `...Bluetooth.LastConnectedTime`,
 38// `...Bluetooth.Le.DeviceAddress`) are deliberately absent.
 39static const wchar_t* const ffBluetoothLeProperties[] = {
 40    L"System.Devices.Aep.IsConnected",
 41    L"System.Devices.Aep.SignalStrength",
 42    L"System.Devices.Aep.DeviceAddress",
 43    L"System.Devices.Aep.ContainerId",
 44    L"System.Devices.Aep.Bluetooth.Le.Appearance",
 45};
 46
 47// ---------------------------------------------------------------------------------------------
 48// Reading the AEP property store
 49// ---------------------------------------------------------------------------------------------
 50
 51// Every value in `DeviceInformation.Properties()` is boxed, and the one box that can be read back
 52// without knowing the concrete type is the property-value interface. So: ask the map whether the key
 53// is there, then ask the box for the type that key is documented to carry.
 54//
 55// "The device does not have it" is an ordinary answer rather than a failure -- an endpoint that has
 56// never been in range has no SignalStrength, and one whose firmware leaves the appearance at its
 57// default reports 0 instead of omitting the key. The caller owns the returned value.
 58static FFBluetoothPropertyValue* ffBluetoothLeLookup(FFBluetoothProperties* properties, const wchar_t* key) {
 59    HSTRING_HEADER header;
 60    HSTRING name;
 61    if (FAILED(WindowsCreateStringReference(key, (UINT32) ::wcslen(key), &header, &name))) {
 62        return nullptr;
 63    }
 64
 65    bool present = false; // `Lookup` on a missing key is an error, so ask first
 66    if (FAILED(properties->HasKey(reinterpret_cast<void*>(name), &present)) || !present) {
 67        return nullptr;
 68    }
 69
 70    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::IInspectable>* boxed = nullptr;
 71    if (FAILED(properties->Lookup(reinterpret_cast<void*>(name), reinterpret_cast<void**>(&boxed))) || !boxed) {
 72        return nullptr;
 73    }
 74
 75    FFBluetoothPropertyValue* value = nullptr;
 76    if (FAILED(ffQueryInterface<winrt::Windows::Foundation::IPropertyValue>(boxed, &value))) {
 77        return nullptr;
 78    }
 79
 80    return value;
 81}
 82
 83static bool ffBluetoothLeGetBool(FFBluetoothProperties* properties, const wchar_t* key, bool* result) {
 84    FF_AUTO_RELEASE_COM_OBJECT FFBluetoothPropertyValue* value = ffBluetoothLeLookup(properties, key);
 85    return value && SUCCEEDED(value->GetBoolean(result));
 86}
 87
 88static bool ffBluetoothLeGetInt32(FFBluetoothProperties* properties, const wchar_t* key, int32_t* result) {
 89    FF_AUTO_RELEASE_COM_OBJECT FFBluetoothPropertyValue* value = ffBluetoothLeLookup(properties, key);
 90    return value && SUCCEEDED(value->GetInt32(result));
 91}
 92
 93static bool ffBluetoothLeGetUInt16(FFBluetoothProperties* properties, const wchar_t* key, uint16_t* result) {
 94    FF_AUTO_RELEASE_COM_OBJECT FFBluetoothPropertyValue* value = ffBluetoothLeLookup(properties, key);
 95    return value && SUCCEEDED(value->GetUInt16(result));
 96}
 97
 98static bool ffBluetoothLeGetGuid(FFBluetoothProperties* properties, const wchar_t* key, GUID* result) {
 99    FF_AUTO_RELEASE_COM_OBJECT FFBluetoothPropertyValue* value = ffBluetoothLeLookup(properties, key);
100    // `winrt::guid` and `GUID` have the same four members in the same order, and C++/WinRT itself
101    // converts between them with exactly this cast.
102    return value && SUCCEEDED(value->GetGuid(reinterpret_cast<winrt::guid*>(result)));
103}
104
105// The caller owns `result`.
106static bool ffBluetoothLeGetString(FFBluetoothProperties* properties, const wchar_t* key, HSTRING* result) {
107    FF_AUTO_RELEASE_COM_OBJECT FFBluetoothPropertyValue* value = ffBluetoothLeLookup(properties, key);
108    return value && SUCCEEDED(value->GetString(reinterpret_cast<void**>(result)));
109}
110
111// ---------------------------------------------------------------------------------------------
112// The device tree: which addresses belong to the same physical device
113// ---------------------------------------------------------------------------------------------
114
115// A Bluetooth device is one container with one node per stack it speaks, and the two nodes carry
116// *different* addresses: the phone is FC:AB:D0:72:40:90 over BR/EDR and 47:E1:D7:21:C7:FC over LE,
117// both inside {B3F019F1-...}. The container is therefore the only identity the classic and the LE
118// enumerations can be joined on, and the device tree is where the classic side gets it from.
119typedef struct FFBluetoothNodeInfo {
120    char address[13]; // 12 hex digits + terminator, upper case, no separators
121    GUID container;
122} FFBluetoothNodeInfo;
123
124static void ffBluetoothCollectNodes(FFlist* nodes /* FFBluetoothNodeInfo */) {
125    ULONG idListLength = 0;
126    if (CM_Get_Device_ID_List_SizeW(&idListLength, GUID_DEVCLASS_BLUETOOTH_STRING, CM_GETIDLIST_FILTER_CLASS | CM_GETIDLIST_FILTER_PRESENT) != CR_SUCCESS || idListLength == 0) {
127        return;
128    }
129
130    FF_AUTO_FREE wchar_t* idList = (wchar_t*) malloc((size_t) idListLength * sizeof(wchar_t));
131    if (!idList) {
132        return;
133    }
134
135    if (CM_Get_Device_ID_ListW(GUID_DEVCLASS_BLUETOOTH_STRING, idList, idListLength, CM_GETIDLIST_FILTER_CLASS | CM_GETIDLIST_FILTER_PRESENT) != CR_SUCCESS) {
136        return;
137    }
138
139    for (const wchar_t* deviceId = idList; *deviceId; deviceId += wcslen(deviceId) + 1) {
140        DEVINST devInst = 0;
141        if (CM_Locate_DevNodeW(&devInst, (DEVINSTID_W) deviceId, CM_LOCATE_DEVNODE_NORMAL) != CR_SUCCESS) {
142            continue;
143        }
144
145        FFBluetoothNodeInfo node = {};
146
147        {
148            // Both enumerators carry this: BTHENUM nodes spell the address in upper case, BTHLE nodes
149            // in lower case, so it is folded here and the comparison later is plain byte equality.
150            wchar_t address[13];
151            DEVPROPTYPE type = DEVPROP_TYPE_EMPTY;
152            ULONG size = sizeof(address);
153            if (CM_Get_DevNode_PropertyW(devInst, &DEVPKEY_Bluetooth_DeviceAddress, &type, (PBYTE) address, &size, 0) != CR_SUCCESS || type != DEVPROP_TYPE_STRING || size != sizeof(address)) {
154                continue;
155            }
156
157            for (size_t i = 0; i < 12; ++i) {
158                node.address[i] = (char) toupper((unsigned char) address[i]);
159            }
160        }
161
162        {
163            DEVPROPTYPE type = DEVPROP_TYPE_EMPTY;
164            ULONG size = sizeof(node.container);
165            if (CM_Get_DevNode_PropertyW(devInst, &DEVPKEY_Device_ContainerId, &type, (PBYTE) &node.container, &size, 0) != CR_SUCCESS || type != DEVPROP_TYPE_GUID || size != sizeof(node.container)) {
166                continue;
167            }
168        }
169
170        *FF_LIST_ADD(FFBluetoothNodeInfo, *nodes) = node;
171    }
172}
173
174// `address` is the printed form ("AA:BB:CC:DD:EE:FF"); the device tree stores it without separators.
175static bool ffBluetoothFindContainer(const FFlist* nodes, const FFstrbuf* address, GUID* container) {
176    if (address->length != 17) {
177        return false;
178    }
179
180    char plain[13];
181    for (uint32_t i = 0; i < 6; ++i) {
182        plain[i * 2] = (char) toupper((unsigned char) address->chars[i * 3]);
183        plain[i * 2 + 1] = (char) toupper((unsigned char) address->chars[i * 3 + 1]);
184    }
185    plain[12] = '\0';
186
187    FF_LIST_FOR_EACH (FFBluetoothNodeInfo, node, *nodes) {
188        if (memcmp(node->address, plain, sizeof(plain)) == 0) {
189            *container = node->container;
190            return true;
191        }
192    }
193
194    return false;
195}
196
197// ---------------------------------------------------------------------------------------------
198// The appearance, and the device it describes
199// ---------------------------------------------------------------------------------------------
200
201// The GAP appearance is a 16-bit value whose upper 10 bits are the category. Windows names only the
202// first nineteen of them -- `BluetoothLEAppearanceCategories` stops at Cycling (0x12) and has no
203// AudioSink -- so these names are the Bluetooth SIG ones, checked against that class where it has an
204// entry and against `BluetoothLEAppearance::FromRawValue()` for the rest: a pair of earbuds reports
205// 0x0842, which the OS itself decodes as category 0x21, the value used below for Audio Sink.
206static const char* ffBluetoothLeAppearanceName(uint16_t appearance) {
207    switch (appearance >> 6) {
208        case 0x00: return "Unknown";
209        case 0x01: return "Phone";
210        case 0x02: return "Computer";
211        case 0x03: return "Watch";
212        case 0x04: return "Clock";
213        case 0x05: return "Display";
214        case 0x06: return "Remote Control";
215        case 0x07: return "Eye Glasses";
216        case 0x08: return "Tag";
217        case 0x09: return "Keyring";
218        case 0x0A: return "Media Player";
219        case 0x0B: return "Barcode Scanner";
220        case 0x0C: return "Thermometer";
221        case 0x0D: return "Heart Rate Sensor";
222        case 0x0E: return "Blood Pressure";
223        case 0x0F: return "Human Interface Device";
224        case 0x10: return "Glucose Meter";
225        case 0x11: return "Running Walking Sensor";
226        case 0x12: return "Cycling";
227        case 0x13: return "Control Device";
228        case 0x14: return "Network Device";
229        case 0x15: return "Sensor";
230        case 0x16: return "Light Fixture";
231        case 0x17: return "Fan";
232        case 0x18: return "HVAC";
233        case 0x19: return "Air Conditioning";
234        case 0x1A: return "Humidifier";
235        case 0x1B: return "Heating";
236        case 0x1C: return "Access Control";
237        case 0x1D: return "Motorized Device";
238        case 0x1E: return "Power Device";
239        case 0x1F: return "Light Source";
240        case 0x20: return "Window Covering";
241        case 0x21: return "Audio Sink";
242        case 0x22: return "Audio Source";
243        case 0x23: return "Motorized Vehicle";
244        case 0x24: return "Domestic Appliance";
245        case 0x25: return "Wearable Audio Device";
246        case 0x26: return "Aircraft";
247        case 0x27: return "AV Equipment";
248        case 0x28: return "Display Equipment";
249        case 0x29: return "Hearing Aid";
250        case 0x2A: return "Gaming";
251        case 0x2B: return "Signage";
252        default: return "Unknown";
253    }
254}
255
256// Which entry of `devices` each classic device occupies, and which container it belongs to. The
257// index is stored rather than a pointer because appending an LE-only device can move the list.
258typedef struct FFBluetoothClassicEntry {
259    uint32_t index;
260    bool hasContainer;
261    GUID container;
262} FFBluetoothClassicEntry;
263
264// ---------------------------------------------------------------------------------------------
265// The LE stack
266// ---------------------------------------------------------------------------------------------
267
268// `devices` already holds everything the classic (BR/EDR) stack knows. This adds what only the LE
269// stack knows: devices that never speak BR/EDR at all, and -- for the ones that speak both -- the
270// fact that they do, plus the signal strength the LE link is the only source of.
271//
272// The way in matters. `BluetoothLEDevice::GetDeviceSelectorFromPairingState(true)` as an AQS filter
273// answers in 9-22 ms and brings the AEP properties along, whereas enumerating
274// `DeviceInformationKind::AssociationEndpoint` returns the same data but costs about 61 seconds per
275// call because it scans the radio. The 7.7-second trap is one layer further out: any `Uncached` GATT
276// call, which the cache-backed service table hides by returning in a few milliseconds.
277extern "C" const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
278    const char* error = ffInitCom();
279    if (error) {
280        return error;
281    }
282
283    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Devices::Bluetooth::IBluetoothLEDeviceStatics2>* ledeviceStatics = nullptr;
284    HRESULT ledeviceHr = ffGetActivationFactory<winrt::Windows::Devices::Bluetooth::IBluetoothLEDeviceStatics2>(L"Windows.Devices.Bluetooth.BluetoothLEDevice", &ledeviceStatics);
285    if (FAILED(ledeviceHr) || !ledeviceStatics) {
286        FF_DEBUG("winrt: RoGetActivationFactory(BluetoothLEDevice) failed: %s", ffDebugHResult(ledeviceHr));
287        return "winrt: RoGetActivationFactory(BluetoothLEDevice) failed";
288    }
289
290    [[gnu::cleanup(ffDeleteHstring)]] HSTRING selector = nullptr;
291    HRESULT selectorHr = ledeviceStatics->GetDeviceSelectorFromPairingState(true, reinterpret_cast<void**>(&selector));
292    if (FAILED(selectorHr) || !selector) {
293        FF_DEBUG("winrt: GetDeviceSelectorFromPairingState() failed: %s", ffDebugHResult(selectorHr));
294        return "winrt: GetDeviceSelectorFromPairingState() failed";
295    }
296
297    // Without additional properties the map carries only the eight generic ones, and every AEP key
298    // is absent, so the list is not optional.
299    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>* properties = nullptr;
300    HRESULT propertiesHr = ffWinrtCreateHstringIterable(ffBluetoothLeProperties, (uint32_t) ARRAY_SIZE(ffBluetoothLeProperties), &properties);
301    if (FAILED(propertiesHr) || !properties) {
302        FF_DEBUG("winrt: building the additional-properties list failed: %s", ffDebugHResult(propertiesHr));
303        return "winrt: building the additional-properties list failed";
304    }
305
306    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Devices::Enumeration::IDeviceInformationStatics>* enumerationStatics = nullptr;
307    HRESULT deviceInfoHr = ffGetActivationFactory<winrt::Windows::Devices::Enumeration::IDeviceInformationStatics>(L"Windows.Devices.Enumeration.DeviceInformation", &enumerationStatics);
308    if (FAILED(deviceInfoHr) || !enumerationStatics) {
309        FF_DEBUG("winrt: RoGetActivationFactory(DeviceInformation) failed: %s", ffDebugHResult(deviceInfoHr));
310        return "winrt: RoGetActivationFactory(DeviceInformation) failed";
311    }
312
313    FF_AUTO_RELEASE_COM_OBJECT FFBluetoothDeviceList* found = nullptr;
314    HRESULT hr = ffRunAndWait<winrt::Windows::Foundation::Collections::IVectorView<FFBluetoothDeviceInformation>>([&](void** result) {
315        return enumerationStatics->FindAllAsyncAqsFilterAndAdditionalProperties(reinterpret_cast<void*>(selector), reinterpret_cast<void*>(properties), result);
316    },
317        &found);
318    if (FAILED(hr) || !found) {
319        FF_DEBUG("winrt: DeviceInformation::FindAllAsync() failed: %s", ffDebugHResult(hr));
320        return "winrt: DeviceInformation::FindAllAsync() failed";
321    }
322
323    uint32_t count = 0;
324    if (FAILED(found->get_Size(&count)) || count == 0) {
325        return nullptr;
326    }
327
328    FF_LIST_AUTO_DESTROY nodes = ffListCreate();
329    ffBluetoothCollectNodes(&nodes);
330
331    FF_LIST_AUTO_DESTROY classic = ffListCreate();
332    {
333        uint32_t index = 0;
334        FF_LIST_FOR_EACH (FFBluetoothResult, device, *devices) {
335            FFBluetoothClassicEntry* entry = FF_LIST_ADD(FFBluetoothClassicEntry, classic);
336            entry->index = index++;
337            entry->hasContainer = ffBluetoothFindContainer(&nodes, &device->address, &entry->container);
338        }
339    }
340
341    for (uint32_t i = 0; i < count; ++i) {
342        FF_AUTO_RELEASE_COM_OBJECT FFBluetoothDeviceInformationItf* info = nullptr;
343        if (FAILED(found->GetAt(i, reinterpret_cast<void**>(&info))) || !info) {
344            continue;
345        }
346
347        FF_AUTO_RELEASE_COM_OBJECT FFBluetoothProperties* map = nullptr;
348        if (FAILED(info->get_Properties(reinterpret_cast<void**>(&map))) || !map) {
349            continue;
350        }
351
352        GUID container = {};
353        if (!ffBluetoothLeGetGuid(map, L"System.Devices.Aep.ContainerId", &container)) {
354            continue; // Without a container there is no way to tell whether this is a device we know
355        }
356
357        bool connected = false;
358        ffBluetoothLeGetBool(map, L"System.Devices.Aep.IsConnected", &connected);
359
360        FFBluetoothResult* target = nullptr;
361        FF_LIST_FOR_EACH (FFBluetoothClassicEntry, entry, classic) {
362            if (entry->hasContainer && IsEqualGUID(entry->container, container)) {
363                target = FF_LIST_GET(FFBluetoothResult, *devices, entry->index);
364                break;
365            }
366        }
367
368        if (!target && !connected && !options->showDisconnected) {
369            continue; // An LE-only device nobody asked to see. A known one is already in the list.
370        }
371
372        if (!target) {
373            target = FF_LIST_ADD(FFBluetoothResult, *devices);
374            ffStrbufInit(&target->name);
375            ffStrbufInit(&target->address);
376            ffStrbufInit(&target->type);
377            target->deviceType = FF_BLUETOOTH_DEVICE_TYPE_NONE;
378            target->battery = 0;
379            target->signalQuality = -DBL_MAX;
380            target->connected = false;
381        }
382
383        target->deviceType = (FFBluetoothDeviceType) (target->deviceType | FF_BLUETOOTH_DEVICE_TYPE_LE_BIT);
384
385        // The classic half wins wherever it has an answer: its name and address are the ones the
386        // Settings app shows, and its type comes from the class of device rather than from an
387        // appearance value the firmware may never have set.
388        if (target->name.length == 0) {
389            [[gnu::cleanup(ffDeleteHstring)]] HSTRING name = nullptr;
390            if (SUCCEEDED(info->get_Name(reinterpret_cast<void**>(&name))) && name) {
391                ffStrbufSetHstring(&target->name, name);
392            }
393            ffStrbufTrimSpace(&target->name); // A device whose name is missing is reported as a space
394        }
395        if (target->name.length == 0) {
396            ffStrbufSetStatic(&target->name, "Unknown Device");
397        }
398
399        if (target->address.length == 0) {
400            [[gnu::cleanup(ffDeleteHstring)]] HSTRING address = nullptr;
401            if (ffBluetoothLeGetString(map, L"System.Devices.Aep.DeviceAddress", &address)) {
402                ffStrbufSetHstring(&target->address, address);
403                ffStrbufUpperCase(&target->address);
404            }
405        }
406
407        if (target->type.length == 0) {
408            uint16_t appearance = 0;
409            if (ffBluetoothLeGetUInt16(map, L"System.Devices.Aep.Bluetooth.Le.Appearance", &appearance)) {
410                ffStrbufSetStatic(&target->type, ffBluetoothLeAppearanceName(appearance));
411            }
412        }
413
414        // The signal strength is what the LE stack last saw, and it stays readable while the link is
415        // down, so it is reported whenever the endpoint has one. Only the LE side can produce it.
416        int32_t signalStrength = 0;
417        if (ffBluetoothLeGetInt32(map, L"System.Devices.Aep.SignalStrength", &signalStrength)) {
418            target->signalQuality = ffRssiToSignalQuality(signalStrength);
419        }
420
421        // A dual-mode device is connected if either of its two links is up, and the two can disagree.
422        target->connected |= connected;
423    }
424
425    return nullptr;
426}
427
428#else
429
430extern "C" const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
431    FF_UNUSED(options, devices);
432    return nullptr;
433}
434
435#endif // FF_HAVE_WINRT