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