master ohmu/fastfetch-openbsd / src / detection / bluetooth / bluetooth_windows.c
ohmu  ·  2026-10-15
  1#include "bluetooth.h"
  2#include "common/debug.h"
  3#include "common/library.h"
  4#include "common/mallocHelper.h"
  5#include "common/windows/unicode.h"
  6
  7#define INITGUID
  8#include <windows.h>
  9#include <bluetoothapis.h>
 10#include <cfgmgr32.h>
 11#include <devpkey.h>
 12
 13#pragma GCC diagnostic ignored "-Wpointer-sign"
 14
 15// https://github.com/wine-mirror/wine/blob/ab6f4584b89f28504b0b277c0b4c723a86b4d6b7/include/ddk/bthguid.h#L4
 16/* DEVPROP_TYPE_STRING */
 17DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_DeviceAddress, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a, 1);
 18/* DEVPROP_TYPE_UINT32 */
 19DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_ClassOfDevice, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a, 10);
 20/* DEVPROP_TYPE_FILETIME */
 21DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_LastConnectedTime, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a, 11);
 22/* DEVPROP_TYPE_GUID */
 23DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_ServiceGUID, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a, 2);
 24/* DEVPROP_TYPE_UINT8 */
 25DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_BatteryLevel, 0x104ea319, 0x6ee2, 0x4701, 0xbd, 0x47, 0x8d, 0xdb, 0xf4, 0x25, 0xbb, 0xe5, 2);
 26
 27// Windows keeps the classic (BR/EDR) and the Low Energy stacks apart, and so does fastfetch: the
 28// device search below only ever returns BR/EDR devices, and the LE half lives in
 29// bluetooth_windows.cpp because reaching it means WinRT, which is C++ only. This is the bridge --
 30// it walks the LE stack, then folds what it finds into the list the classic half already built.
 31const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */);
 32
 33// The device tree cannot answer "is this connected". DEVPKEY_DeviceContainer_IsConnected and
 34// IsPaired are absent from every present devnode, and CM_Get_DevNode_PropertyW answers
 35// CR_NO_SUCH_VALUE for PKEY_Devices_Aep_IsConnected. The value does exist, but only in the
 36// Association Endpoint property store, which is reachable through WinRT alone -- see
 37// ffBluetoothDetectLe() and the note there about the cost of each way in.
 38#define GUID_DEVCLASS_BLUETOOTH_STRING L"{e0cbf06c-cd8b-4647-bb8a-263b43f0f974}" // Found in <devguid.h>
 39#define GUID_DEVCLASS_MEDIA_STRING L"{4d36e96c-e325-11ce-bfc1-08002be10318}"     // Found in <devguid.h>
 40
 41// The device tree spells an address without separators, while the list holds the printed form. The
 42// case has to be folded because it depends on the enumerator: the BTHENUM nodes are upper case and
 43// the BTHLE / BTHLEDevice ones lower case, and the list is always upper case.
 44static bool addressEquals(const wchar_t* plain, const FFstrbuf* address) {
 45    if (address->length != 17) {
 46        return false;
 47    }
 48
 49    for (uint32_t i = 0; i < 6; ++i) {
 50        if (toupper((unsigned char) plain[i * 2]) != toupper((unsigned char) address->chars[i * 3]) ||
 51            toupper((unsigned char) plain[i * 2 + 1]) != toupper((unsigned char) address->chars[i * 3 + 1])) {
 52            return false;
 53        }
 54    }
 55
 56    return true;
 57}
 58
 59static const char* detectBattery(FFlist* devices) {
 60    ULONG idListLength = 0;
 61    // The class filter is a no-op here, and deliberately so: CM_Get_Device_ID_ListW() only honours the
 62    // filter string together with CM_GETIDLIST_FILTER_CLASS, so this walks every present node of the
 63    // tree rather than the media class. That is what finds the level -- the node carrying
 64    // DEVPKEY_Bluetooth_BatteryLevel is a BTHENUM service node (the Hands-Free profile one), which is
 65    // not in the media class at all, so narrowing the filter would lose it.
 66    CONFIGRET status = CM_Get_Device_ID_List_SizeW(&idListLength, GUID_DEVCLASS_MEDIA_STRING, CM_GETIDLIST_FILTER_PRESENT);
 67    if (status != CR_SUCCESS) {
 68        FF_DEBUG("CM_Get_Device_ID_List_SizeW failed: %s", ffDebugConfigRet(status));
 69        return "CM_Get_Device_ID_List_SizeW failed";
 70    }
 71
 72    if (idListLength == 0) {
 73        return nullptr;
 74    }
 75
 76    FF_AUTO_FREE wchar_t* idList = (wchar_t*) malloc((size_t) idListLength * sizeof(wchar_t));
 77    if (!idList) {
 78        FF_DEBUG("malloc() failed");
 79        return "malloc() failed";
 80    }
 81
 82    status = CM_Get_Device_ID_ListW(GUID_DEVCLASS_MEDIA_STRING, idList, idListLength, CM_GETIDLIST_FILTER_PRESENT);
 83    if (status != CR_SUCCESS) {
 84        FF_DEBUG("CM_Get_Device_ID_ListW failed: %s", ffDebugConfigRet(status));
 85        return "CM_Get_Device_ID_ListW failed";
 86    }
 87
 88    for (const wchar_t* deviceId = idList; *deviceId; deviceId += wcslen(deviceId) + 1) {
 89        DEVINST devInst = 0;
 90
 91        if (CM_Locate_DevNodeW(&devInst, (DEVINSTID_W) deviceId, CM_LOCATE_DEVNODE_NORMAL) != CR_SUCCESS) {
 92            continue;
 93        }
 94
 95        uint8_t battery = 0;
 96        {
 97            DEVPROPTYPE devPropertyType = DEVPROP_TYPE_EMPTY;
 98            ULONG propertySize = sizeof(battery);
 99            if (CM_Get_DevNode_PropertyW(devInst, &DEVPKEY_Bluetooth_BatteryLevel, &devPropertyType, (PBYTE) &battery, &propertySize, 0) != CR_SUCCESS || devPropertyType != DEVPROP_TYPE_BYTE || propertySize != sizeof(battery)) {
100                continue;
101            }
102        }
103
104        WCHAR deviceAddress[13]; // 6 bytes in hex + null terminator
105        {
106            DEVPROPTYPE devPropertyType = DEVPROP_TYPE_EMPTY;
107            ULONG propertySize = sizeof(deviceAddress);
108            if (CM_Get_DevNode_PropertyW(devInst, &DEVPKEY_Bluetooth_DeviceAddress, &devPropertyType, (PBYTE) deviceAddress, &propertySize, 0) != CR_SUCCESS || devPropertyType != DEVPROP_TYPE_STRING || propertySize != sizeof(deviceAddress)) {
109                continue;
110            }
111        }
112
113        FF_LIST_FOR_EACH (FFBluetoothResult, bt, *devices) {
114            if (addressEquals(deviceAddress, &bt->address)) {
115                bt->battery = battery;
116                break;
117            }
118        }
119    }
120
121    return nullptr;
122}
123
124// The classic (BR/EDR) half. `BluetoothFindFirstDevice` only ever answers with BR/EDR devices; the
125// Low Energy stack is a separate walk, in bluetooth_windows.cpp, which folds what it finds into the
126// list this function builds.
127static const char* detectClassic(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
128    FF_LIBRARY_LOAD_MESSAGE(bluetoothapis, "bluetoothapis.dll", 1)
129    FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindFirstDevice)
130    FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindNextDevice)
131    FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindDeviceClose)
132
133    BLUETOOTH_DEVICE_INFO btdi = {
134        .dwSize = sizeof(btdi)
135    };
136    HBLUETOOTH_DEVICE_FIND hFind = ffBluetoothFindFirstDevice(&(BLUETOOTH_DEVICE_SEARCH_PARAMS) {
137                                                                  .fReturnConnected = TRUE,
138                                                                  .fReturnRemembered = options->showDisconnected,
139                                                                  .fReturnAuthenticated = options->showDisconnected,
140                                                                  .dwSize = sizeof(BLUETOOTH_DEVICE_SEARCH_PARAMS) },
141        &btdi);
142    if (!hFind) {
143        if (GetLastError() != ERROR_NO_MORE_ITEMS) {
144            FF_DEBUG("BluetoothFindFirstDevice() failed: %s", ffDebugWin32Error(GetLastError()));
145            return "BluetoothFindFirstDevice() failed";
146        }
147        // "Nothing found" is not an error here: this search only ever sees BR/EDR devices, and a
148        // machine whose only peripheral is a Low Energy one reaches this branch with a working
149        // radio. The LE half below is what finds that device, so falling through is the point.
150    } else {
151        do {
152            if (!options->showDisconnected && !btdi.fConnected) {
153                continue;
154            }
155
156            FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices);
157            ffStrbufInitWS(&device->name, btdi.szName);
158            ffStrbufInitF(&device->address, "%02X:%02X:%02X:%02X:%02X:%02X", btdi.Address.rgBytes[5], btdi.Address.rgBytes[4], btdi.Address.rgBytes[3], btdi.Address.rgBytes[2], btdi.Address.rgBytes[1], btdi.Address.rgBytes[0]);
159            ffStrbufInit(&device->type);
160            device->deviceType = FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT;
161            device->battery = 0;
162            device->signalQuality = -DBL_MAX; // Only the LE stack can report a signal strength
163            device->connected = !!btdi.fConnected;
164
165            // https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Assigned%20Numbers.pdf
166
167            if (BitTest(&btdi.ulClassofDevice, 13)) {
168                ffStrbufAppendS(&device->type, "Limited Discoverable Mode, ");
169            }
170            if (BitTest(&btdi.ulClassofDevice, 14)) {
171                ffStrbufAppendS(&device->type, "LE audio, ");
172            }
173            if (BitTest(&btdi.ulClassofDevice, 15)) {
174                ffStrbufAppendS(&device->type, "Reserved for future use, ");
175            }
176            if (BitTest(&btdi.ulClassofDevice, 16)) {
177                ffStrbufAppendS(&device->type, "Positioning, ");
178            }
179            if (BitTest(&btdi.ulClassofDevice, 17)) {
180                ffStrbufAppendS(&device->type, "Networking, ");
181            }
182            if (BitTest(&btdi.ulClassofDevice, 18)) {
183                ffStrbufAppendS(&device->type, "Rendering, ");
184            }
185            if (BitTest(&btdi.ulClassofDevice, 19)) {
186                ffStrbufAppendS(&device->type, "Capturing, ");
187            }
188            if (BitTest(&btdi.ulClassofDevice, 20)) {
189                ffStrbufAppendS(&device->type, "Object Transfer, ");
190            }
191            if (BitTest(&btdi.ulClassofDevice, 21)) {
192                ffStrbufAppendS(&device->type, "Audio, ");
193            }
194            if (BitTest(&btdi.ulClassofDevice, 22)) {
195                ffStrbufAppendS(&device->type, "Telephony, ");
196            }
197            if (BitTest(&btdi.ulClassofDevice, 23)) {
198                ffStrbufAppendS(&device->type, "Information, ");
199            }
200
201            if (device->type.length == 0) {
202                uint32_t majorDeviceClasses = (btdi.ulClassofDevice >> 8) & ~(UINT32_MAX << 5);
203                switch (majorDeviceClasses) {
204                    case 0b00000:
205                        ffStrbufAppendS(&device->type, "Miscellaneous");
206                        break;
207                    case 0b00001:
208                        ffStrbufAppendS(&device->type, "Computer");
209                        break;
210                    case 0b00010:
211                        ffStrbufAppendS(&device->type, "Phone");
212                        break;
213                    case 0b00011:
214                        ffStrbufAppendS(&device->type, "LAN/Network Access point");
215                        break;
216                    case 0b00100:
217                        ffStrbufAppendS(&device->type, "Audio/Video");
218                        break;
219                    case 0b00101:
220                        ffStrbufAppendS(&device->type, "Peripheral");
221                        break;
222                    case 0b00110:
223                        ffStrbufAppendS(&device->type, "Imaging");
224                        break;
225                    case 0b00111:
226                        ffStrbufAppendS(&device->type, "Wearable");
227                        break;
228                    case 0b01000:
229                        ffStrbufAppendS(&device->type, "Toy");
230                        break;
231                    case 0b01001:
232                        ffStrbufAppendS(&device->type, "Health");
233                        break;
234                    case 0b11111:
235                        ffStrbufAppendS(&device->type, "Uncategorized");
236                        break;
237                    default:
238                        ffStrbufAppendS(&device->type, "Unknown");
239                        break;
240                }
241            } else {
242                ffStrbufTrimRight(&device->type, ' ');
243                ffStrbufTrimRight(&device->type, ',');
244            }
245        } while (ffBluetoothFindNextDevice(hFind, &btdi));
246
247        ffBluetoothFindDeviceClose(hFind);
248    }
249
250    return nullptr;
251}
252
253const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
254    // `showType` selects which stacks are walked, not merely which entries are printed. The classic
255    // and the Low Energy stack are two different APIs here, so a stack the user switched off is not
256    // called at all.
257    if (options->showType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT) {
258        const char* error = detectClassic(options, devices);
259        if (error) {
260            return error;
261        }
262    }
263
264    if (options->showType & FF_BLUETOOTH_DEVICE_TYPE_LE_BIT) {
265        // A failure here is not fatal: the LE half only adds to the classic list, which stands on
266        // its own, and "no LE link on this machine" is the ordinary case rather than an error.
267        ffBluetoothDetectLe(options, devices);
268    }
269
270    if (devices->length > 0) {
271        detectBattery(devices);
272    }
273
274    return nullptr;
275}