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}