summaryrefslogtreecommitdiffstats
path: root/src/detection/bluetoothradio
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/detection/bluetoothradio
Add the files
Diffstat (limited to 'src/detection/bluetoothradio')
-rw-r--r--src/detection/bluetoothradio/bluetoothradio.c161
-rw-r--r--src/detection/bluetoothradio/bluetoothradio.h18
-rw-r--r--src/detection/bluetoothradio/bluetoothradio_apple.m69
-rw-r--r--src/detection/bluetoothradio/bluetoothradio_linux.c153
-rw-r--r--src/detection/bluetoothradio/bluetoothradio_nosupport.c5
-rw-r--r--src/detection/bluetoothradio/bluetoothradio_windows.c99
6 files changed, 505 insertions, 0 deletions
diff --git a/src/detection/bluetoothradio/bluetoothradio.c b/src/detection/bluetoothradio/bluetoothradio.c
new file mode 100644
index 0000000..117e225
--- /dev/null
+++ b/src/detection/bluetoothradio/bluetoothradio.c
@@ -0,0 +1,161 @@
+#include "bluetoothradio.h"
+
+// https://github.com/ziglang/zig/blob/a84951465b409495095a9598db0cae745f34fa7b/lib/libc/include/any-windows-any/bthdef.h#L187-L236
+
+#define BTH_MFG_ERICSSON 0
+#define BTH_MFG_NOKIA 1
+#define BTH_MFG_INTEL 2
+#define BTH_MFG_IBM 3
+#define BTH_MFG_TOSHIBA 4
+#define BTH_MFG_3COM 5
+#define BTH_MFG_MICROSOFT 6
+#define BTH_MFG_LUCENT 7
+#define BTH_MFG_MOTOROLA 8
+#define BTH_MFG_INFINEON 9
+#define BTH_MFG_CSR 10
+#define BTH_MFG_SILICONWAVE 11
+#define BTH_MFG_DIGIANSWER 12
+#define BTH_MFG_TI 13
+#define BTH_MFG_PARTHUS 14
+#define BTH_MFG_BROADCOM 15
+#define BTH_MFG_MITEL 16
+#define BTH_MFG_WIDCOMM 17
+#define BTH_MFG_ZEEVO 18
+#define BTH_MFG_ATMEL 19
+#define BTH_MFG_MITSIBUSHI 20
+#define BTH_MFG_RTX_TELECOM 21
+#define BTH_MFG_KC_TECHNOLOGY 22
+#define BTH_MFG_NEWLOGIC 23
+#define BTH_MFG_TRANSILICA 24
+#define BTH_MFG_ROHDE_SCHWARZ 25
+#define BTH_MFG_TTPCOM 26
+#define BTH_MFG_SIGNIA 27
+#define BTH_MFG_CONEXANT 28
+#define BTH_MFG_QUALCOMM 29
+#define BTH_MFG_INVENTEL 30
+#define BTH_MFG_AVM_BERLIN 31
+#define BTH_MFG_BANDSPEED 32
+#define BTH_MFG_MANSELLA 33
+#define BTH_MFG_NEC 34
+#define BTH_MFG_WAVEPLUS_TECHNOLOGY_CO 35
+#define BTH_MFG_ALCATEL 36
+#define BTH_MFG_PHILIPS_SEMICONDUCTOR 37
+#define BTH_MFG_C_TECHNOLOGIES 38
+#define BTH_MFG_OPEN_INTERFACE 39
+#define BTH_MFG_RF_MICRO_DEVICES 40
+#define BTH_MFG_HITACHI 41
+#define BTH_MFG_SYMBOL_TECHNOLOGIES 42
+#define BTH_MFG_TENOVIS 43
+#define BTH_MFG_MACRONIX_INTERNATIONAL 44
+#define BTH_MFG_MARVELL 72
+#define BTH_MFG_APPLE 76
+#define BTH_MFG_NORDIC_SEMICONDUCTORS_ASA 89
+#define BTH_MFG_ARUBA_NETWORKS 283
+#define BTH_MFG_INTERNAL_USE 65535
+
+const char* ffBluetoothRadioGetVendor(uint32_t manufacturerId) {
+ switch (manufacturerId) {
+ case BTH_MFG_ERICSSON:
+ return "Ericsson";
+ case BTH_MFG_NOKIA:
+ return "Nokia";
+ case BTH_MFG_INTEL:
+ return "Intel";
+ case BTH_MFG_IBM:
+ return "IBM";
+ case BTH_MFG_TOSHIBA:
+ return "Toshiba";
+ case BTH_MFG_3COM:
+ return "3Com";
+ case BTH_MFG_MICROSOFT:
+ return "Microsoft";
+ case BTH_MFG_LUCENT:
+ return "Lucent";
+ case BTH_MFG_MOTOROLA:
+ return "Motorola";
+ case BTH_MFG_INFINEON:
+ return "Infineon";
+ case BTH_MFG_CSR:
+ return "CSR";
+ case BTH_MFG_SILICONWAVE:
+ return "Silicon-Wave";
+ case BTH_MFG_DIGIANSWER:
+ return "Digi-Answer";
+ case BTH_MFG_TI:
+ return "Ti";
+ case BTH_MFG_PARTHUS:
+ return "Parthus";
+ case BTH_MFG_BROADCOM:
+ return "Broadcom";
+ case BTH_MFG_MITEL:
+ return "Mitel";
+ case BTH_MFG_WIDCOMM:
+ return "Widcomm";
+ case BTH_MFG_ZEEVO:
+ return "Zeevo";
+ case BTH_MFG_ATMEL:
+ return "Atmel";
+ case BTH_MFG_MITSIBUSHI:
+ return "Mitsubishi";
+ case BTH_MFG_RTX_TELECOM:
+ return "RTX Telecom";
+ case BTH_MFG_KC_TECHNOLOGY:
+ return "KC Technology";
+ case BTH_MFG_NEWLOGIC:
+ return "Newlogic";
+ case BTH_MFG_TRANSILICA:
+ return "Transilica";
+ case BTH_MFG_ROHDE_SCHWARZ:
+ return "Rohde-Schwarz";
+ case BTH_MFG_TTPCOM:
+ return "TTPCom";
+ case BTH_MFG_SIGNIA:
+ return "Signia";
+ case BTH_MFG_CONEXANT:
+ return "Conexant";
+ case BTH_MFG_QUALCOMM:
+ return "Qualcomm";
+ case BTH_MFG_INVENTEL:
+ return "Inventel";
+ case BTH_MFG_AVM_BERLIN:
+ return "AVM Berlin";
+ case BTH_MFG_BANDSPEED:
+ return "Bandspeed";
+ case BTH_MFG_MANSELLA:
+ return "Mansella";
+ case BTH_MFG_NEC:
+ return "NEC";
+ case BTH_MFG_WAVEPLUS_TECHNOLOGY_CO:
+ return "Waveplus";
+ case BTH_MFG_ALCATEL:
+ return "Alcatel";
+ case BTH_MFG_PHILIPS_SEMICONDUCTOR:
+ return "Philips Semiconductors";
+ case BTH_MFG_C_TECHNOLOGIES:
+ return "C Technologies";
+ case BTH_MFG_OPEN_INTERFACE:
+ return "Open Interface";
+ case BTH_MFG_RF_MICRO_DEVICES:
+ return "RF Micro Devices";
+ case BTH_MFG_HITACHI:
+ return "Hitachi";
+ case BTH_MFG_SYMBOL_TECHNOLOGIES:
+ return "Symbol Technologies";
+ case BTH_MFG_TENOVIS:
+ return "Tenovis";
+ case BTH_MFG_MACRONIX_INTERNATIONAL:
+ return "Macronix International";
+ case BTH_MFG_MARVELL:
+ return "Marvell";
+ case BTH_MFG_APPLE:
+ return "Apple";
+ case BTH_MFG_NORDIC_SEMICONDUCTORS_ASA:
+ return "Nordic Semiconductor ASA";
+ case BTH_MFG_ARUBA_NETWORKS:
+ return "Aruba Networks";
+ case BTH_MFG_INTERNAL_USE:
+ return "Internal Use";
+ default:
+ return "Unknown";
+ }
+}
diff --git a/src/detection/bluetoothradio/bluetoothradio.h b/src/detection/bluetoothradio/bluetoothradio.h
new file mode 100644
index 0000000..a3c1208
--- /dev/null
+++ b/src/detection/bluetoothradio/bluetoothradio.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "fastfetch.h"
+#include "modules/bluetoothradio/option.h"
+
+typedef struct FFBluetoothRadioResult {
+ FFstrbuf name;
+ FFstrbuf address;
+ FFstrbuf vendor;
+ int32_t lmpVersion;
+ int32_t lmpSubversion;
+ bool enabled;
+ bool discoverable;
+ bool connectable;
+} FFBluetoothRadioResult;
+
+const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */);
+const char* ffBluetoothRadioGetVendor(uint32_t manufacturerId);
diff --git a/src/detection/bluetoothradio/bluetoothradio_apple.m b/src/detection/bluetoothradio/bluetoothradio_apple.m
new file mode 100644
index 0000000..c5154ab
--- /dev/null
+++ b/src/detection/bluetoothradio/bluetoothradio_apple.m
@@ -0,0 +1,69 @@
+#include "bluetoothradio.h"
+#include "common/processing.h"
+
+#import <IOBluetooth/IOBluetooth.h>
+
+// For some reason the official declaration of IOBluetoothHostController doesn't include property `controllers`
+@interface IOBluetoothHostController()
++ (id)controllers;
+@end
+
+const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */)
+{
+ NSArray<IOBluetoothHostController*>* ctrls = IOBluetoothHostController.controllers;
+ if(!ctrls)
+ return "IOBluetoothHostController.controllers returns nil";
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ if (ffProcessAppendStdOut(&buffer, (char* const[]) {
+ "system_profiler",
+ "SPBluetoothDataType",
+ "-xml",
+ "-detailLevel",
+ "basic",
+ NULL
+ }) != NULL)
+ return "Starting `system_profiler SPBluetoothDataType -xml -detailLevel basic` failed";
+
+ NSArray* arr = [NSPropertyListSerialization propertyListWithData:[NSData dataWithBytes:buffer.chars length:buffer.length]
+ options:NSPropertyListImmutable
+ format:nil
+ error:nil];
+ if (!arr || !arr.count)
+ return "system_profiler SPBluetoothDataType returned an empty array";
+
+ for (IOBluetoothHostController* ctrl in ctrls)
+ {
+ FFBluetoothRadioResult* device = FF_LIST_ADD(FFBluetoothRadioResult, *devices);
+ ffStrbufInitS(&device->name, ctrl.nameAsString.UTF8String);
+ ffStrbufInitS(&device->address, ctrl.addressAsString.UTF8String);
+ ffStrbufInitStatic(&device->vendor, "Apple");
+ device->lmpVersion = INT_MIN;
+ device->lmpSubversion = INT_MIN;
+ device->enabled = ctrl.powerState == kBluetoothHCIPowerStateON;
+ device->discoverable = false;
+ device->connectable = true;
+
+ for (NSDictionary* itemDict in arr[0][@"_items"])
+ {
+ NSDictionary* props = itemDict[@"controller_properties"];
+ if (!props) continue;
+
+ if (![ctrl.addressAsString isEqualToString:props[@"controller_address"]]) continue;
+
+ NSString* services = props[@"controller_supportedServices"];
+ if ([services containsString:@" LEA "])
+ device->lmpVersion = -11;
+ else if ([services containsString:@" GATT "])
+ device->lmpVersion = -6;
+
+ device->discoverable = ![props[@"controller_discoverable"] isEqualToString:@"attrib_off"];
+ ffStrbufSetS(&device->vendor, ((NSString*) props[@"controller_vendorID"]).UTF8String);
+ ffStrbufSubstrAfterFirstC(&device->vendor, '(');
+ ffStrbufTrimRight(&device->vendor, ')');
+ break;
+ }
+ }
+
+ return NULL;
+}
diff --git a/src/detection/bluetoothradio/bluetoothradio_linux.c b/src/detection/bluetoothradio/bluetoothradio_linux.c
new file mode 100644
index 0000000..a7d92a4
--- /dev/null
+++ b/src/detection/bluetoothradio/bluetoothradio_linux.c
@@ -0,0 +1,153 @@
+#include "bluetoothradio.h"
+#include "common/strutil.h"
+
+#ifdef FF_HAVE_DBUS
+ #include "common/dbus.h"
+ #include "common/io.h"
+
+/* Example dbus reply:
+array [
+ dict entry(
+ string "Address"
+ variant string "XX:XX:XX:XX:XX:XX"
+ )
+ dict entry(
+ string "Name"
+ variant string "xxxxxxxx"
+ )
+ dict entry(
+ string "Powered"
+ variant boolean true
+ )
+ dict entry(
+ string "PowerState"
+ variant string "on"
+ )
+ dict entry(
+ string "Manufacturer"
+ variant uint16 2
+ )
+ dict entry(
+ string "Version"
+ variant byte 12
+ )
+]
+*/
+
+static const char* detectBluetoothProperty(FFBluetoothRadioResult* device, FFDBusData* dbus, DBusMessageIter* iter) {
+ if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) {
+ return "Expected dict entry";
+ }
+
+ DBusMessageIter dictIter;
+ dbus->lib->ffdbus_message_iter_recurse(iter, &dictIter);
+
+ if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_STRING) {
+ return "Expected dict entry key to be a string";
+ }
+
+ const char* deviceProperty;
+ dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &deviceProperty);
+
+ dbus->lib->ffdbus_message_iter_next(&dictIter);
+
+ if (ffStrEquals(deviceProperty, "Address")) {
+ ffDBusGetString(dbus, &dictIter, &device->address);
+ } else if (ffStrEquals(deviceProperty, "Alias")) {
+ ffDBusGetString(dbus, &dictIter, &device->name);
+ } else if (ffStrEquals(deviceProperty, "Manufacturer")) {
+ uint64_t vendorId;
+ if (ffDBusGetUint(dbus, &dictIter, &vendorId)) {
+ ffStrbufSetStatic(&device->vendor, ffBluetoothRadioGetVendor((uint32_t) vendorId));
+ }
+ } else if (ffStrEquals(deviceProperty, "Version")) {
+ uint64_t version;
+ if (ffDBusGetUint(dbus, &dictIter, &version)) {
+ device->lmpVersion = (int32_t) version;
+ }
+ } else if (ffStrEquals(deviceProperty, "Powered")) {
+ ffDBusGetBool(dbus, &dictIter, &device->enabled);
+ } else if (ffStrEquals(deviceProperty, "Discoverable")) {
+ ffDBusGetBool(dbus, &dictIter, &device->discoverable);
+ } else if (ffStrEquals(deviceProperty, "Pairable")) {
+ ffDBusGetBool(dbus, &dictIter, &device->connectable);
+ }
+
+ return NULL;
+}
+
+static const char* detectBluetoothRoot(FFBluetoothRadioResult* device, const char* hciName, FFDBusData* dbus) {
+ char objPath[300];
+ snprintf(objPath, sizeof(objPath), "/org/bluez/%s", hciName);
+
+ DBusMessage* properties = ffDBusGetMethodReply(dbus, "org.bluez", objPath, "org.freedesktop.DBus.Properties", "GetAll", "org.bluez.Adapter1", NULL);
+ if (!properties) {
+ return "Failed to call org.freedesktop.DBus.Properties.GetAll";
+ }
+
+ DBusMessageIter rootIter;
+ if (!dbus->lib->ffdbus_message_iter_init(properties, &rootIter)) {
+ dbus->lib->ffdbus_message_unref(properties);
+ return "Failed to get root iterator of org.freedesktop.DBus.Properties.GetAll";
+ }
+
+ if (dbus->lib->ffdbus_message_iter_get_arg_type(&rootIter) != DBUS_TYPE_ARRAY) {
+ dbus->lib->ffdbus_message_unref(properties);
+ return "Expected array";
+ }
+
+ DBusMessageIter arrayIter;
+ dbus->lib->ffdbus_message_iter_recurse(&rootIter, &arrayIter);
+
+ do {
+ detectBluetoothProperty(device, dbus, &arrayIter);
+ } while (dbus->lib->ffdbus_message_iter_next(&arrayIter));
+
+ dbus->lib->ffdbus_message_unref(properties);
+ return NULL;
+}
+
+static const char* detectBluetooth(FFlist* devices) {
+ FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/bluetooth");
+ if (dirp == NULL) {
+ return "Failed to open /sys/class/bluetooth";
+ }
+
+ FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {};
+ const char* error = ffDBusLoadData(DBUS_BUS_SYSTEM, &dbus);
+ if (error) {
+ return error;
+ }
+
+ struct dirent* entry;
+ while ((entry = readdir(dirp)) != NULL) {
+ if (entry->d_name[0] == '.') {
+ continue;
+ }
+
+ if (strchr(entry->d_name, ':') != NULL) { // ignore connected devices
+ continue;
+ }
+
+ FFBluetoothRadioResult* device = FF_LIST_ADD(FFBluetoothRadioResult, *devices);
+ ffStrbufInit(&device->name);
+ ffStrbufInit(&device->address);
+ ffStrbufInitStatic(&device->vendor, "Unknown");
+ device->lmpVersion = INT_MIN;
+ device->lmpSubversion = INT_MIN;
+ device->enabled = false;
+ detectBluetoothRoot(device, entry->d_name, &dbus);
+ }
+ return NULL;
+}
+
+#endif
+
+const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */) {
+#ifdef FF_HAVE_DBUS
+ return detectBluetooth(devices);
+#else
+ FF_UNUSED(devices)
+ return "Fastfetch was compiled without DBus support";
+#endif
+}
diff --git a/src/detection/bluetoothradio/bluetoothradio_nosupport.c b/src/detection/bluetoothradio/bluetoothradio_nosupport.c
new file mode 100644
index 0000000..08fb6f8
--- /dev/null
+++ b/src/detection/bluetoothradio/bluetoothradio_nosupport.c
@@ -0,0 +1,5 @@
+#include "bluetoothradio.h"
+
+const char* ffDetectBluetoothRadio(FF_A_UNUSED FFlist* devices /* FFBluetoothRadioResult */) {
+ return "Not supported on this platform";
+}
diff --git a/src/detection/bluetoothradio/bluetoothradio_windows.c b/src/detection/bluetoothradio/bluetoothradio_windows.c
new file mode 100644
index 0000000..f0b55d4
--- /dev/null
+++ b/src/detection/bluetoothradio/bluetoothradio_windows.c
@@ -0,0 +1,99 @@
+#include "bluetoothradio.h"
+#include "common/library.h"
+#include "common/io.h"
+#include "common/windows/unicode.h"
+
+#include <windows.h>
+#include <bluetoothapis.h>
+#include <winioctl.h>
+
+// #include <bthioctl.h>
+
+#define BTH_IOCTL_BASE 0
+#define BTH_CTL(id) CTL_CODE(FILE_DEVICE_BLUETOOTH, (id), METHOD_BUFFERED, FILE_ANY_ACCESS)
+#define IOCTL_BTH_GET_LOCAL_INFO BTH_CTL(BTH_IOCTL_BASE + 0x00)
+#define LMP_LE_SUPPORTED(x) ((x >> 38) & 1)
+
+typedef struct _BTH_RADIO_INFO {
+ // Supported LMP features of the radio. Use LMP_XXX() to extract
+ // the desired bits.
+ ULONGLONG lmpSupportedFeatures;
+
+ // Manufacturer ID (possibly BTH_MFG_XXX)
+ USHORT mfg;
+
+ // LMP subversion
+ USHORT lmpSubversion;
+
+ // LMP version
+ UCHAR lmpVersion;
+} FF_A_PACKED BTH_RADIO_INFO;
+
+typedef struct _BTH_LOCAL_RADIO_INFO {
+ // Local BTH_ADDR, class of device, and radio name
+ BTH_DEVICE_INFO localInfo;
+
+ // Combo of LOCAL_RADIO_XXX values
+ ULONG flags;
+
+ // HCI revision, see core spec
+ USHORT hciRevision;
+
+ // HCI version, see core spec
+ UCHAR hciVersion;
+
+ // More information about the local radio (LMP, MFG)
+ BTH_RADIO_INFO radioInfo;
+} FF_A_PACKED BTH_LOCAL_RADIO_INFO;
+static_assert(sizeof(BTH_LOCAL_RADIO_INFO) == 292, "BTH_LOCAL_RADIO_INFO should be 292 bytes");
+
+#pragma GCC diagnostic ignored "-Wpointer-sign"
+
+const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */) {
+ // Actually bluetoothapis.dll, but it's missing on Windows 7
+ FF_LIBRARY_LOAD_MESSAGE(bluetoothapis, "bluetoothapis.dll", 1)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindFirstRadio)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindNextRadio)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindRadioClose)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothIsConnectable)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothIsDiscoverable)
+
+ HANDLE hRadio = NULL;
+ HBLUETOOTH_DEVICE_FIND hFind = ffBluetoothFindFirstRadio(&(BLUETOOTH_FIND_RADIO_PARAMS) {
+ .dwSize = sizeof(BLUETOOTH_FIND_RADIO_PARAMS) },
+ &hRadio);
+ if (!hFind) {
+ if (GetLastError() == ERROR_NO_MORE_ITEMS) {
+ return "No Bluetooth radios found or service disabled";
+ } else {
+ return "BluetoothFindFirstRadio() failed";
+ }
+ }
+
+ do {
+ BTH_LOCAL_RADIO_INFO blri;
+ DWORD returned;
+ if (!DeviceIoControl(hRadio, IOCTL_BTH_GET_LOCAL_INFO, NULL, 0, &blri, sizeof(blri), &returned, NULL)) {
+ continue;
+ }
+
+ FFBluetoothRadioResult* device = FF_LIST_ADD(FFBluetoothRadioResult, *devices);
+ ffStrbufInitS(&device->name, blri.localInfo.name);
+
+ BLUETOOTH_ADDRESS_STRUCT addr = { .ullLong = blri.localInfo.address };
+ ffStrbufInitF(&device->address, "%02X:%02X:%02X:%02X:%02X:%02X", addr.rgBytes[5], addr.rgBytes[4], addr.rgBytes[3], addr.rgBytes[2], addr.rgBytes[1], addr.rgBytes[0]);
+
+ device->lmpVersion = blri.radioInfo.lmpVersion;
+ device->lmpSubversion = blri.radioInfo.lmpSubversion;
+ ffStrbufInitStatic(&device->vendor, ffBluetoothRadioGetVendor(blri.radioInfo.mfg));
+ device->enabled = true;
+ device->connectable = ffBluetoothIsConnectable(hRadio);
+ device->discoverable = ffBluetoothIsDiscoverable(hRadio);
+
+ NtClose(hRadio);
+ } while (ffBluetoothFindNextRadio(hFind, &hRadio));
+
+ ffBluetoothFindRadioClose(hFind);
+
+ return NULL;
+}