swiftmacosbluetoothmacos-sierraiobluetooth

IOBluetooth register for channel open notification not firing when channel is opened


I'm working on a project that requires bluetooth 2.1 and I am trying to make a testing client to connect to the hardware.

So I have been pouring through the 'docs' for IOBluetooth and have managed to broadcast an SDP Service Record.

Opening a connection on the device (android) and writing data to my computer works (according to the remote device) but my client tool is not actually calling the selector for the notification...

That is until I stop the client program and start it again, in which case it fires immediately.

There is also little documentation on how the notifications are fired, whether they are async or not.

I have been wracking my brain for a week, thoughts?

Dummy Client Code:

BTSocketSession.swift

class BTSocketSession {

    var lock : DispatchSemaphore = DispatchSemaphore(value: 0);
    var serviceRecord : IOBluetoothSDPServiceRecord? = nil;
    var channel : IOBluetoothRFCOMMChannel? = nil;
    var notification : IOBluetoothUserNotification? = nil;
    var serverID : BluetoothRFCOMMChannelID = 255;
    var done = false;

    init(serviceRecordPListPath: String) {
        print("Loading Service Dictionary...");
        let serviceDict : NSMutableDictionary = NSMutableDictionary(contentsOfFile: serviceRecordPListPath)!;
        print("Done.");
        print("Publishing service...");
        self.serviceRecord = IOBluetoothSDPServiceRecord.publishedServiceRecord(with: serviceDict as! [AnyHashable : Any]);
        print("Done.");
    }

    func listen() {
        var id = BluetoothRFCOMMChannelID();
        self.serviceRecord?.getRFCOMMChannelID(&id);
        self.serverID = id;
        print(serviceRecord ?? "Derp");
        print(id);
        self.notification = IOBluetoothRFCOMMChannel.register(forChannelOpenNotifications: self, selector: #selector(self.rfcommChannelOpen(notification:channel:)), withChannelID: id, direction: kIOBluetoothUserNotificationChannelDirectionIncoming);
        self.lock.wait();
    }

    func cancel() {
        print("Cancelling service advertisement...");
        if (self.serviceRecord == nil) {
            self.removeServiceRecord();
            self.lock.signal();
            print("Done.");
        } else {
            print("Error: No service broadcasting.");
        }
    }

    private func removeServiceRecord() {
        self.serviceRecord?.remove();
        self.serviceRecord = nil;
        self.notification?.unregister();
        self.notification = nil;
    }

    @objc func rfcommChannelOpen(notification: IOBluetoothUserNotification, channel: IOBluetoothRFCOMMChannel) {
        if (self.serverID != channel.getID()) {
            return;
        }
        print("Notification Recieved!!!");
        print(channel);
        print(notification);
        self.done = true;
        notification.unregister();
        self.removeServiceRecord();
        self.channel = channel;
        self.lock.signal();
    }

}

main.swift

func main() {
    let q = DispatchQueue(label: "Demo");
    let s = DispatchSemaphore(value: 0);
    let c = BTSocketSession(serviceRecordPListPath: "/path/to/plist");

    q.async {
        print("Waiting for connection event...");
        c.listen();
        print("Done.");
        print("Releasing main thread...");
        s.signal()
        print("Done.");
    }

    s.wait();
    print("Closing channel...");
    c.channel?.close();
    c.cancel();
    print("Done.");
    print("Exiting...");

}

main();
print("Wellp");

Solution

  • Solved:

    As it turns out, when making a command-line / headless application in OSX you must use the RunLoop interface to properly register for System Notifications like bluetooth channel open notifications.

    Solution found here.

    Changes:

    The following allows the program to work as expected.

    func main() {
        let q = DispatchQueue(label: "Demo");
        let c = BTSocketSession(serviceRecordPListPath: "/path/to/plist");
    
        q.async {
            print("Waiting for connection event...");
            c.listen();
            print("Done.");
            print("Releasing main thread...");
            c.channel.close();
            CFRunLoopStop(RunLoop.main.getCFRunLoop());
            print("Done.");
        }
        RunLoop.main.run();
        print("Exiting...");
    }
    
    main();
    print("Wellp");
    

    Life is Wonderful again =) I hope you have a good day!