442 lines
14 KiB
Plaintext
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
|