#import "classic_bluetooth.h" #import #import #include #include #include #include #include // Objective-C delegate for IOBluetoothDeviceInquiry @interface BluetoothInquiryDelegate : NSObject { ClassicBluetooth::DeviceFoundCallback deviceFoundCallback; ClassicBluetooth::InquiryCompleteCallback inquiryCompleteCallback; std::vector* discoveredDevices; std::mutex* devicesMutex; } - (instancetype)initWithCallbacks:(ClassicBluetooth::DeviceFoundCallback)foundCb completeCb:(ClassicBluetooth::InquiryCompleteCallback)completeCb devices:(std::vector*)devices mutex:(std::mutex*)mutex; @end @implementation BluetoothInquiryDelegate - (instancetype)initWithCallbacks:(ClassicBluetooth::DeviceFoundCallback)foundCb completeCb:(ClassicBluetooth::InquiryCompleteCallback)completeCb devices:(std::vector*)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] : ""; 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 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 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 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 Scanner::get_discovered_devices() { std::lock_guard lock(pImpl->devices_mutex); return pImpl->discovered_devices; } std::vector Scanner::query_sdp_services(const std::string& address) { std::vector 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 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