Created
March 7, 2026 09:17
-
-
Save JJTech0130/fae6b6ee6ae4232172a9188fb199d5d9 to your computer and use it in GitHub Desktop.
Creating fake/virtual USB devices on macOS using IOUSBHostControllerInterface
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ConsumerKeys.swift | |
| // Boot-protocol keyboard that reports keycodes in Consumer page (0x0C). | |
| // iOS maps consumer page usages received this way to system actions. | |
| import Foundation | |
| public final class ConsumerKeys: SyntheticHID { | |
| // MARK: - Consumer usages sent as boot-keyboard keycodes (page 0x0C, ≤ 0xFF) | |
| public enum Key: UInt8 { | |
| case acHome = 0x40 // Consumer "Menu" → iOS home button | |
| case mute = 0xE2 | |
| case volumeUp = 0xE9 | |
| case volumeDown = 0xEA | |
| case playPause = 0xCD | |
| case nextTrack = 0xB5 | |
| case prevTrack = 0xB6 | |
| } | |
| // MARK: - Init | |
| public init() { | |
| super.init( | |
| reportDescriptor: Self.reportDescriptor, | |
| reportSize: 8, | |
| vendorID: 0x05AC, | |
| productID: 0x0002, | |
| manufacturer: "Apple Inc.", | |
| product: "Virtual Consumer Control", | |
| interfaceSubClass: 0x01, | |
| interfaceProtocol: 0x01) | |
| } | |
| // MARK: - Key injection | |
| public func keyDown(_ key: Key) { | |
| sendReport([0, 0, key.rawValue, 0, 0, 0, 0, 0]) | |
| } | |
| public func keyUp() { | |
| clearReport() | |
| } | |
| // MARK: - Report descriptor | |
| // Minimal boot-keyboard shell with Consumer page (0x0C) keycodes. | |
| private static let reportDescriptor: [UInt8] = [ | |
| 0x05, 0x0C, // Usage Page (Consumer) | |
| 0x09, 0x01, // Usage (Consumer Control) | |
| 0xA1, 0x01, // Collection (Application) | |
| 0x19, 0x00, // Usage Minimum (0) | |
| 0x29, 0xFF, // Usage Maximum (255) | |
| 0x15, 0x00, // Logical Minimum (0) | |
| 0x25, 0xFF, // Logical Maximum (255) | |
| 0x75, 0x08, // Report Size (8) | |
| 0x95, 0x08, // Report Count (8) | |
| 0x81, 0x00, // Input (Data, Array) | |
| 0xC0, // End Collection | |
| ] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // A synthetic USB HID device that delivers arbitrary fixed-size HID reports. | |
| // Build descriptors with SyntheticHID.Descriptors or supply your own. | |
| import Foundation | |
| // MARK: - SyntheticHID | |
| /// A synthetic USB HID device backed by SyntheticIOUSBDevice. | |
| /// Inject reports with `sendReport(_:)` / `sendReportAndRelease(_:)`. | |
| open class SyntheticHID: SyntheticIOUSBDevice { | |
| private var currentReportData: [UInt8] | |
| private let emptyReport: [UInt8] | |
| // MARK: Init | |
| /// - Parameters: | |
| /// - reportDescriptor: Raw HID report descriptor bytes. | |
| /// - reportSize: Size (in bytes) of a single input report. | |
| /// - vendorID: USB vendor ID (little-endian). Default: Apple (0x05AC). | |
| /// - productID: USB product ID (little-endian). Default: 0x0001. | |
| /// - manufacturer: Manufacturer string. | |
| /// - product: Product string. | |
| /// - interfaceSubClass: bInterfaceSubClass (0x01 = Boot Interface, 0x00 = none). | |
| /// - interfaceProtocol: bInterfaceProtocol (0x01 = Keyboard, 0x02 = Mouse, 0x00 = none). | |
| public init(reportDescriptor: [UInt8], | |
| reportSize: Int, | |
| vendorID: UInt16 = 0x05AC, | |
| productID: UInt16 = 0x0001, | |
| manufacturer: String = "Apple Inc.", | |
| product: String = "Virtual HID Device", | |
| interfaceSubClass: UInt8 = 0x01, | |
| interfaceProtocol: UInt8 = 0x01) { | |
| emptyReport = [UInt8](repeating: 0, count: reportSize) | |
| currentReportData = emptyReport | |
| let device = Self.makeDeviceDescriptor(vendorID: vendorID, productID: productID) | |
| let config = Self.makeConfigDescriptor(hidReportLen: reportDescriptor.count, | |
| subClass: interfaceSubClass, | |
| protocol_: interfaceProtocol) | |
| let descs = USBDeviceDescriptors(device: device, configuration: config, | |
| hidReport: reportDescriptor, | |
| manufacturer: manufacturer, product: product) | |
| super.init(descriptors: descs) | |
| } | |
| // MARK: Report injection | |
| /// Set the report that will be returned on the next interrupt IN poll. | |
| public func sendReport(_ bytes: [UInt8]) { | |
| currentReportData = bytes | |
| } | |
| /// Reset the current report to all zeros. | |
| public func clearReport() { | |
| currentReportData = emptyReport | |
| } | |
| // MARK: SyntheticIOUSBDevice overrides | |
| open override func interruptINData(maxLength: Int) -> [UInt8] { | |
| Array(currentReportData.prefix(maxLength)) | |
| } | |
| open override func getReport(maxLength: Int) -> [UInt8] { | |
| Array(emptyReport.prefix(maxLength)) | |
| } | |
| // MARK: Descriptor builders | |
| private static func makeDeviceDescriptor(vendorID: UInt16, productID: UInt16) -> [UInt8] { | |
| [ | |
| 18, // bLength | |
| 0x01, // bDescriptorType: DEVICE | |
| 0x00, 0x02, // bcdUSB: USB 2.0 | |
| 0x00, // bDeviceClass: defined by interface | |
| 0x00, // bDeviceSubClass | |
| 0x00, // bDeviceProtocol | |
| 8, // bMaxPacketSize0 | |
| UInt8(vendorID & 0xFF), UInt8(vendorID >> 8), | |
| UInt8(productID & 0xFF), UInt8(productID >> 8), | |
| 0x00, 0x01, // bcdDevice: 1.00 | |
| 0x01, // iManufacturer | |
| 0x02, // iProduct | |
| 0x00, // iSerialNumber: none | |
| 0x01, // bNumConfigurations | |
| ] | |
| } | |
| /// Builds a standard single-interface HID configuration descriptor | |
| /// (Config 9B + Interface 9B + HID 9B + EP 7B = 34B total). | |
| private static func makeConfigDescriptor(hidReportLen: Int, | |
| subClass: UInt8 = 0x01, | |
| protocol_: UInt8 = 0x01) -> [UInt8] { | |
| let lo = UInt8(hidReportLen & 0xFF) | |
| let hi = UInt8((hidReportLen >> 8) & 0xFF) | |
| return [ | |
| // Configuration descriptor (9 bytes) | |
| 9, 0x02, 34, 0, // bLength, bDescriptorType, wTotalLength LE | |
| 0x01, // bNumInterfaces | |
| 0x01, // bConfigurationValue | |
| 0x00, // iConfiguration | |
| 0xA0, // bmAttributes: bus powered + remote wakeup | |
| 50, // bMaxPower: 100 mA (2 mA units) | |
| // Interface descriptor (9 bytes) | |
| 9, 0x04, // bLength, bDescriptorType: INTERFACE | |
| 0x00, 0x00, // bInterfaceNumber, bAlternateSetting | |
| 0x01, // bNumEndpoints | |
| 0x03, // bInterfaceClass: HID | |
| subClass, // bInterfaceSubClass | |
| protocol_, // bInterfaceProtocol | |
| 0x00, // iInterface | |
| // HID descriptor (9 bytes) | |
| 9, 0x21, // bLength, bDescriptorType: HID | |
| 0x11, 0x01, // bcdHID: 1.11 | |
| 0x00, // bCountryCode | |
| 0x01, // bNumDescriptors | |
| 0x22, // bDescriptorType[0]: Report | |
| lo, hi, // wDescriptorLength[0] | |
| // Endpoint descriptor (7 bytes) — EP1 IN, Interrupt | |
| 7, 0x05, // bLength, bDescriptorType: ENDPOINT | |
| 0x81, // bEndpointAddress: IN EP1 | |
| 0x03, // bmAttributes: Interrupt | |
| 8, 0, // wMaxPacketSize: 8 bytes LE | |
| 10, // bInterval: 10 ms | |
| ] | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Generic synthetic USB device backed by IOUSBHostControllerInterface. | |
| // Subclass and override `interruptINData(maxLength:)` and `getReport(maxLength:)` | |
| // to deliver custom data to the host. | |
| // | |
| // Entitlement required: com.apple.developer.usb.host-controller-interface | |
| import Foundation | |
| import IOUSBHost | |
| // MARK: - USB descriptor bundle | |
| public struct USBDeviceDescriptors { | |
| public let device: [UInt8] | |
| public let configuration: [UInt8] | |
| public let hidReport: [UInt8] | |
| public let manufacturer: String | |
| public let product: String | |
| public init( | |
| device: [UInt8], configuration: [UInt8], hidReport: [UInt8], | |
| manufacturer: String, product: String | |
| ) { | |
| self.device = device | |
| self.configuration = configuration | |
| self.hidReport = hidReport | |
| self.manufacturer = manufacturer | |
| self.product = product | |
| } | |
| } | |
| // MARK: - USB / HID request constants | |
| private let kUSBReqGetDescriptor: UInt8 = 0x06 | |
| private let kUSBReqGetConfig: UInt8 = 0x08 | |
| private let kUSBReqGetInterface: UInt8 = 0x0A | |
| private let kUSBDescDevice: UInt8 = 0x01 | |
| private let kUSBDescConfig: UInt8 = 0x02 | |
| private let kUSBDescString: UInt8 = 0x03 | |
| private let kHIDReqGetDescriptor: UInt8 = 0x06 | |
| private let kHIDDescHID: UInt8 = 0x21 | |
| private let kHIDDescReport: UInt8 = 0x22 | |
| private let kMsgTypeMask: UInt32 = 0x3F | |
| private let kMsgValid: UInt32 = (1 << 15) | |
| // MARK: - SyntheticIOUSBDevice | |
| /// A generic synthetic USB 2.0 full-speed device presented via IOUSBHostControllerInterface. | |
| /// Descriptors are supplied at init time. Subclass to customise data delivery. | |
| open class SyntheticIOUSBDevice: NSObject { | |
| // MARK: Properties | |
| public let descriptors: USBDeviceDescriptors | |
| private var controller: IOUSBHostControllerInterface? | |
| private var deviceSMs: [Int: IOUSBHostCIDeviceStateMachine] = [:] | |
| private var endpointSMs: [Int: IOUSBHostCIEndpointStateMachine] = [:] | |
| private var frameNumber: UInt64 = 0 | |
| private var portSM: IOUSBHostCIPortStateMachine? | |
| private var deviceConnected = false | |
| private var pendingResponse: Data? | |
| private let string0: [UInt8] = [4, 0x03, 0x09, 0x04] // English (United States) language ID | |
| private let stringManuf: [UInt8] | |
| private let stringProd: [UInt8] | |
| // MARK: Init | |
| public init(descriptors: USBDeviceDescriptors) { | |
| self.descriptors = descriptors | |
| self.stringManuf = Self.makeStringDescriptor(descriptors.manufacturer) | |
| self.stringProd = Self.makeStringDescriptor(descriptors.product) | |
| super.init() | |
| } | |
| // MARK: Override points | |
| /// Called each time the host polls the interrupt IN endpoint (EP 0x81). | |
| /// Return the bytes to deliver; the result is clamped to `maxLength`. | |
| open func interruptINData(maxLength: Int) -> [UInt8] { [] } | |
| /// Called for a HID GET_REPORT class request (bmRequestType 0xA1). | |
| open func getReport(maxLength: Int) -> [UInt8] { [] } | |
| // MARK: Start / Stop | |
| public func start() throws { | |
| var err: NSError? | |
| let ci = IOUSBHostControllerInterface( | |
| __capabilities: buildCapabilities(), | |
| queue: nil, | |
| interruptRateHz: 0, | |
| error: &err, | |
| commandHandler: { [weak self] ci, cmd in self?.handleCommand(ci, cmd) }, | |
| doorbellHandler: { [weak self] ci, db, n in self?.handleDoorbells(ci, db, n) }, | |
| interestHandler: nil) | |
| if let e = err, e.code != 0 { throw e } | |
| guard let ci else { | |
| throw NSError( | |
| domain: "SyntheticIOUSBDevice", code: -1, | |
| userInfo: [ | |
| NSLocalizedDescriptionKey: "Failed to create IOUSBHostControllerInterface" | |
| ]) | |
| } | |
| controller = ci | |
| print("[\(typeName)] Controller created — UUID: \(ci.uuid.uuidString)") | |
| } | |
| public func stop() { | |
| controller?.destroy() | |
| controller = nil | |
| print("[\(typeName)] Stopped.") | |
| } | |
| private var typeName: String { String(describing: type(of: self)) } | |
| // MARK: Capabilities | |
| private func buildCapabilities() -> Data { | |
| var ctlCap = IOUSBHostCIMessage() | |
| ctlCap.control = | |
| UInt32(IOUSBHostCIMessageTypeControllerCapabilities.rawValue) | |
| | (1 << 14) // NoResponse | |
| | (1 << 15) // Valid | |
| | (1 << 16) // PortCount = 1 | |
| ctlCap.data0 = (1 << 0) | (2 << 4) // CommandTimeoutThreshold=2s, ConnectionLatency=4ms | |
| var portCap = IOUSBHostCIMessage() | |
| portCap.control = | |
| UInt32(IOUSBHostCIMessageTypePortCapabilities.rawValue) | |
| | (1 << 14) // NoResponse | |
| | (1 << 15) // Valid | |
| | (1 << 16) // PortNumber = 1 | |
| | (0 << 24) // ConnectorType = TypeA | |
| portCap.data0 = UInt32(500 / 8) // MaxPower: 500 mA in 8 mA units | |
| var data = Data(bytes: &ctlCap, count: MemoryLayout<IOUSBHostCIMessage>.size) | |
| data.append(Data(bytes: &portCap, count: MemoryLayout<IOUSBHostCIMessage>.size)) | |
| return data | |
| } | |
| // MARK: Command handler | |
| private func handleCommand(_ ci: IOUSBHostControllerInterface, _ cmdIn: IOUSBHostCIMessage) { | |
| var cmd = cmdIn | |
| let rawType = cmd.control & kMsgTypeMask | |
| let msgType = IOUSBHostCIMessageType(rawValue: rawType) | |
| let name = | |
| IOUSBHostCIMessageTypeToString(msgType).flatMap { String(cString: $0) } | |
| ?? "0x\(String(format: "%02X", rawType))" | |
| print("[\(typeName)] CMD \(name)") | |
| do { | |
| switch msgType { | |
| // ── Controller ──────────────────────────────────────────────── | |
| case IOUSBHostCIMessageTypeControllerPowerOn, | |
| IOUSBHostCIMessageTypeControllerPowerOff, | |
| IOUSBHostCIMessageTypeControllerStart, | |
| IOUSBHostCIMessageTypeControllerPause: | |
| try ci.controllerStateMachine.respond( | |
| toCommand: &cmd, status: IOUSBHostCIMessageStatusSuccess) | |
| // ── Port ────────────────────────────────────────────────────── | |
| case IOUSBHostCIMessageTypePortPowerOn, | |
| IOUSBHostCIMessageTypePortPowerOff, | |
| IOUSBHostCIMessageTypePortResume, | |
| IOUSBHostCIMessageTypePortSuspend, | |
| IOUSBHostCIMessageTypePortReset, | |
| IOUSBHostCIMessageTypePortDisable, | |
| IOUSBHostCIMessageTypePortStatus: | |
| var portErr: NSError? | |
| let psm = ci.getPortStateMachine(forCommand: &cmd, error: &portErr) | |
| if portErr == nil || portErr!.code == 0 { | |
| portSM = psm | |
| try psm.respond(toCommand: &cmd, status: IOUSBHostCIMessageStatusSuccess) | |
| if msgType == IOUSBHostCIMessageTypePortPowerOn { | |
| psm.powered = true | |
| if !deviceConnected { | |
| deviceConnected = true | |
| psm.connected = true | |
| try psm.updateLinkState( | |
| IOUSBHostCILinkStateU0, | |
| speed: IOUSBHostCIDeviceSpeedFull, | |
| inhibitLinkStateChange: false) | |
| print("[\(typeName)] Port 1: device connected (full-speed)") | |
| } | |
| } else if msgType == IOUSBHostCIMessageTypePortReset { | |
| try psm.updateLinkState( | |
| IOUSBHostCILinkStateU0, | |
| speed: IOUSBHostCIDeviceSpeedFull, | |
| inhibitLinkStateChange: false) | |
| } | |
| } else if let e = portErr { | |
| print("[\(typeName)] getPortStateMachine: \(e)") | |
| } | |
| // ── Device ──────────────────────────────────────────────────── | |
| case IOUSBHostCIMessageTypeDeviceCreate: | |
| let dsm = try IOUSBHostCIDeviceStateMachine(__interface: ci, command: &cmd) | |
| let addr = 1 | |
| try dsm.respond( | |
| toCommand: &cmd, status: IOUSBHostCIMessageStatusSuccess, deviceAddress: addr) | |
| deviceSMs[addr] = dsm | |
| print("[\(typeName)] Device at address \(addr)") | |
| case IOUSBHostCIMessageTypeDeviceDestroy, | |
| IOUSBHostCIMessageTypeDeviceStart, | |
| IOUSBHostCIMessageTypeDevicePause, | |
| IOUSBHostCIMessageTypeDeviceUpdate: | |
| let devAddr = Int(cmd.data0 & 0xFF) | |
| if let dsm = deviceSMs[devAddr] { | |
| try dsm.respond(toCommand: &cmd, status: IOUSBHostCIMessageStatusSuccess) | |
| if msgType == IOUSBHostCIMessageTypeDeviceDestroy { | |
| deviceSMs.removeValue(forKey: devAddr) | |
| } | |
| } | |
| // ── Endpoint ────────────────────────────────────────────────── | |
| case IOUSBHostCIMessageTypeEndpointCreate: | |
| let esm = try IOUSBHostCIEndpointStateMachine(__interface: ci, command: &cmd) | |
| try esm.respond(toCommand: &cmd, status: IOUSBHostCIMessageStatusSuccess) | |
| let key = (esm.deviceAddress << 8) | esm.endpointAddress | |
| endpointSMs[key] = esm | |
| print( | |
| "[\(typeName)] Endpoint device=\(esm.deviceAddress) ep=0x\(String(format: "%02X", esm.endpointAddress))" | |
| ) | |
| case IOUSBHostCIMessageTypeEndpointDestroy, | |
| IOUSBHostCIMessageTypeEndpointPause, | |
| IOUSBHostCIMessageTypeEndpointUpdate, | |
| IOUSBHostCIMessageTypeEndpointReset, | |
| IOUSBHostCIMessageTypeEndpointSetNextTransfer: | |
| let devAddr = Int(cmd.data0 & 0xFF) | |
| let epAddr = Int((cmd.data0 >> 8) & 0xFF) | |
| let key = (devAddr << 8) | epAddr | |
| if let esm = endpointSMs[key] { | |
| try esm.respond(toCommand: &cmd, status: IOUSBHostCIMessageStatusSuccess) | |
| if msgType == IOUSBHostCIMessageTypeEndpointDestroy { | |
| endpointSMs.removeValue(forKey: key) | |
| } | |
| } | |
| default: | |
| print("[\(typeName)] Unhandled 0x\(String(format: "%02X", rawType))") | |
| } | |
| } catch { | |
| print("[\(typeName)] handleCommand error: \(error)") | |
| } | |
| } | |
| // MARK: Doorbell handler | |
| private func handleDoorbells( | |
| _ ci: IOUSBHostControllerInterface, | |
| _ doorbells: UnsafePointer<IOUSBHostCIDoorbell>, | |
| _ count: UInt32 | |
| ) { | |
| for i in 0..<Int(count) { | |
| let db = doorbells[i] | |
| let devAddr = Int(db & 0xFF) | |
| let epAddr = Int((db >> 8) & 0xFF) | |
| let key = (devAddr << 8) | epAddr | |
| guard let esm = endpointSMs[key] else { continue } | |
| do { | |
| try esm.processDoorbell(db) | |
| try processTransfers(for: esm) | |
| } catch { | |
| print( | |
| "[\(typeName)] Doorbell ep=0x\(String(format: "%02X", epAddr)) error: \(error)") | |
| } | |
| } | |
| } | |
| // MARK: Transfer processing | |
| private func processTransfers(for esm: IOUSBHostCIEndpointStateMachine) throws { | |
| while esm.endpointState == IOUSBHostCIEndpointStateActive { | |
| let xfer = esm.currentTransferMessage | |
| guard (xfer.pointee.control & kMsgValid) != 0 else { break } | |
| switch IOUSBHostCIMessageType(rawValue: xfer.pointee.control & kMsgTypeMask) { | |
| case IOUSBHostCIMessageTypeSetupTransfer: | |
| handleSetupTransfer(esm: esm, xfer: xfer) | |
| case IOUSBHostCIMessageTypeNormalTransfer: | |
| try handleNormalTransfer(esm: esm, xfer: xfer) | |
| case IOUSBHostCIMessageTypeStatusTransfer: | |
| try esm.enqueueTransferCompletion( | |
| for: xfer, | |
| status: IOUSBHostCIMessageStatusSuccess, | |
| transferLength: 0) | |
| default: | |
| return // non-data message — wait for next doorbell | |
| } | |
| } | |
| } | |
| private func handleSetupTransfer( | |
| esm: IOUSBHostCIEndpointStateMachine, | |
| xfer: UnsafePointer<IOUSBHostCIMessage> | |
| ) { | |
| let d1 = xfer.pointee.data1 | |
| let bmRequestType = UInt8((d1 >> 0) & 0xFF) | |
| let bRequest = UInt8((d1 >> 8) & 0xFF) | |
| let wValue = UInt16((d1 >> 16) & 0xFFFF) | |
| let wLength = UInt16((d1 >> 48) & 0xFFFF) | |
| let descType = UInt8((wValue >> 8) & 0xFF) | |
| let descIndex = UInt8(wValue & 0xFF) | |
| print( | |
| "[\(typeName)] SETUP bmRT=0x\(String(format: "%02X", bmRequestType)) bReq=0x\(String(format: "%02X", bRequest)) wVal=0x\(String(format: "%04X", wValue)) wLen=\(wLength)" | |
| ) | |
| pendingResponse = resolveControlRequest( | |
| bmRequestType: bmRequestType, bRequest: bRequest, | |
| descType: descType, descIndex: descIndex, wLength: wLength) | |
| do { | |
| try esm.enqueueTransferCompletion( | |
| for: xfer, | |
| status: IOUSBHostCIMessageStatusSuccess, | |
| transferLength: 0) | |
| } catch { | |
| print("[\(typeName)] Setup ACK error: \(error)") | |
| } | |
| } | |
| private func handleNormalTransfer( | |
| esm: IOUSBHostCIEndpointStateMachine, | |
| xfer: UnsafePointer<IOUSBHostCIMessage> | |
| ) throws { | |
| if esm.endpointAddress == 0x81 { | |
| // Interrupt IN — deliver subclass data | |
| let maxLen = Int(xfer.pointee.data0 & 0x0FFF_FFFF) | |
| let bytes = interruptINData(maxLength: maxLen) | |
| let n = min(bytes.count, maxLen) | |
| if let buf = UnsafeMutableRawPointer(bitPattern: UInt(xfer.pointee.data1)) { | |
| buf.copyMemory(from: bytes, byteCount: n) | |
| } | |
| try esm.enqueueTransferCompletion( | |
| for: xfer, | |
| status: IOUSBHostCIMessageStatusSuccess, | |
| transferLength: n) | |
| } else { | |
| // EP0 data IN phase — fill from pending control response | |
| let maxLen = Int(xfer.pointee.data0 & 0x0FFF_FFFF) | |
| var written = 0 | |
| if let resp = pendingResponse, !resp.isEmpty, | |
| let buf = UnsafeMutableRawPointer(bitPattern: UInt(xfer.pointee.data1)) | |
| { | |
| let n = min(resp.count, maxLen) | |
| resp.withUnsafeBytes { buf.copyMemory(from: $0.baseAddress!, byteCount: n) } | |
| written = n | |
| pendingResponse = nil | |
| } | |
| try esm.enqueueTransferCompletion( | |
| for: xfer, | |
| status: IOUSBHostCIMessageStatusSuccess, | |
| transferLength: written) | |
| } | |
| } | |
| // MARK: Control request dispatch | |
| private func resolveControlRequest( | |
| bmRequestType: UInt8, bRequest: UInt8, | |
| descType: UInt8, descIndex: UInt8, | |
| wLength: UInt16 | |
| ) -> Data? { | |
| switch bmRequestType { | |
| case 0x80: // Standard Device → Host | |
| switch bRequest { | |
| case kUSBReqGetDescriptor: | |
| switch descType { | |
| case kUSBDescDevice: return prefix(descriptors.device, wLength) | |
| case kUSBDescConfig: return prefix(descriptors.configuration, wLength) | |
| case kUSBDescString: | |
| switch descIndex { | |
| case 0: return prefix(string0, wLength) | |
| case 1: return prefix(stringManuf, wLength) | |
| case 2: return prefix(stringProd, wLength) | |
| default: return nil | |
| } | |
| default: return nil | |
| } | |
| case kUSBReqGetConfig: return Data([1]) | |
| default: return Data() | |
| } | |
| case 0x81: // Standard Interface → Host | |
| switch bRequest { | |
| case kUSBReqGetInterface: return Data([0]) | |
| case kHIDReqGetDescriptor: | |
| switch descType { | |
| case kHIDDescReport: return prefix(descriptors.hidReport, wLength) | |
| case kHIDDescHID: | |
| // HID descriptor sits at bytes [18 ... 26] of the config blob | |
| // (after 9-byte config + 9-byte interface descriptors) | |
| let start = 18 | |
| let end = 27 | |
| guard descriptors.configuration.count >= end else { return nil } | |
| return prefix(Array(descriptors.configuration[start..<end]), wLength) | |
| default: return nil | |
| } | |
| default: return nil | |
| } | |
| case 0x21: // Class Host → Device (SET_PROTOCOL, SET_IDLE, SET_REPORT) | |
| return Data() | |
| case 0xA1: // Class Device → Host (GET_REPORT) | |
| return Data(getReport(maxLength: Int(wLength))) | |
| default: | |
| return Data() | |
| } | |
| } | |
| // MARK: Helpers | |
| private func prefix(_ bytes: [UInt8], _ max: UInt16) -> Data { | |
| Data(bytes.prefix(Int(max))) | |
| } | |
| private static func makeStringDescriptor(_ text: String) -> [UInt8] { | |
| let utf16 = Array(text.utf16) | |
| var result: [UInt8] = [UInt8(2 + utf16.count * 2), 0x03] | |
| for cp in utf16 { | |
| result.append(UInt8(cp & 0xFF)) | |
| result.append(UInt8(cp >> 8)) | |
| } | |
| return result | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment