Initial commit
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user