From 4a97cc682588952a0df27952491ea6dc27990002 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Wed, 23 Sep 2026 15:11:06 +0800 Subject: [PATCH] Bluetooth: adds option `showType` --- CHANGELOG.md | 1 + doc/json_schema.json | 18 ++++++ src/detection/bluetooth/bluetooth.h | 8 --- src/detection/bluetooth/bluetooth_apple.m | 64 +++++++++++++++------ src/detection/bluetooth/bluetooth_bsd.c | 8 ++- src/detection/bluetooth/bluetooth_haiku.cpp | 8 ++- src/detection/bluetooth/bluetooth_linux.c | 11 +++- src/detection/bluetooth/bluetooth_windows.c | 33 +++++++++-- src/modules/bluetooth/bluetooth.c | 34 ++++++++++- src/modules/bluetooth/option.h | 12 ++++ 10 files changed, 160 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc1310793..e1bbd0c31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Changes: Features: * Added Bluetooth Low Energy detection on Windows and macOS, through WinRT and Core Bluetooth. (Bluetooth, Windows / macOS) +* Added the `showType` option to the `Bluetooth` module, which selects which stacks are looked for. (Bluetooth) * Added `{device-type}`, `{signal-quality}` and `{signal-quality-bar}` to the `Bluetooth` module, and `deviceType` and `signalQuality` to its JSON output. (Bluetooth) * `deviceType` names the stacks a device answers on, `Classic` and/or `Low Energy`, and `signalQuality` is the signal strength of the LE link, converted from dBm on the same scale the `Wifi` module uses. * Linux fills both from the BlueZ `Class`, `Appearance` and `RSSI` properties, and macOS from Core Bluetooth. The remaining platforms report `Classic` and no signal quality, which is what their backends can tell. diff --git a/doc/json_schema.json b/doc/json_schema.json index f8ac42894..232dae5c3 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -1749,6 +1749,24 @@ "type": "boolean", "default": false }, + "devType": { + "description": "Which Bluetooth stacks to look for", + "oneOf": [ + { + "const": "classic", + "description": "Look for BR/EDR devices only" + }, + { + "const": "le", + "description": "Look for Bluetooth Low Energy devices only" + }, + { + "const": "both", + "description": "Look for devices on either stack" + } + ], + "default": "both" + }, "percent": { "$ref": "#/$defs/percent" }, diff --git a/src/detection/bluetooth/bluetooth.h b/src/detection/bluetooth/bluetooth.h index ac7379df3..b25b173a4 100644 --- a/src/detection/bluetooth/bluetooth.h +++ b/src/detection/bluetooth/bluetooth.h @@ -3,14 +3,6 @@ #include "fastfetch.h" #include "modules/bluetooth/option.h" -// Which radio a device was seen over. A dual-mode device answers on both, and a platform that only -// walks one stack can only report the bit it knows about, so this is a bitfield rather than a value. -typedef enum FFBluetoothDeviceType: uint8_t { - FF_BLUETOOTH_DEVICE_TYPE_NONE = 0, - FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT = 1 << 0, // BR/EDR, the BTHENUM enumerator - FF_BLUETOOTH_DEVICE_TYPE_LE_BIT = 1 << 1, // Low Energy, the BTHLE enumerator -} FFBluetoothDeviceType; - typedef struct FFBluetoothResult { FFstrbuf name; FFstrbuf address; diff --git a/src/detection/bluetooth/bluetooth_apple.m b/src/detection/bluetooth/bluetooth_apple.m index 2e966dff0..7c8d42191 100644 --- a/src/detection/bluetooth/bluetooth_apple.m +++ b/src/detection/bluetooth/bluetooth_apple.m @@ -100,6 +100,11 @@ // link is up, and the callbacks are waited for as a group, so a machine with no LE link pays // nothing. // +// * `showType` is honoured here rather than in the module, because the two stacks are two separate +// walks with two separate permission surfaces: a stack the user switched off is not asked about +// at all. The LE pass keeps working without the classic one, it only loses the address join and +// falls back to matching by name. +// // Note on the permission model, which is what the first bullet above cannot show: any process that // touches Bluetooth is subject to the `kTCCServiceBluetoothAlways` service, and the request is // attributed to the *responsible* process. A host application that does not declare @@ -148,7 +153,7 @@ // The assigned 16-bit UUIDs, plus the 0xFEE0-0xFEFF block, which is where the vendor-specific ones // live. `retrieveConnectedPeripheralsWithServices:` filters on this list, so anything exposing only // a 128-bit custom service is out of reach -- that is a limit of the API, not of this list. -static NSArray* ffBluetoothLeServiceUuids(void) { +static NSArray* leServiceUuids(void) { static NSArray* uuids; static dispatch_once_t once; dispatch_once(&once, ^{ @@ -166,7 +171,7 @@ static NSArray* ffBluetoothLeServiceUuids(void) { // See the note on `CBPeripheral.isConnectedToSystem` at the top of the file. The fallback keeps the // module working on a system that does not answer the private property. -static bool ffBluetoothLeIsConnected(CBPeripheral* peripheral) { +static bool leIsConnected(CBPeripheral* peripheral) { if ([peripheral respondsToSelector: @selector(isConnectedToSystem)]) { return peripheral.isConnectedToSystem; } @@ -175,7 +180,7 @@ static bool ffBluetoothLeIsConnected(CBPeripheral* peripheral) { // The address is what a device is looked up by, both while the classic list is walked and by the LE // pass, and it is the only field the two copies of a dual-mode device are guaranteed to agree on. -static FFBluetoothResult* ffBluetoothFindByAddress(FFlist* devices, const char* address) { +static FFBluetoothResult* findByAddress(FFlist* devices, const char* address) { FF_LIST_FOR_EACH (FFBluetoothResult, device, *devices) { if (ffStrbufEqualS(&device->address, address)) { return device; @@ -187,7 +192,7 @@ static FFBluetoothResult* ffBluetoothFindByAddress(FFlist* devices, const char* // The class of device, as the classic stack reports it. It is built into a scratch buffer rather than // straight into the entry because the entry may already exist: see the merge in `ffDetectBluetooth`, // where the second copy of a dual-mode device only fills what the first copy left empty. -static void ffBluetoothSetClassOfDevice(FFstrbuf* type, IOBluetoothDevice* ioDevice) { +static void setClassOfDevice(FFstrbuf* type, IOBluetoothDevice* ioDevice) { if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorLimitedDiscoverableMode) ffStrbufAppendS(type, "Limited Discoverable Mode, "); if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorReserved1) @@ -263,7 +268,7 @@ static void ffBluetoothSetClassOfDevice(FFstrbuf* type, IOBluetoothDevice* ioDev // The fallback for a system whose `IOBluetoothDevice` does not answer `identifier`. A name that is // carried by more than one classic entry cannot identify anything, so an ambiguous name joins // nothing and the peripheral is reported as its own device instead of being merged. -static FFBluetoothResult* ffBluetoothFindByName(FFlist* devices, const char* name) { +static FFBluetoothResult* findByName(FFlist* devices, const char* name) { FFBluetoothResult* found = nullptr; FF_LIST_FOR_EACH (FFBluetoothResult, device, *devices) { if (!ffStrEqualsIgnCase(device->name.chars, name)) { @@ -277,7 +282,7 @@ static FFBluetoothResult* ffBluetoothFindByName(FFlist* devices, const char* nam return found; } -static const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */, NSDictionary* classicAddressByIdentifier) { +static const char* detectLe(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */, NSDictionary* classicAddressByIdentifier) { CBManagerAuthorization authorization = CBCentralManager.authorization; if (authorization == CBManagerAuthorizationDenied || authorization == CBManagerAuthorizationRestricted) { return nullptr; // The user said no. `NotDetermined` is left to the manager below, which asks. @@ -297,13 +302,13 @@ static const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devi return nullptr; // Powered off, unauthorized or no radio: the classic list stands on its own } - NSArray* peripherals = [delegate.central retrieveConnectedPeripheralsWithServices: ffBluetoothLeServiceUuids()]; + NSArray* peripherals = [delegate.central retrieveConnectedPeripheralsWithServices: leServiceUuids()]; if (peripherals.count == 0) { return nullptr; } for (CBPeripheral* peripheral in peripherals) { - if (ffBluetoothLeIsConnected(peripheral)) { + if (leIsConnected(peripheral)) { peripheral.delegate = delegate; ++delegate.signalStrengthAsked; [peripheral readRSSI]; @@ -319,7 +324,7 @@ static const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devi for (CBPeripheral* peripheral in peripherals) { const char* name = peripheral.name.length > 0 ? peripheral.name.UTF8String : nullptr; - bool connected = ffBluetoothLeIsConnected(peripheral); + bool connected = leIsConnected(peripheral); // The classic half wins wherever it has an answer: its name, address and type come from the // class of device rather than from a name the two stacks may spell differently. The @@ -327,10 +332,10 @@ static const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devi FFBluetoothResult* target = nullptr; NSString* address = classicAddressByIdentifier[peripheral.identifier.UUIDString]; if (address) { - target = ffBluetoothFindByAddress(devices, address.UTF8String); + target = findByAddress(devices, address.UTF8String); } if (!target && name) { - target = ffBluetoothFindByName(devices, name); + target = findByName(devices, name); } if (!target && !connected && !options->showDisconnected) { @@ -370,16 +375,15 @@ static const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devi return nullptr; } -const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) +// The classic (BR/EDR) half. `classicAddressByIdentifier` is an output rather than an input: the +// classic list is the only place a device address can be had from, and the LE pass needs those +// addresses to recognise a peripheral that has already been listed. +static const char* detectClassic(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */, NSMutableDictionary* classicAddressByIdentifier) { NSArray* ioDevices = IOBluetoothDevice.pairedDevices; if(!ioDevices) return "IOBluetoothDevice.pairedDevices failed"; - // The identifier the LE pass matches on, mapped to the address rather than to a position in - // `devices`: the list is the caller's, and a position in it is not this module's to promise. - NSMutableDictionary* classicAddressByIdentifier = [NSMutableDictionary dictionary]; - for(IOBluetoothDevice* ioDevice in ioDevices) { if (!options->showDisconnected && !ioDevice.isConnected) @@ -404,7 +408,7 @@ const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FF } FF_STRBUF_AUTO_DESTROY type = ffStrbufCreate(); - ffBluetoothSetClassOfDevice(&type, ioDevice); + setClassOfDevice(&type, ioDevice); uint8_t battery = 0; if (ioDevice.batteryPercentSingle) @@ -415,7 +419,7 @@ const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FF battery = ioDevice.batteryPercentCase; // A device with no address cannot be recognised as a repeat, so it is always its own entry. - FFBluetoothResult* device = address.length > 0 ? ffBluetoothFindByAddress(devices, address.chars) : nullptr; + FFBluetoothResult* device = address.length > 0 ? findByAddress(devices, address.chars) : nullptr; if (!device) { device = FF_LIST_ADD(FFBluetoothResult, *devices); @@ -442,9 +446,31 @@ const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FF device->connected |= !!ioDevice.isConnected; } + return nullptr; +} + +const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) +{ + // `showType` selects which stacks are walked, not merely which entries are printed: a stack the + // user switched off is not asked about at all. The classic pass is the one that produces the + // identifier map the LE pass joins on, so it is nil when that pass does not run -- the LE pass + // then has nothing to match against and reports every peripheral as a device of its own. + NSMutableDictionary* classicAddressByIdentifier = nil; + + if (options->showType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT) { + classicAddressByIdentifier = [NSMutableDictionary dictionary]; + + const char* error = detectClassic(options, devices, classicAddressByIdentifier); + if (error) { + return error; + } + } + // Last, so that a peripheral answering on both stacks is matched against the classic list and // ends up as one entry rather than two. - ffBluetoothDetectLe(options, devices, classicAddressByIdentifier); + if (options->showType & FF_BLUETOOTH_DEVICE_TYPE_LE_BIT) { + detectLe(options, devices, classicAddressByIdentifier); + } return nullptr; } diff --git a/src/detection/bluetooth/bluetooth_bsd.c b/src/detection/bluetooth/bluetooth_bsd.c index 0326d0fd6..9b7b03343 100644 --- a/src/detection/bluetooth/bluetooth_bsd.c +++ b/src/detection/bluetooth/bluetooth_bsd.c @@ -22,7 +22,13 @@ static int enumDev([[maybe_unused]] int sockfd, struct bt_devinfo const* dev, FF return 0; } -const char* ffDetectBluetooth([[maybe_unused]] FFBluetoothOptions* options, [[maybe_unused]] FFlist* devices /* FFBluetoothResult */) { +const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) { + // Netgraph carries the BR/EDR stack and nothing else, so there is no second function to dispatch + // to here: a Low Energy-only configuration simply has nothing to walk. + if (!(options->showType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT)) { + return nullptr; + } + // struct hostent* ent = bt_gethostent(); if (bt_devenum((void*) enumDev, devices) < 0) { return "bt_devenum() failed"; diff --git a/src/detection/bluetooth/bluetooth_haiku.cpp b/src/detection/bluetooth/bluetooth_haiku.cpp index c0835e034..ac0144d05 100644 --- a/src/detection/bluetooth/bluetooth_haiku.cpp +++ b/src/detection/bluetooth/bluetooth_haiku.cpp @@ -5,7 +5,13 @@ extern "C" { #include -const char* ffDetectBluetooth([[maybe_unused]] FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) { +const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) { + // The local adapter's device class is a BR/EDR concept and the adapter is the only device this + // backend has, so a Low Energy-only configuration leaves the list empty. + if (!(options->showType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT)) { + return nullptr; + } + using namespace Bluetooth; FF_SUPPRESS_IO(); diff --git a/src/detection/bluetooth/bluetooth_linux.c b/src/detection/bluetooth/bluetooth_linux.c index 5794252d5..e335acb2b 100644 --- a/src/detection/bluetooth/bluetooth_linux.c +++ b/src/detection/bluetooth/bluetooth_linux.c @@ -196,11 +196,20 @@ static void detectBluetoothRoot(FFBluetoothOptions* options, FFlist* devices, FF FFBluetoothResult* device = detectBluetoothObject(devices, dbus, &arrayIter); if (device) { - if (!options->showDisconnected && !device->connected) { + // `showType` selects which stacks are reported. Unlike the other backends there is no + // per-stack call to skip here: BlueZ publishes the class of device and the GATT appearance + // as two properties of the same `org.bluez.Device1` object, so both stacks arrive on this + // one walk and the filter lands on the result instead. A device whose stack could not be + // determined is kept -- BlueZ does not promise that a Device1 object carries either + // property, and dropping those would lose devices the default configuration reports. + bool deviceTypeWanted = device->deviceType == FF_BLUETOOTH_DEVICE_TYPE_NONE || (device->deviceType & options->showType); + + if (!deviceTypeWanted || (!options->showDisconnected && !device->connected)) { ffStrbufDestroy(&device->name); ffStrbufDestroy(&device->address); ffStrbufDestroy(&device->type); --devices->length; + continue; } if (device->name.length == 0) { diff --git a/src/detection/bluetooth/bluetooth_windows.c b/src/detection/bluetooth/bluetooth_windows.c index 6aec97983..f58a12501 100644 --- a/src/detection/bluetooth/bluetooth_windows.c +++ b/src/detection/bluetooth/bluetooth_windows.c @@ -40,7 +40,7 @@ const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devices /* // The device tree spells an address without separators, while the list holds the printed form. The // case has to be folded because it depends on the enumerator: the BTHENUM nodes are upper case and // the BTHLE / BTHLEDevice ones lower case, and the list is always upper case. -static bool ffBluetoothAddressEquals(const wchar_t* plain, const FFstrbuf* address) { +static bool addressEquals(const wchar_t* plain, const FFstrbuf* address) { if (address->length != 17) { return false; } @@ -55,7 +55,7 @@ static bool ffBluetoothAddressEquals(const wchar_t* plain, const FFstrbuf* addre return true; } -static const char* ffBluetoothDetectBattery(FFlist* devices) { +static const char* detectBattery(FFlist* devices) { ULONG idListLength = 0; // The class filter is a no-op here, and deliberately so: CM_Get_Device_ID_ListW() only honours the // filter string together with CM_GETIDLIST_FILTER_CLASS, so this walks every present node of the @@ -107,7 +107,7 @@ static const char* ffBluetoothDetectBattery(FFlist* devices) { } FF_LIST_FOR_EACH (FFBluetoothResult, bt, *devices) { - if (ffBluetoothAddressEquals(deviceAddress, &bt->address)) { + if (addressEquals(deviceAddress, &bt->address)) { bt->battery = battery; break; } @@ -117,7 +117,10 @@ static const char* ffBluetoothDetectBattery(FFlist* devices) { return nullptr; } -const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) { +// The classic (BR/EDR) half. `BluetoothFindFirstDevice` only ever answers with BR/EDR devices; the +// Low Energy stack is a separate walk, in bluetooth_windows.cpp, which folds what it finds into the +// list this function builds. +static const char* detectClassic(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) { FF_LIBRARY_LOAD_MESSAGE(bluetoothapis, "bluetoothapis.dll", 1) FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindFirstDevice) FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindNextDevice) @@ -239,10 +242,28 @@ const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FF ffBluetoothFindDeviceClose(hFind); } - ffBluetoothDetectLe(options, devices); + return nullptr; +} + +const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) { + // `showType` selects which stacks are walked, not merely which entries are printed. The classic + // and the Low Energy stack are two different APIs here, so a stack the user switched off is not + // called at all. + if (options->showType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT) { + const char* error = detectClassic(options, devices); + if (error) { + return error; + } + } + + if (options->showType & FF_BLUETOOTH_DEVICE_TYPE_LE_BIT) { + // A failure here is not fatal: the LE half only adds to the classic list, which stands on + // its own, and "no LE link on this machine" is the ordinary case rather than an error. + ffBluetoothDetectLe(options, devices); + } if (devices->length > 0) { - ffBluetoothDetectBattery(devices); + detectBattery(devices); } return nullptr; diff --git a/src/modules/bluetooth/bluetooth.c b/src/modules/bluetooth/bluetooth.c index f9cac3971..66d6c809a 100644 --- a/src/modules/bluetooth/bluetooth.c +++ b/src/modules/bluetooth/bluetooth.c @@ -1,7 +1,6 @@ #include "common/percent.h" #include "common/printing.h" #include "common/jsonconfig.h" -#include "common/strutil.h" #include "detection/bluetooth/bluetooth.h" #include "modules/bluetooth/bluetooth.h" @@ -120,6 +119,22 @@ void ffParseBluetoothJsonObject(FFBluetoothOptions* options, yyjson_val* module) continue; } + if (unsafe_yyjson_equals_str(key, "showType")) { + int value; + const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) { + { "classic", FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT }, + { "le", FF_BLUETOOTH_DEVICE_TYPE_LE_BIT }, + { "both", FF_BLUETOOTH_DEVICE_TYPE_BOTH }, + {}, + }); + if (error) { + ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Bluetooth), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error); + } else { + options->showType = (FFBluetoothDeviceType) value; + } + continue; + } + if (ffPercentParseJsonObject(key, val, &options->percent)) { continue; } @@ -133,6 +148,21 @@ void ffGenerateBluetoothJsonConfig(FFBluetoothOptions* options, yyjson_mut_doc* yyjson_mut_obj_add_bool(doc, module, "showDisconnected", options->showDisconnected); + switch (options->showType) { + case FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT: + yyjson_mut_obj_add_str(doc, module, "showType", "classic"); + break; + case FF_BLUETOOTH_DEVICE_TYPE_LE_BIT: + yyjson_mut_obj_add_str(doc, module, "showType", "le"); + break; + case FF_BLUETOOTH_DEVICE_TYPE_BOTH: + yyjson_mut_obj_add_str(doc, module, "showType", "both"); + break; + case FF_BLUETOOTH_DEVICE_TYPE_NONE: + default: + break; + } + ffPercentGenerateJsonConfig(doc, module, options->percent); } @@ -181,6 +211,8 @@ bool ffGenerateBluetoothJsonResult(FFBluetoothOptions* options, yyjson_mut_doc* void ffInitBluetoothOptions(FFBluetoothOptions* options) { ffOptionInitModuleArg(&options->moduleArgs, ""); options->showDisconnected = false; + // Both stacks are walked by default; `showType` exists to switch one off, not to switch one on. + options->showType = FF_BLUETOOTH_DEVICE_TYPE_BOTH; options->percent = (FFPercentageModuleConfig) { 50, 20, 0 }; } diff --git a/src/modules/bluetooth/option.h b/src/modules/bluetooth/option.h index 1f11d295c..357af792a 100644 --- a/src/modules/bluetooth/option.h +++ b/src/modules/bluetooth/option.h @@ -3,10 +3,22 @@ #include "common/option.h" #include "common/percent.h" +// Which radio a device answers on. A dual-mode device answers on both, and a backend that walks only +// one stack can only report the bit it knows about, so this is a bitfield rather than a value. It +// carries both meanings the module needs: which stacks a result was seen on, and which stacks +// `options->showType` asks the detection to walk at all. +typedef enum FFBluetoothDeviceType: uint8_t { + FF_BLUETOOTH_DEVICE_TYPE_NONE = 0, + FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT = 1 << 0, // BR/EDR, the BTHENUM enumerator + FF_BLUETOOTH_DEVICE_TYPE_LE_BIT = 1 << 1, // Low Energy, the BTHLE enumerator + FF_BLUETOOTH_DEVICE_TYPE_BOTH = FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT | FF_BLUETOOTH_DEVICE_TYPE_LE_BIT, +} FFBluetoothDeviceType; + typedef struct FFBluetoothOptions { FFModuleArgs moduleArgs; bool showDisconnected; + FFBluetoothDeviceType showType; FFPercentageModuleConfig percent; } FFBluetoothOptions;