Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

USB Connection Delegate on Swift

Tags:

macos

swift

usb

Is there a delegate in Swift that would let my class know when new devices are plugged in via the computer's USB? I would like to know when a new device becomes available to my program.

like image 574
Jacobo Koenig Avatar asked Aug 17 '16 18:08

Jacobo Koenig


2 Answers

Eric Aya's answer is already quite good, but here's a Swift 3 adaptation. I wrapped most of the ugly stuff in a USBWatcher class; set yourself as the delegate of this object to receive notifications.

You can copy/paste the following into a playground to see it work — the example just logs a message to the console when devices are connected/disconnected.

It's unfortunate that the IOKit APIs haven't gotten the same Swift-ifying treatment that some other C APIs have been (e.g. CoreGraphics). io_name_t is a clunky tuple instead of a proper struct, the way C structs are usually imported to Swift; io_object_t isn't a real reference type, so it can't take advantage of ARC. Perhaps in the future this will change — if you'd like to see a better Swift API, you should file an enhancement request.

import Foundation
import IOKit
import IOKit.usb

public protocol USBWatcherDelegate: class {
    /// Called on the main thread when a device is connected.
    func deviceAdded(_ device: io_object_t)

    /// Called on the main thread when a device is disconnected.
    func deviceRemoved(_ device: io_object_t)
}

/// An object which observes USB devices added and removed from the system.
/// Abstracts away most of the ugliness of IOKit APIs.
public class USBWatcher {
    private weak var delegate: USBWatcherDelegate?
    private let notificationPort = IONotificationPortCreate(kIOMasterPortDefault)
    private var addedIterator: io_iterator_t = 0
    private var removedIterator: io_iterator_t = 0

    public init(delegate: USBWatcherDelegate) {
        self.delegate = delegate

        func handleNotification(instance: UnsafeMutableRawPointer?, _ iterator: io_iterator_t) {
            let watcher = Unmanaged<USBWatcher>.fromOpaque(instance!).takeUnretainedValue()
            let handler: ((io_iterator_t) -> Void)?
            switch iterator {
            case watcher.addedIterator: handler = watcher.delegate?.deviceAdded
            case watcher.removedIterator: handler = watcher.delegate?.deviceRemoved
            default: assertionFailure("received unexpected IOIterator"); return
            }
            while case let device = IOIteratorNext(iterator), device != IO_OBJECT_NULL {
                handler?(device)
                IOObjectRelease(device)
            }
        }

        let query = IOServiceMatching(kIOUSBDeviceClassName)
        let opaqueSelf = Unmanaged.passUnretained(self).toOpaque()

        // Watch for connected devices.
        IOServiceAddMatchingNotification(
            notificationPort, kIOMatchedNotification, query,
            handleNotification, opaqueSelf, &addedIterator)

        handleNotification(instance: opaqueSelf, addedIterator)

        // Watch for disconnected devices.
        IOServiceAddMatchingNotification(
            notificationPort, kIOTerminatedNotification, query,
            handleNotification, opaqueSelf, &removedIterator)

        handleNotification(instance: opaqueSelf, removedIterator)

        // Add the notification to the main run loop to receive future updates.
        CFRunLoopAddSource(
            CFRunLoopGetMain(),
            IONotificationPortGetRunLoopSource(notificationPort).takeUnretainedValue(),
            .commonModes)
    }

    deinit {
        IOObjectRelease(addedIterator)
        IOObjectRelease(removedIterator)
        IONotificationPortDestroy(notificationPort)
    }
}

extension io_object_t {
    /// - Returns: The device's name.
    func name() -> String? {
        let buf = UnsafeMutablePointer<io_name_t>.allocate(capacity: 1)
        defer { buf.deallocate(capacity: 1) }
        return buf.withMemoryRebound(to: CChar.self, capacity: MemoryLayout<io_name_t>.size) {
            if IORegistryEntryGetName(self, $0) == KERN_SUCCESS {
                return String(cString: $0)
            }
            return nil
        }
    }
}


import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

class Example: USBWatcherDelegate {
    private var usbWatcher: USBWatcher!
    init() {
        usbWatcher = USBWatcher(delegate: self)
    }

    func deviceAdded(_ device: io_object_t) {
        print("device added: \(device.name() ?? "<unknown>")")
    }

    func deviceRemoved(_ device: io_object_t) {
        print("device removed: \(device.name() ?? "<unknown>")")
    }
}

let example = Example()
like image 66
jtbandes Avatar answered Nov 18 '22 09:11

jtbandes


This answer worked for me https://stackoverflow.com/a/35788694 but it needed some adaptation, like creating a bridging header to import some specific IOKit parts.

First, add IOKit.framework to your project (click "+" in "Linked Frameworks and Libraries").

Then create a new empty ".m" file, whatever its name. Xcode will then ask if it should make a "bridging header". Say YES.

Ignore the ".m" file. In the new "YOURAPPNAME-Bridging-Header.h" file that Xcode just created, add the following lines:

#include <IOKit/IOKitLib.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/hid/IOHIDKeys.h>

Now you can use the code in the linked answer. Here's a simplified version:

class USBDetector {
    class func monitorUSBEvent() {
        var portIterator: io_iterator_t = 0
        let matchingDict = IOServiceMatching(kIOUSBDeviceClassName)
        let gNotifyPort: IONotificationPortRef = IONotificationPortCreate(kIOMasterPortDefault)
        let runLoopSource: Unmanaged<CFRunLoopSource>! = IONotificationPortGetRunLoopSource(gNotifyPort)
        let gRunLoop: CFRunLoop! = CFRunLoopGetCurrent()
        CFRunLoopAddSource(gRunLoop, runLoopSource.takeRetainedValue(), kCFRunLoopDefaultMode)
        let observer = UnsafeMutablePointer<Void>(unsafeAddressOf(self))
        _ = IOServiceAddMatchingNotification(gNotifyPort,
                                              kIOMatchedNotification,
                                              matchingDict,
                                              deviceAdded,
                                              observer,
                                              &portIterator)
        deviceAdded(nil, iterator: portIterator)
        _ = IOServiceAddMatchingNotification(gNotifyPort,
                                              kIOTerminatedNotification,
                                              matchingDict,
                                              deviceRemoved,
                                              observer,
                                              &portIterator)
        deviceRemoved(nil, iterator: portIterator)
    }
}

func deviceAdded(refCon: UnsafeMutablePointer<Void>, iterator: io_iterator_t) {
    var kr: kern_return_t = KERN_FAILURE
    while case let usbDevice = IOIteratorNext(iterator) where usbDevice != 0 {
        let deviceNameAsCFString = UnsafeMutablePointer<io_name_t>.alloc(1)
        defer {deviceNameAsCFString.dealloc(1)}
        kr = IORegistryEntryGetName(usbDevice, UnsafeMutablePointer(deviceNameAsCFString))
        if kr != KERN_SUCCESS {
            deviceNameAsCFString.memory.0 = 0
        }
        let deviceName = String.fromCString(UnsafePointer(deviceNameAsCFString))
        print("Active device: \(deviceName!)")
        IOObjectRelease(usbDevice)
    }
}

func deviceRemoved(refCon: UnsafeMutablePointer<Void>, iterator: io_iterator_t) {
    // ...
}

Note: deviceAdded and deviceRemoved need to be functions (not methods).

To use this code, just launch the observer:

USBDetector.monitorUSBEvent()

This will list the currently plugged devices, and on every new USB device plug/unplug event it will print the device name.

like image 38
Eric Aya Avatar answered Nov 18 '22 08:11

Eric Aya