diff --git a/README.md b/README.md index c730339..9e0dfa2 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,14 @@ To reproduce the validation: 6. **Verify rumble per slot**: Send rumble to interface 0 and confirm only the slot 0 controller vibrates. Send rumble to interface 1 and confirm only the slot 1 controller vibrates. 7. **Verify motion**: Enable gyro/accel on both controllers. Rotate each controller independently and confirm that motion is per-slot (rotating controller 0 does not affect controller 1's IMU output). +On macOS, inspect the firmware's raw Game Pad values before GameController or browser remapping with: + +```sh +swift tools/Test-SwitchPicoMac.swift +``` + +The diagnostic matches only `CAFE:4021`, identifies each of the four interfaces by interface and location, and prints changed axes, hats, and buttons with their HID usage and logical range. Move each analog trigger slowly and confirm that `Ry`/`Rz` report intermediate values across `0...65535`, rather than only the endpoints. It continues through hot-plug events until Ctrl-C. If opening a device fails, allow the terminal (or the app launching Swift) under **System Settings → Privacy & Security → Input Monitoring**, then rerun it. + On the tested Linux host, all four HID interfaces enumerated, but `hid-nintendo` timed out (`-110`) while requesting controller information from the composite device and removed the transient hidraw nodes. This is an observed, undiagnosed composite interoperability limitation; its root cause has not been established. The timeout was not observed on the Switch, so successful `hid-nintendo` binding is not the release criterion for the four-interface AIO firmware. The pairing CLI uses vendor control transfers on endpoint 0 and does not depend on those hidraw nodes. Bluepad32 is Apache-2.0. BTstack use on Pico W/Pico 2 W is covered by Raspberry Pi's BTstack license. diff --git a/generic_hid_descriptors.h b/generic_hid_descriptors.h index b742a5c..7293735 100644 --- a/generic_hid_descriptors.h +++ b/generic_hid_descriptors.h @@ -157,7 +157,7 @@ inline constexpr uint8_t kMacDeviceDescriptor[] = { }; // One report per interface, so no Report ID item is needed. -inline constexpr uint8_t kReportDescriptor[] = { +inline constexpr uint8_t kDInputReportDescriptor[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x05, // Usage (Game Pad) 0xa1, 0x01, // Collection (Application) @@ -207,11 +207,64 @@ inline constexpr uint8_t kReportDescriptor[] = { 0xc0, // End Collection }; +inline constexpr uint8_t kMacReportDescriptor[] = { + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x05, // Usage (Game Pad) + 0xa1, 0x01, // Collection (Application) + + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x16, 0x00, 0x80, // Logical Minimum (-32768) + 0x26, 0xff, 0x7f, // Logical Maximum (32767) + 0x75, 0x10, // Report Size (16) + 0x95, 0x04, // Report Count (4) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x09, 0x32, // Usage (Z) + 0x09, 0x33, // Usage (Rx) + 0x81, 0x02, // Input (Data, Variable, Absolute) + + 0x15, 0x00, // Logical Minimum (0) + 0x27, 0xff, 0xff, 0x00, 0x00, // Logical Maximum (65535) + 0x95, 0x02, // Report Count (2) + 0x09, 0x34, // Usage (Ry) + 0x09, 0x35, // Usage (Rz) + 0x81, 0x02, // Input (Data, Variable, Absolute) + + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x07, // Logical Maximum (7) + 0x35, 0x00, // Physical Minimum (0) + 0x46, 0x3b, 0x01, // Physical Maximum (315) + 0x65, 0x14, // Unit (English Rotation, Degrees) + 0x75, 0x04, // Report Size (4) + 0x95, 0x01, // Report Count (1) + 0x09, 0x39, // Usage (Hat Switch) + 0x81, 0x42, // Input (Data, Variable, Absolute, Null State) + 0x75, 0x04, // Report Size (4) + 0x95, 0x01, // Report Count (1) + 0x81, 0x03, // Input (Constant, Variable, Absolute) + + 0x05, 0x09, // Usage Page (Button) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x35, 0x00, // Physical Minimum (0) + 0x45, 0x00, // Physical Maximum (0) + 0x65, 0x00, // Unit (None) + 0x19, 0x01, // Usage Minimum (Button 1) + 0x29, 0x10, // Usage Maximum (Button 16) + 0x75, 0x01, // Report Size (1) + 0x95, 0x10, // Report Count (16) + 0x81, 0x02, // Input (Data, Variable, Absolute) + 0xc0, // End Collection +}; + +static_assert(sizeof(kDInputReportDescriptor) == + sizeof(kMacReportDescriptor)); + #define GENERIC_HID_INTERFACE(number, endpoint) \ 0x09, 0x04, number, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, \ 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, \ - static_cast(sizeof(kReportDescriptor)), \ - static_cast(sizeof(kReportDescriptor) >> 8u), \ + static_cast(sizeof(kDInputReportDescriptor)), \ + static_cast(sizeof(kDInputReportDescriptor) >> 8u), \ 0x07, 0x05, static_cast(0x80u | endpoint), 0x03, \ kEndpointSize, 0x00, kEndpointIntervalMs diff --git a/generic_hid_driver.cpp b/generic_hid_driver.cpp index 530b7e6..6b733a6 100644 --- a/generic_hid_driver.cpp +++ b/generic_hid_driver.cpp @@ -74,8 +74,12 @@ uint16_t generic_hid_get_report(uint8_t instance, uint8_t report_id, return report_size; } -const uint8_t* generic_hid_report_descriptor(uint8_t instance) { - return context_for(instance) == nullptr - ? nullptr - : GenericHid::kReportDescriptor; +const uint8_t* generic_hid_report_descriptor( + uint8_t instance, GenericHid::ReportDescriptorVariant variant) { + if (context_for(instance) == nullptr) { + return nullptr; + } + return variant == GenericHid::ReportDescriptorVariant::kMac + ? GenericHid::kMacReportDescriptor + : GenericHid::kDInputReportDescriptor; } diff --git a/generic_hid_driver.h b/generic_hid_driver.h index d68274d..1c9d6e5 100644 --- a/generic_hid_driver.h +++ b/generic_hid_driver.h @@ -4,6 +4,15 @@ #include "controller_state.h" #include "tusb.h" +namespace GenericHid { + +enum class ReportDescriptorVariant : uint8_t { + kDInput, + kMac, +}; + +} // namespace GenericHid + void generic_hid_init(uint8_t instance); void generic_hid_set_input(uint8_t instance, const ControllerState& state); @@ -13,4 +22,5 @@ bool generic_hid_is_ready(uint8_t instance); uint16_t generic_hid_get_report(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t requested_length); -const uint8_t* generic_hid_report_descriptor(uint8_t instance); +const uint8_t* generic_hid_report_descriptor( + uint8_t instance, GenericHid::ReportDescriptorVariant variant); diff --git a/tests/generic_hid_descriptors_test.cpp b/tests/generic_hid_descriptors_test.cpp index f46d722..7d82554 100644 --- a/tests/generic_hid_descriptors_test.cpp +++ b/tests/generic_hid_descriptors_test.cpp @@ -15,6 +15,9 @@ static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == EXPECTED_HID_INSTANCE_COUNT); static_assert(sizeof(GenericHid::kConfigurationDescriptor) == 9u + 25u * EXPECTED_HID_INSTANCE_COUNT); static_assert(sizeof(GenericHid::InputReport) == 15); +static_assert(sizeof(GenericHid::kDInputReportDescriptor) == + sizeof(GenericHid::kMacReportDescriptor)); + namespace { @@ -43,7 +46,7 @@ struct ItemGolden { uint32_t value; }; -constexpr std::array kReportItemGolden{{ +constexpr std::array kDInputReportItemGolden{{ {1, 0, 1, 0x01}, {2, 0, 1, 0x05}, {0, 10, 1, 0x01}, {1, 0, 1, 0x01}, {1, 1, 2, 0x8000}, {1, 2, 2, 0x7fff}, {1, 7, 1, 0x10}, {1, 9, 1, 0x04}, {2, 0, 1, 0x30}, @@ -102,9 +105,14 @@ struct InputField { LocalState locals; }; -void inspect_report_descriptor() { - const uint8_t* descriptor = GenericHid::kReportDescriptor; - const size_t descriptor_size = sizeof(GenericHid::kReportDescriptor); +void inspect_report_descriptor(const uint8_t* descriptor, + size_t descriptor_size, bool mac_variant) { + const std::array expected_stick_usages = + mac_variant ? std::array{{0x30, 0x31, 0x32, 0x33}} + : std::array{{0x30, 0x31, 0x33, 0x34}}; + const std::array expected_trigger_usages = + mac_variant ? std::array{{0x34, 0x35}} + : std::array{{0x32, 0x35}}; size_t offset = 0; size_t decoded = 0; uint16_t report_bits = 0; @@ -141,13 +149,25 @@ void inspect_report_descriptor() { const uint8_t type = static_cast((prefix >> 2u) & 0x03u); const uint8_t tag = static_cast(prefix >> 4u); - expect(decoded < kReportItemGolden.size(), + expect(decoded < kDInputReportItemGolden.size(), "report descriptor contains an extra HID item"); - if (decoded < kReportItemGolden.size()) { - const ItemGolden& golden = kReportItemGolden[decoded]; + if (decoded < kDInputReportItemGolden.size()) { + const ItemGolden& golden = kDInputReportItemGolden[decoded]; + uint32_t expected_value = golden.value; + if (mac_variant) { + if (decoded == 10) { + expected_value = 0x32; + } else if (decoded == 11) { + expected_value = 0x33; + } else if (decoded == 16) { + expected_value = 0x34; + } else if (decoded == 17) { + expected_value = 0x35; + } + } expect(type == golden.type && tag == golden.tag && - size == golden.size && value == golden.value, - "decoded HID item differs from the golden contract"); + size == golden.size && value == expected_value, + "decoded HID item differs from its mode golden"); } ++decoded; @@ -246,7 +266,7 @@ void inspect_report_descriptor() { } expect(offset == descriptor_size, "report descriptor was not fully decoded"); - expect(decoded == kReportItemGolden.size(), + expect(decoded == kDInputReportItemGolden.size(), "report descriptor is missing a golden HID item"); expect(gamepad_application && collection_depth == 0, "report is not one balanced Game Pad application collection"); @@ -265,12 +285,13 @@ void inspect_report_descriptor() { sticks.flags == 0x02 && sticks.globals.usage_page == 1 && sticks.globals.logical_minimum == -32768 && sticks.globals.logical_maximum == 32767 && - sticks.locals.usage_count == 4 && - sticks.locals.usages[0] == 0x30 && - sticks.locals.usages[1] == 0x31 && - sticks.locals.usages[2] == 0x33 && - sticks.locals.usages[3] == 0x34, - "signed X/Y/Rx/Ry field layout is wrong"); + sticks.locals.usage_count == expected_stick_usages.size() && + sticks.locals.usages[0] == expected_stick_usages[0] && + sticks.locals.usages[1] == expected_stick_usages[1] && + sticks.locals.usages[2] == expected_stick_usages[2] && + sticks.locals.usages[3] == expected_stick_usages[3], + mac_variant ? "signed Mac X/Y/Z/Rx stick field layout is wrong" + : "signed DInput X/Y/Rx/Ry stick field layout is wrong"); const InputField& triggers = fields[1]; expect(triggers.bit_offset == 64 && triggers.size == 16 && @@ -278,10 +299,33 @@ void inspect_report_descriptor() { triggers.globals.usage_page == 1 && triggers.globals.logical_minimum == 0 && triggers.globals.logical_maximum == 65535 && - triggers.locals.usage_count == 2 && - triggers.locals.usages[0] == 0x32 && - triggers.locals.usages[1] == 0x35, - "unsigned Z/Rz trigger field layout is wrong"); + triggers.locals.usage_count == + expected_trigger_usages.size() && + triggers.locals.usages[0] == expected_trigger_usages[0] && + triggers.locals.usages[1] == expected_trigger_usages[1], + mac_variant ? "unsigned Mac Ry/Rz trigger field layout is wrong" + : "unsigned DInput Z/Rz trigger field layout is wrong"); + if (mac_variant) { + const std::array usage_indices{{ + sticks.locals.usages[0] - 0x30u, + sticks.locals.usages[1] - 0x30u, + sticks.locals.usages[2] - 0x30u, + sticks.locals.usages[3] - 0x30u, + triggers.locals.usages[0] - 0x30u, + triggers.locals.usages[1] - 0x30u, + }}; + constexpr std::array kSemanticAxisGolden{{ + 0, 1, 2, 3, 4, 5, + }}; + expect(usage_indices == kSemanticAxisGolden, + "Mac physical fields do not map to Chromium axes 0..5"); + expect((sticks.bit_offset + 3u * sticks.size) / 8u == 6u && + usage_indices[3] == 3u && + triggers.bit_offset / 8u == 8u && + usage_indices[4] == 4u, + "Mac field offset 6 must be Rx/axis3 and offset 8 must be " + "Ry/axis4; reversing them reproduces the hardware failure"); + } const InputField& hat = fields[2]; expect(hat.bit_offset == 96 && hat.size == 4 && hat.count == 1 && @@ -312,6 +356,35 @@ void inspect_report_descriptor() { buttons.locals.usage_maximum == 16, "sequential Button 1..16 field layout is wrong"); } +void inspect_report_descriptor_parity() { + constexpr std::array kDInputDifferentBytes{{ + 0x33, 0x34, 0x32, + }}; + constexpr std::array kMacDifferentBytes{{ + 0x32, 0x33, 0x34, + }}; + size_t difference_count = 0; + for (size_t offset = 0; + offset < sizeof(GenericHid::kDInputReportDescriptor); ++offset) { + const uint8_t dinput = + GenericHid::kDInputReportDescriptor[offset]; + const uint8_t mac = GenericHid::kMacReportDescriptor[offset]; + if (dinput == mac) { + continue; + } + expect(difference_count < kDInputDifferentBytes.size(), + "report descriptors differ outside the three Mac axis usages"); + if (difference_count < kDInputDifferentBytes.size()) { + expect(dinput == kDInputDifferentBytes[difference_count] && + mac == kMacDifferentBytes[difference_count], + "report descriptor axis usage difference is wrong"); + } + ++difference_count; + } + expect(difference_count == kDInputDifferentBytes.size(), + "report descriptors do not differ at exactly three axis usages"); +} + void inspect_device_descriptors_and_strings() { constexpr std::array dinput_golden{{ @@ -398,8 +471,12 @@ void inspect_configuration_descriptor() { } else if (type == kHid) { expect(current_interface >= 0 && length == 9, "HID descriptor is not attached to an interface"); - expect(read_u16(descriptor + offset + 7) == - sizeof(GenericHid::kReportDescriptor), + const uint16_t report_descriptor_length = + read_u16(descriptor + offset + 7); + expect(report_descriptor_length == + sizeof(GenericHid::kDInputReportDescriptor) && + report_descriptor_length == + sizeof(GenericHid::kMacReportDescriptor), "HID descriptor advertises the wrong report length"); if (current_interface >= 0) { ++hid_counts[static_cast(current_interface)]; @@ -540,7 +617,12 @@ void inspect_report_encoding() { } // namespace int main() { - inspect_report_descriptor(); + inspect_report_descriptor(GenericHid::kDInputReportDescriptor, + sizeof(GenericHid::kDInputReportDescriptor), + false); + inspect_report_descriptor(GenericHid::kMacReportDescriptor, + sizeof(GenericHid::kMacReportDescriptor), true); + inspect_report_descriptor_parity(); inspect_device_descriptors_and_strings(); inspect_configuration_descriptor(); inspect_report_encoding(); diff --git a/tests/generic_hid_driver_test.cpp b/tests/generic_hid_driver_test.cpp index ffe9130..f84d191 100644 --- a/tests/generic_hid_driver_test.cpp +++ b/tests/generic_hid_driver_test.cpp @@ -73,14 +73,28 @@ void test_initial_report_and_descriptor_routing() { "initialized generic HID report is not neutral"); } } - const uint8_t* descriptor = - generic_hid_report_descriptor(instance); - expect(descriptor != nullptr && - std::memcmp(descriptor, GenericHid::kReportDescriptor, - sizeof(GenericHid::kReportDescriptor)) == 0, - "valid HID instance did not receive the shared descriptor"); + const uint8_t* dinput_descriptor = generic_hid_report_descriptor( + instance, GenericHid::ReportDescriptorVariant::kDInput); + const uint8_t* mac_descriptor = generic_hid_report_descriptor( + instance, GenericHid::ReportDescriptorVariant::kMac); + expect(dinput_descriptor != nullptr && + std::memcmp(dinput_descriptor, + GenericHid::kDInputReportDescriptor, + sizeof(GenericHid::kDInputReportDescriptor)) == + 0, + "valid HID instance did not receive the DInput descriptor"); + expect(mac_descriptor != nullptr && + std::memcmp(mac_descriptor, + GenericHid::kMacReportDescriptor, + sizeof(GenericHid::kMacReportDescriptor)) == 0, + "valid HID instance did not receive the Mac descriptor"); } - expect(generic_hid_report_descriptor(kInvalidInstance) == nullptr, + expect(generic_hid_report_descriptor( + kInvalidInstance, + GenericHid::ReportDescriptorVariant::kDInput) == nullptr && + generic_hid_report_descriptor( + kInvalidInstance, + GenericHid::ReportDescriptorVariant::kMac) == nullptr, "invalid HID instance received a report descriptor"); } diff --git a/tests/usb_output_driver_test.cpp b/tests/usb_output_driver_test.cpp index 50e291c..b2f61c5 100644 --- a/tests/usb_output_driver_test.cpp +++ b/tests/usb_output_driver_test.cpp @@ -560,6 +560,7 @@ void test_xinput_boundary_dispatch() { void test_generic_boundary_dispatch( AdapterUsbMode mode, const uint8_t* expected_device_descriptor, + const uint8_t* expected_report_descriptor, const char* expected_driver_name, const char* expected_mode_name, const char* expected_product, const char* expected_serial) { reset_usb_harness(); @@ -593,9 +594,8 @@ void test_generic_boundary_dispatch( const uint8_t* report_descriptor = tud_hid_descriptor_report_cb(0); expect(report_descriptor != nullptr && - std::memcmp(report_descriptor, - GenericHid::kReportDescriptor, - sizeof(GenericHid::kReportDescriptor)) == 0 && + std::memcmp(report_descriptor, expected_report_descriptor, + sizeof(GenericHid::kDInputReportDescriptor)) == 0 && tud_hid_descriptor_report_cb(kInvalidInstance) == nullptr, "generic report descriptor routing was incorrect"); @@ -679,12 +679,12 @@ void test_generic_boundary_dispatch( void test_generic_modes_boundary_dispatch() { test_generic_boundary_dispatch( AdapterUsbMode::kDInput, GenericHid::kDInputDeviceDescriptor, - "DINPUT", "DInput", GenericHid::kDInputProductString, - GenericHid::kDInputSerialString); + GenericHid::kDInputReportDescriptor, "DINPUT", "DInput", + GenericHid::kDInputProductString, GenericHid::kDInputSerialString); test_generic_boundary_dispatch( AdapterUsbMode::kMac, GenericHid::kMacDeviceDescriptor, - "MAC", "Mac", GenericHid::kMacProductString, - GenericHid::kMacSerialString); + GenericHid::kMacReportDescriptor, "MAC", "Mac", + GenericHid::kMacProductString, GenericHid::kMacSerialString); } void test_vendor_control_boundary() { diff --git a/tools/Test-SwitchPicoMac.swift b/tools/Test-SwitchPicoMac.swift new file mode 100755 index 0000000..b688368 --- /dev/null +++ b/tools/Test-SwitchPicoMac.swift @@ -0,0 +1,293 @@ +#!/usr/bin/env swift + +import Darwin +import Dispatch +import Foundation +import IOKit.hid + +private let vendorID = 0xCAFE +private let productID = 0x4021 +private let genericDesktopPage = 0x01 +private let gamePadUsage = 0x05 +private let buttonPage = 0x09 +private let expectedInterfaceCount = 4 + +private func writeError(_ message: String) { + FileHandle.standardError.write(Data((message + "\n").utf8)) +} + +private func ioReturnDescription(_ result: IOReturn) -> String { + String(format: "0x%08X", UInt32(bitPattern: Int32(result))) +} + +private func printInputMonitoringHint() { + writeError( + "If macOS denied HID access, enable your terminal (or the app launching Swift) in " + + "System Settings > Privacy & Security > Input Monitoring, then rerun the command." + ) +} + +private func numberProperty(_ device: IOHIDDevice, keys: [String]) -> Int? { + for key in keys { + if let number = IOHIDDeviceGetProperty(device, key as CFString) as? NSNumber { + return number.intValue + } + } + return nil +} + +private func deviceKey(_ device: IOHIDDevice) -> ObjectIdentifier { + ObjectIdentifier(device) +} + +private struct DeviceIdentity { + let interface: String + let location: String + + init(_ device: IOHIDDevice) { + if let value = numberProperty(device, keys: ["InterfaceID", "InterfaceNumber", "bInterfaceNumber"]) { + interface = String(value) + } else { + interface = "unknown" + } + + if let value = numberProperty(device, keys: ["LocationID", "locationID"]) { + location = String(format: "0x%08X", UInt32(truncatingIfNeeded: value)) + } else { + location = "unknown" + } + } + + var prefix: String { + "[interface=\(interface) location=\(location)]" + } +} + +private final class DeviceState { + let device: IOHIDDevice + let identity: DeviceIdentity + var lastValues: [Int: CFIndex] = [:] + + init(device: IOHIDDevice) { + self.device = device + identity = DeviceIdentity(device) + } +} + +private final class HIDMonitor { + private let manager: IOHIDManager + private let runLoop: CFRunLoop + private var devices: [ObjectIdentifier: DeviceState] = [:] + private var interruptSource: DispatchSourceSignal? + private var didPrintPermissionHint = false + private var isCleanedUp = false + + init(manager: IOHIDManager, runLoop: CFRunLoop) { + self.manager = manager + self.runLoop = runLoop + } + + func start() -> IOReturn { + let context = Unmanaged.passUnretained(self).toOpaque() + let matching: [String: Any] = [ + kIOHIDVendorIDKey as String: vendorID, + kIOHIDProductIDKey as String: productID, + kIOHIDDeviceUsagePageKey as String: genericDesktopPage, + kIOHIDDeviceUsageKey as String: gamePadUsage, + ] + + IOHIDManagerSetDeviceMatching(manager, matching as CFDictionary) + IOHIDManagerRegisterDeviceMatchingCallback(manager, deviceMatchedCallback, context) + IOHIDManagerRegisterDeviceRemovalCallback(manager, deviceRemovedCallback, context) + IOHIDManagerRegisterInputValueCallback(manager, inputValueCallback, context) + IOHIDManagerScheduleWithRunLoop(manager, runLoop, CFRunLoopMode.defaultMode.rawValue) + + let result = IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone)) + guard result == kIOReturnSuccess else { + writeError("Could not open the IOHID manager (\(ioReturnDescription(result))).") + printPermissionHintOnce() + return result + } + + let matchedDeviceCount: Int + if let matchedDevices = IOHIDManagerCopyDevices(manager) { + matchedDeviceCount = CFSetGetCount(matchedDevices) + } else { + matchedDeviceCount = 0 + } + if matchedDeviceCount == 0 { + print( + "No CAFE:4021 Generic Desktop Game Pad interfaces are connected; " + + "waiting for hot-plug. Press Ctrl-C to stop." + ) + } else { + print( + "Monitoring \(matchedDeviceCount)/\(expectedInterfaceCount) CAFE:4021 " + + "raw Game Pad interfaces. Press Ctrl-C to stop." + ) + } + + signal(SIGINT, SIG_IGN) + let source = DispatchSource.makeSignalSource(signal: SIGINT, queue: .global(qos: .userInitiated)) + source.setEventHandler { [weak self] in + guard let self else { return } + CFRunLoopStop(self.runLoop) + } + source.resume() + interruptSource = source + return result + } + + func deviceMatched(_ device: IOHIDDevice, result: IOReturn) { + guard result == kIOReturnSuccess else { + writeError("Device matching callback failed (\(ioReturnDescription(result))).") + return + } + + let key = deviceKey(device) + guard devices[key] == nil else { return } + + let identity = DeviceIdentity(device) + let openResult = IOHIDDeviceOpen(device, IOOptionBits(kIOHIDOptionsTypeNone)) + guard openResult == kIOReturnSuccess else { + writeError("\(identity.prefix) Could not open HID interface (\(ioReturnDescription(openResult))).") + printPermissionHintOnce() + return + } + + devices[key] = DeviceState(device: device) + print("\(identity.prefix) attached (\(devices.count)/\(expectedInterfaceCount) Game Pad interfaces open)") + } + + func deviceRemoved(_ device: IOHIDDevice, result: IOReturn) { + let key = deviceKey(device) + let identity = devices.removeValue(forKey: key)?.identity ?? DeviceIdentity(device) + IOHIDDeviceClose(device, IOOptionBits(kIOHIDOptionsTypeNone)) + + if result == kIOReturnSuccess { + print("\(identity.prefix) removed (\(devices.count)/\(expectedInterfaceCount) Game Pad interfaces open)") + } else { + writeError("\(identity.prefix) removal callback failed (\(ioReturnDescription(result))).") + } + } + + func inputValue(_ value: IOHIDValue, result: IOReturn) { + guard result == kIOReturnSuccess else { + writeError("Input callback failed (\(ioReturnDescription(result))).") + return + } + + let element = IOHIDValueGetElement(value) + let usagePage = IOHIDElementGetUsagePage(element) + let usage = IOHIDElementGetUsage(element) + guard let name = usageName(page: usagePage, usage: usage) else { return } + + let device = IOHIDElementGetDevice(element) + let key = deviceKey(device) + guard let state = devices[key] else { return } + + let integerValue = IOHIDValueGetIntegerValue(value) + let elementCookie = Int(IOHIDElementGetCookie(element)) + guard state.lastValues[elementCookie] != integerValue else { return } + state.lastValues[elementCookie] = integerValue + + let logicalMin = IOHIDElementGetLogicalMin(element) + let logicalMax = IOHIDElementGetLogicalMax(element) + print( + "\(state.identity.prefix) \(name) " + + "page=\(hexUsage(usagePage)) usage=\(hexUsage(usage)) " + + "logical=\(logicalMin)...\(logicalMax) value=\(integerValue)" + ) + } + + func cleanup() { + guard !isCleanedUp else { return } + isCleanedUp = true + + interruptSource?.cancel() + interruptSource = nil + for state in devices.values { + print("\(state.identity.prefix) closing") + IOHIDDeviceClose(state.device, IOOptionBits(kIOHIDOptionsTypeNone)) + } + devices.removeAll() + IOHIDManagerUnscheduleFromRunLoop(manager, runLoop, CFRunLoopMode.defaultMode.rawValue) + IOHIDManagerClose(manager, IOOptionBits(kIOHIDOptionsTypeNone)) + } + + private func printPermissionHintOnce() { + guard !didPrintPermissionHint else { return } + didPrintPermissionHint = true + printInputMonitoringHint() + } +} + +private func usageName(page: UInt32, usage: UInt32) -> String? { + if page == UInt32(genericDesktopPage) { + switch usage { + case 0x30: return "X" + case 0x31: return "Y" + case 0x32: return "Z" + case 0x33: return "Rx" + case 0x34: return "Ry" + case 0x35: return "Rz" + case 0x39: return "Hat" + default: return nil + } + } + if page == UInt32(buttonPage) { + return "Button \(usage)" + } + return nil +} + +private func hexUsage(_ value: UInt32) -> String { + String(format: "0x%02X", value) +} + +private func monitor(from context: UnsafeMutableRawPointer?) -> HIDMonitor? { + guard let context else { return nil } + return Unmanaged.fromOpaque(context).takeUnretainedValue() +} + +private func deviceMatchedCallback( + context: UnsafeMutableRawPointer?, + result: IOReturn, + sender: UnsafeMutableRawPointer?, + device: IOHIDDevice +) { + _ = sender + monitor(from: context)?.deviceMatched(device, result: result) +} + +private func deviceRemovedCallback( + context: UnsafeMutableRawPointer?, + result: IOReturn, + sender: UnsafeMutableRawPointer?, + device: IOHIDDevice +) { + _ = sender + monitor(from: context)?.deviceRemoved(device, result: result) +} + +private func inputValueCallback( + context: UnsafeMutableRawPointer?, + result: IOReturn, + sender: UnsafeMutableRawPointer?, + value: IOHIDValue +) { + _ = sender + monitor(from: context)?.inputValue(value, result: result) +} + +let manager: IOHIDManager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone)) +let monitorInstance = HIDMonitor(manager: manager, runLoop: CFRunLoopGetCurrent()) +let openResult = monitorInstance.start() +if openResult != kIOReturnSuccess { + monitorInstance.cleanup() + exit(EXIT_FAILURE) +} + +CFRunLoopRun() +monitorInstance.cleanup() +print("Stopped.") diff --git a/udev/99-switch-pico.rules b/udev/99-switch-pico.rules new file mode 100644 index 0000000..9d3b925 --- /dev/null +++ b/udev/99-switch-pico.rules @@ -0,0 +1,5 @@ +# Development USB identities used by switch-pico XInput and generic HID modes. +# Restrict configuration access to the active local seat and plugdev members. +SUBSYSTEM=="usb", ATTR{idVendor}=="cafe", ATTR{idProduct}=="4010", MODE="0660", GROUP="plugdev", TAG+="uaccess" +SUBSYSTEM=="usb", ATTR{idVendor}=="cafe", ATTR{idProduct}=="4020", MODE="0660", GROUP="plugdev", TAG+="uaccess" +SUBSYSTEM=="usb", ATTR{idVendor}=="cafe", ATTR{idProduct}=="4021", MODE="0660", GROUP="plugdev", TAG+="uaccess" diff --git a/usb_output_driver.cpp b/usb_output_driver.cpp index 3393fb6..6560a73 100644 --- a/usb_output_driver.cpp +++ b/usb_output_driver.cpp @@ -225,7 +225,11 @@ extern "C" uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) { return nullptr; } if (generic_selected()) { - return generic_hid_report_descriptor(instance); + const GenericHid::ReportDescriptorVariant variant = + g_mode == AdapterUsbMode::kMac + ? GenericHid::ReportDescriptorVariant::kMac + : GenericHid::ReportDescriptorVariant::kDInput; + return generic_hid_report_descriptor(instance, variant); } #endif return switch_pro_hid_report_descriptor(instance);