Files
bluehound/classic_bluetooth.mm
2026-07-29 00:39:00 -04:00

442 lines
14 KiB
Plaintext

#import "classic_bluetooth.h"
#import <IOBluetooth/IOBluetooth.h>
#import <Foundation/Foundation.h>
#include <map>
#include <mutex>
#include <chrono>
#include <iomanip>
#include <sstream>
// Objective-C delegate for IOBluetoothDeviceInquiry
@interface BluetoothInquiryDelegate : NSObject <IOBluetoothDeviceInquiryDelegate>
{
ClassicBluetooth::DeviceFoundCallback deviceFoundCallback;
ClassicBluetooth::InquiryCompleteCallback inquiryCompleteCallback;
std::vector<ClassicBluetooth::ClassicDevice>* discoveredDevices;
std::mutex* devicesMutex;
}
- (instancetype)initWithCallbacks:(ClassicBluetooth::DeviceFoundCallback)foundCb
completeCb:(ClassicBluetooth::InquiryCompleteCallback)completeCb
devices:(std::vector<ClassicBluetooth::ClassicDevice>*)devices
mutex:(std::mutex*)mutex;
@end
@implementation BluetoothInquiryDelegate
- (instancetype)initWithCallbacks:(ClassicBluetooth::DeviceFoundCallback)foundCb
completeCb:(ClassicBluetooth::InquiryCompleteCallback)completeCb
devices:(std::vector<ClassicBluetooth::ClassicDevice>*)devices
mutex:(std::mutex*)mutex {
self = [super init];
if (self) {
deviceFoundCallback = foundCb;
inquiryCompleteCallback = completeCb;
discoveredDevices = devices;
devicesMutex = mutex;
}
return self;
}
- (void)deviceInquiryDeviceFound:(IOBluetoothDeviceInquiry*)sender
device:(IOBluetoothDevice*)device {
ClassicBluetooth::ClassicDevice classicDevice;
// Basic info
classicDevice.address = [[device addressString] UTF8String];
classicDevice.name = device.name ? [device.name UTF8String] : "<unnamed>";
classicDevice.rssi = [device rawRSSI];
classicDevice.is_connected = [device isConnected];
classicDevice.is_paired = [device isPaired];
// Device class
BluetoothClassOfDevice cod = [device classOfDevice];
classicDevice.device_class.class_of_device = cod;
classicDevice.device_class.major_class = ClassicBluetooth::decode_device_class_major(cod);
classicDevice.device_class.minor_class = ClassicBluetooth::decode_device_class_minor(cod);
// Timestamp
auto now = std::chrono::system_clock::now();
auto now_time_t = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf;
gmtime_r(&now_time_t, &tm_buf);
std::ostringstream oss;
oss << std::put_time(&tm_buf, "%Y-%m-%d %H:%M:%S");
classicDevice.last_seen = oss.str();
// Get SDP services (if available)
NSArray *services = [device services];
if (services && [services count] > 0) {
for (IOBluetoothSDPServiceRecord *record in services) {
ClassicBluetooth::SDPService sdpService;
// Get RFCOMM channel (for SPP services)
BluetoothRFCOMMChannelID channelID;
if ([record getRFCOMMChannelID:&channelID] == kIOReturnSuccess) {
sdpService.rfcomm_channel = channelID;
} else {
sdpService.rfcomm_channel = 0;
}
// Try to get service UUID from service record
BluetoothSDPServiceRecordHandle handle;
if ([record getServiceRecordHandle:&handle] == kIOReturnSuccess) {
char uuidStr[10];
snprintf(uuidStr, sizeof(uuidStr), "%04X", (uint16_t)handle);
sdpService.uuid = uuidStr;
}
// Get service name if available
NSString *serviceName = nil;
IOBluetoothSDPDataElement *nameElement = [record getAttributeDataElement:kBluetoothSDPAttributeIdentifierServiceName];
if (nameElement) {
serviceName = [nameElement getStringValue];
}
if (serviceName) {
sdpService.name = [serviceName UTF8String];
} else if (!sdpService.uuid.empty()) {
sdpService.name = ClassicBluetooth::decode_service_uuid(sdpService.uuid);
} else {
sdpService.name = "Unknown Service";
}
classicDevice.services.push_back(sdpService);
}
}
// Store device
{
std::lock_guard<std::mutex> lock(*devicesMutex);
discoveredDevices->push_back(classicDevice);
}
// Callback
if (deviceFoundCallback) {
deviceFoundCallback(classicDevice);
}
}
- (void)deviceInquiryComplete:(IOBluetoothDeviceInquiry*)sender
error:(IOReturn)error
aborted:(BOOL)aborted {
if (inquiryCompleteCallback) {
bool success = (error == kIOReturnSuccess && !aborted);
std::string errorMsg;
if (!success) {
if (aborted) {
errorMsg = "Inquiry aborted";
} else {
errorMsg = "Inquiry failed with error code: " + std::to_string(error);
}
}
inquiryCompleteCallback(success, errorMsg);
}
}
@end
// PIMPL implementation
namespace ClassicBluetooth {
class Scanner::Impl {
public:
IOBluetoothDeviceInquiry *inquiry;
BluetoothInquiryDelegate *delegate;
std::vector<ClassicDevice> discovered_devices;
std::mutex devices_mutex;
DeviceFoundCallback device_found_callback;
InquiryCompleteCallback inquiry_complete_callback;
bool is_scanning;
Impl() : inquiry(nil), delegate(nil), is_scanning(false) {
@autoreleasepool {
delegate = [[BluetoothInquiryDelegate alloc]
initWithCallbacks:nullptr
completeCb:nullptr
devices:&discovered_devices
mutex:&devices_mutex];
inquiry = [[IOBluetoothDeviceInquiry alloc] initWithDelegate:delegate];
}
}
~Impl() {
@autoreleasepool {
if (inquiry) {
[inquiry stop];
inquiry = nil;
}
delegate = nil;
}
}
};
// Scanner implementation
Scanner::Scanner() : pImpl(new Impl()) {}
Scanner::~Scanner() {
delete pImpl;
}
bool Scanner::start_inquiry(int duration_seconds) {
@autoreleasepool {
if (pImpl->is_scanning) {
return false; // Already scanning
}
// Clear previous results
{
std::lock_guard<std::mutex> lock(pImpl->devices_mutex);
pImpl->discovered_devices.clear();
}
[pImpl->inquiry setInquiryLength:duration_seconds];
[pImpl->inquiry setUpdateNewDeviceNames:YES];
IOReturn result = [pImpl->inquiry start];
if (result == kIOReturnSuccess) {
pImpl->is_scanning = true;
return true;
}
return false;
}
}
void Scanner::stop_inquiry() {
@autoreleasepool {
if (pImpl->inquiry) {
[pImpl->inquiry stop];
pImpl->is_scanning = false;
}
}
}
bool Scanner::is_scanning() const {
return pImpl->is_scanning;
}
void Scanner::set_device_found_callback(DeviceFoundCallback callback) {
pImpl->device_found_callback = callback;
@autoreleasepool {
pImpl->delegate = [[BluetoothInquiryDelegate alloc]
initWithCallbacks:callback
completeCb:pImpl->inquiry_complete_callback
devices:&pImpl->discovered_devices
mutex:&pImpl->devices_mutex];
[pImpl->inquiry setDelegate:pImpl->delegate];
}
}
void Scanner::set_inquiry_complete_callback(InquiryCompleteCallback callback) {
pImpl->inquiry_complete_callback = callback;
@autoreleasepool {
pImpl->delegate = [[BluetoothInquiryDelegate alloc]
initWithCallbacks:pImpl->device_found_callback
completeCb:callback
devices:&pImpl->discovered_devices
mutex:&pImpl->devices_mutex];
[pImpl->inquiry setDelegate:pImpl->delegate];
}
}
std::vector<ClassicDevice> Scanner::get_discovered_devices() {
std::lock_guard<std::mutex> lock(pImpl->devices_mutex);
return pImpl->discovered_devices;
}
std::vector<SDPService> Scanner::query_sdp_services(const std::string& address) {
std::vector<SDPService> services;
@autoreleasepool {
NSString *addressStr = [NSString stringWithUTF8String:address.c_str()];
IOBluetoothDevice *device = [IOBluetoothDevice deviceWithAddressString:addressStr];
if (!device) {
return services;
}
// Perform SDP query
IOReturn result = [device performSDPQuery:nil];
if (result != kIOReturnSuccess) {
return services;
}
NSArray *sdpRecords = [device services];
if (!sdpRecords) {
return services;
}
for (IOBluetoothSDPServiceRecord *record in sdpRecords) {
SDPService service;
// Service name
NSString *serviceName = [record getServiceName];
if (serviceName) {
service.name = [serviceName UTF8String];
}
// RFCOMM channel
BluetoothRFCOMMChannelID channelID;
if ([record getRFCOMMChannelID:&channelID] == kIOReturnSuccess) {
service.rfcomm_channel = channelID;
}
// Try to get service UUID
BluetoothSDPServiceRecordHandle handle;
if ([record getServiceRecordHandle:&handle] == kIOReturnSuccess) {
char uuidStr[10];
snprintf(uuidStr, sizeof(uuidStr), "%04X", (uint16_t)handle);
service.uuid = uuidStr;
service.name = decode_service_uuid(service.uuid);
}
services.push_back(service);
}
}
return services;
}
DeviceClass Scanner::get_device_class(const std::string& address) {
DeviceClass devClass;
@autoreleasepool {
NSString *addressStr = [NSString stringWithUTF8String:address.c_str()];
IOBluetoothDevice *device = [IOBluetoothDevice deviceWithAddressString:addressStr];
if (device) {
BluetoothClassOfDevice cod = [device classOfDevice];
devClass.class_of_device = cod;
devClass.major_class = decode_device_class_major(cod);
devClass.minor_class = decode_device_class_minor(cod);
}
}
return devClass;
}
// Utility functions
std::string decode_device_class_major(uint32_t cod) {
uint8_t major = (cod >> 8) & 0x1F;
switch (major) {
case 0x00: return "Miscellaneous";
case 0x01: return "Computer";
case 0x02: return "Phone";
case 0x03: return "LAN/Network Access Point";
case 0x04: return "Audio/Video";
case 0x05: return "Peripheral";
case 0x06: return "Imaging";
case 0x07: return "Wearable";
case 0x08: return "Toy";
case 0x09: return "Health";
case 0x1F: return "Uncategorized";
default: return "Unknown";
}
}
std::string decode_device_class_minor(uint32_t cod) {
uint8_t major = (cod >> 8) & 0x1F;
uint8_t minor = (cod >> 2) & 0x3F;
// Computer minor classes
if (major == 0x01) {
switch (minor) {
case 0x00: return "Uncategorized";
case 0x01: return "Desktop Workstation";
case 0x02: return "Server-class Computer";
case 0x03: return "Laptop";
case 0x04: return "Handheld PC/PDA";
case 0x05: return "Palm-size PC/PDA";
case 0x06: return "Wearable Computer";
case 0x07: return "Tablet";
default: return "Unknown Computer";
}
}
// Phone minor classes
if (major == 0x02) {
switch (minor) {
case 0x00: return "Uncategorized";
case 0x01: return "Cellular";
case 0x02: return "Cordless";
case 0x03: return "Smartphone";
case 0x04: return "Wired Modem/Voice Gateway";
case 0x05: return "Common ISDN Access";
default: return "Unknown Phone";
}
}
// Audio/Video minor classes
if (major == 0x04) {
switch (minor) {
case 0x00: return "Uncategorized";
case 0x01: return "Wearable Headset";
case 0x02: return "Hands-free";
case 0x04: return "Microphone";
case 0x05: return "Loudspeaker";
case 0x06: return "Headphones";
case 0x07: return "Portable Audio";
case 0x08: return "Car Audio";
case 0x09: return "Set-top Box";
case 0x0A: return "HiFi Audio";
case 0x0B: return "VCR";
case 0x0C: return "Video Camera";
case 0x0D: return "Camcorder";
case 0x0E: return "Video Monitor";
case 0x0F: return "Video Display and Loudspeaker";
case 0x10: return "Video Conferencing";
case 0x12: return "Gaming/Toy";
default: return "Unknown Audio/Video";
}
}
return "Unknown";
}
std::string decode_service_uuid(const std::string& uuid) {
static const std::map<std::string, std::string> SERVICE_MAP = {
{"1101", "Serial Port (SPP)"},
{"1102", "LAN Access Using PPP"},
{"1103", "Dialup Networking (DUN)"},
{"1104", "IrMC Sync"},
{"1105", "OBEX Object Push"},
{"1106", "OBEX File Transfer"},
{"1107", "IrMC Sync Command"},
{"1108", "Headset"},
{"1109", "Cordless Telephony"},
{"110A", "Audio Source"},
{"110B", "Audio Sink"},
{"110C", "A/V Remote Control Target"},
{"110D", "Advanced Audio Distribution"},
{"110E", "A/V Remote Control"},
{"110F", "A/V Remote Control Controller"},
{"1112", "Headset Audio Gateway"},
{"1115", "PANU"},
{"1116", "NAP"},
{"1117", "GN"},
{"1118", "Direct Printing"},
{"111E", "Handsfree"},
{"111F", "Handsfree Audio Gateway"},
{"1200", "PnP Information"},
{"1201", "Generic Networking"},
{"1202", "Generic File Transfer"},
{"1203", "Generic Audio"},
{"1204", "Generic Telephony"},
{"1812", "Human Interface Device (HID)"}
};
auto it = SERVICE_MAP.find(uuid);
if (it != SERVICE_MAP.end()) {
return it->second;
}
return "Unknown Service (" + uuid + ")";
}
} // namespace ClassicBluetooth