Xbox One 控制器转换为 macOS HID 设备的工作原理分析

发布于:2025-07-04 ⋅ 阅读:(23) ⋅ 点赞:(0)

Xbox One 控制器转换为 macOS HID 设备的工作原理分析

源代码在 https://github.com/guilhermearaujo/xboxonecontrollerenabler.git

这个工程的核心功能是将 Xbox One 控制器(macOS 原生不支持的设备)转换为 macOS 可识别的 HID 设备。这里通过分析代码,详细解释其工作原理、设备描述和报告描述符的实现。

整体架构

该项目由三个主要部分组成:

  1. Xbox 控制器通信层:通过 IOKit 框架与 Xbox One 控制器进行 USB 通信
  2. 虚拟 HID 设备层:使用 VHID 框架创建虚拟 HID 设备
  3. 系统集成层:使用 WirtualJoy 框架将虚拟设备注册到 macOS 系统

Xbox One 控制器设备描述

Xbox One 控制器的设备描述在代码中通过 XboxOneButtonMap 结构体定义:

typedef struct {
  bool sync;
  bool dummy; // Always 0.
  bool menu;  // Not entirely sure what these are
  bool view;  // called on the new controller
  
  bool a;
  bool b;
  bool x;
  bool y;
  
  bool dpad_up;
  bool dpad_down;
  bool dpad_left;
  bool dpad_right;
  
  bool bumper_left;
  bool bumper_right;
  bool stick_left_click;
  bool stick_right_click;
  
  unsigned short trigger_left;
  unsigned short trigger_right;
  
  short stick_left_x;
  short stick_left_y;
  short stick_right_x;
  short stick_right_y;
  
  bool home;
} XboxOneButtonMap;

这个结构体映射了 Xbox One 控制器的所有输入元素,包括:

  • 按钮(A、B、X、Y、方向键、肩键等)
  • 摇杆(左右摇杆的 X/Y 坐标)
  • 扳机键(左右扳机的模拟值)

控制器通信实现

GAXboxControllerCommunication 类负责与 Xbox One 控制器通信:

  1. 通过 USB 供应商 ID (0x045e) 和产品 ID (0x02d1) 识别 Xbox One 控制器
  2. 使用 IOKit 框架打开设备并配置接口
  3. 初始化控制器并开始轮询数据
  4. poll 方法中读取原始数据并解析为 XboxOneButtonMap 结构

关键代码片段:


- (void)poll {
  while (shouldPoll) {
    UInt32 numBytes = 20;
    char dataBuffer[32];
    returnCode = (*usbInterface)->ReadPipe(usbInterface, 2, dataBuffer, &numBytes);
    
    if (numBytes == 18) {
      Byte b = dataBuffer[4];
      buttonMap.sync  = (b & (1 << 0)) != 0;
      buttonMap.dummy = (b & (1 << 1)) != 0;
      buttonMap.menu  = (b & (1 << 2)) != 0;
      buttonMap.view  = (b & (1 << 3)) != 0;
      
      buttonMap.a = (b & (1 << 4)) != 0;
      buttonMap.b = (b & (1 << 5)) != 0;
      buttonMap.x = (b & (1 << 6)) != 0;
      buttonMap.y = (b & (1 << 7)) != 0;
      
      b = dataBuffer[5];
      buttonMap.dpad_up    = (b & (1 << 0)) != 0;
      buttonMap.dpad_down  = (b & (1 << 1)) != 0;
      buttonMap.dpad_left  = (b & (1 << 2)) != 0;
      buttonMap.dpad_right = (b & (1 << 3)) != 0;
      
      buttonMap.bumper_left       = (b & (1 << 4)) != 0;
      buttonMap.bumper_right      = (b & (1 << 5)) != 0;
      buttonMap.stick_left_click  = (b & (1 << 6)) != 0;
      buttonMap.stick_right_click = (b & (1 << 7)) != 0;
      
      buttonMap.trigger_left  = (dataBuffer[7] << 8) + (dataBuffer[6] & 0xff);
      buttonMap.trigger_right = (dataBuffer[9] << 8) + (dataBuffer[8] & 0xff);
      
      buttonMap.stick_left_x  = (dataBuffer[11] << 8) + dataBuffer[10];
      buttonMap.stick_left_y  = (dataBuffer[13] << 8) + dataBuffer[12];
      buttonMap.stick_right_x = (dataBuffer[15] << 8) + dataBuffer[14];
      buttonMap.stick_right_y = (dataBuffer[17] << 8) + dataBuffer[16];

      [delegate controllerDidUpdateData:buttonMap];
    }
    else if (numBytes == 6) {
      buttonMap.home = dataBuffer[4] & 1;
      [delegate controllerDidUpdateData:buttonMap];
    }
    
    [NSThread sleepForTimeInterval:0.005f];
  }
}

虚拟 HID 设备实现

VHIDDevice 类负责创建虚拟 HID 设备,它通过组合 VHIDButtonCollectionVHIDPointerCollection 来管理按钮和指针(摇杆)状态:

- (id)initWithType:(VHIDDeviceType)type
      pointerCount:(NSUInteger)pointerCount
       buttonCount:(NSUInteger)buttonCount
        isRelative:(BOOL)isRelative
{
    self = [super init];

    m_Type      = type;
    m_Buttons   = [[VHIDButtonCollection alloc] initWithButtonCount:buttonCount];
    m_Pointers  = [[VHIDPointerCollection alloc] initWithPointerCount:pointerCount
                                                           isRelative:isRelative];

    // ... 初始化状态数据
    m_Descriptor = [[self createDescriptor] retain];

    return self;
}

HID 报告描述符生成

VHIDDevice 类的 createDescriptor 方法负责生成 HID 报告描述符,这是关键部分:

- (NSData*)createDescriptor
{
    BOOL             isMouse        = (m_Type == VHIDDeviceTypeMouse);
    NSData          *buttonsHID     = [m_Buttons descriptor];
    NSData          *pointersHID    = [m_Pointers descriptor];
    NSMutableData   *result         = [NSMutableData dataWithLength:
                                                    [buttonsHID length] +
                                                    [pointersHID length] +
                                                    ((isMouse)?
                                                        (HIDDescriptorMouseAdditionalBytes):
                                                        (HIDDescriptorJoystickAdditionalBytes))];

    unsigned char   *data           = [result mutableBytes];
    unsigned char    usage          = ((isMouse)?(0x02):(0x05));

    *data = 0x05; data++; *data = 0x01; data++;      // USAGE_PAGE (Generic Desktop)
    *data = 0x09; data++; *data = usage; data++;     // USAGE (Mouse/Game Pad)
    *data = 0xA1; data++; *data = 0x01; data++;      // COLLECTION (Application)

    // ... 添加按钮和指针描述符

    *data = 0xC0; data++; // END_COLLECTION
    *data = 0xC0; data++; // END_COLLECTION

    return result;
}

这个方法创建了一个标准的 HID 报告描述符,定义了设备类型(游戏手柄)、按钮和指针(摇杆)的布局。

系统集成

WJoyDeviceWJoyDeviceImpl 类负责将虚拟 HID 设备注册到 macOS 系统:

  1. 加载内核驱动程序
  2. 创建与驱动程序的连接
  3. 设置设备属性(产品名称、供应商 ID、产品 ID 等)
  4. 启用设备并更新 HID 状态

关键代码:

- (id)initWithHIDDescriptor:(NSData*)HIDDescriptor properties:(NSDictionary*)properties
{
    // ... 初始化代码

    m_Impl = [[WJoyDeviceImpl alloc] init];
    
    // 设置设备属性
    if(productString != nil)
        [m_Impl setDeviceProductString:productString];

    // ... 设置其他属性

    // 启用设备
    if(![m_Impl enable:HIDDescriptor])
    {
        [self release];
        return nil;
    }

    return self;
}

数据流转换过程

整个数据流转换过程如下:

  1. GAXboxControllerCommunication 从 Xbox One 控制器读取原始 USB 数据
  2. 数据被解析为 XboxOneButtonMap 结构体
  3. GAXboxController 处理这些数据并提供高级访问方法
  4. GAMainViewController 将控制器数据映射到虚拟 HID 设备:
- (void)updateVHID:(GAXboxController *)controller {
  [_VHID setButton:0 pressed:[controller A]];
  [_VHID setButton:1 pressed:[controller B]];
  // ... 设置其他按钮
  
  NSPoint point = NSZeroPoint;
  point.x = [controller leftAnalogX];
  point.y = [controller leftAnalogY];
  [_VHID setPointer:0 position:point];
  // ... 设置其他指针
}
  1. VHIDDevice 更新其内部状态并通知代理
  2. WJoyDevice 将更新后的 HID 状态发送到系统

报告提交流程

  1. VHIDDevice.m 中的状态更新和报告生成
    当按钮或指针状态发生变化时,VHIDDevice会生成新的状态报告:

    - (void)setButton:(NSUInteger)buttonIndex pressed:(BOOL)pressed {
      // ... 检查按钮状态是否变化
      [m_Buttons setButton:buttonIndex pressed:pressed];
      
      if(m_Delegate != nil)
          [m_Delegate VHIDDevice:self stateChanged:[self state]];
    }
    
    - (NSData*)state {
      unsigned char *data = [m_State mutableBytes];
      NSData *buttonState = [m_Buttons state];
      NSData *pointerState = [m_Pointers state];
      
      // 合并按钮和指针状态到一个完整的HID报告
      if(buttonState != nil) {
          memcpy(data, [buttonState bytes], [buttonState length]);
      }
      
      if(pointerState != nil) {
          memcpy(data + [buttonState length], [pointerState bytes], [pointerState length]);
      }
      
      return [[m_State retain] autorelease];
    }
    
  2. VHIDButtonCollection.m 和 VHIDPointerCollection.m
    这两个类负责维护按钮和指针的状态,并生成对应的HID报告部分:

    在VHIDButtonCollection中:

    - (void)setButton:(NSUInteger)buttonIndex pressed:(BOOL)pressed {
      // ... 检查按钮索引
      NSUInteger buttonByte = buttonIndex / 8;
      NSUInteger buttonBit = buttonIndex % 8;
      unsigned char *data = (unsigned char*)[m_State mutableBytes];
      
      // 设置对应位的按钮状态
      if(pressed)
          data[buttonByte] |= buttonMasks[buttonBit];
      else
          data[buttonByte] &= ~(buttonMasks[buttonBit]);
    }
    

    在VHIDPointerCollection中:

    - (void)setPointer:(NSUInteger)pointerIndex position:(NSPoint)position {
      // ... 检查指针索引
      char *data = (char*)[m_State mutableBytes] + pointerIndex * HIDStatePointerSize;
      
      // 设置X和Y坐标值
      *data = [VHIDPointerCollection clipCoordinateTo:position.x];
      *(data + 1) = -[VHIDPointerCollection clipCoordinateTo:position.y];
    }
    
  3. GAMainViewController.m 中的代理方法
    当VHIDDevice状态变化时,通过代理方法将状态传递给WJoyDevice:

    - (void)VHIDDevice:(VHIDDevice *)device stateChanged:(NSData *)state {
      [_virtualDevice updateHIDState:state];
    }
    
  4. WJoyDevice.m 中的更新方法
    最后,WJoyDevice将HID状态报告提交给系统:

    - (BOOL)updateHIDState:(NSData*)HIDState {
      return [m_Impl updateState:HIDState];
    }
    

总结

这个工程通过以下步骤将 Xbox One 控制器转换为 macOS 可识别的 HID 设备:

  1. 使用 IOKit 框架与 Xbox One 控制器通信,读取原始输入数据
  2. 将这些数据解析为结构化的按钮和摇杆状态
  3. 创建一个虚拟 HID 设备,生成标准的 HID 报告描述符
  4. 将控制器状态映射到虚拟 HID 设备状态
  5. 通过内核驱动程序将虚拟设备注册到系统

这种方法允许 macOS 将 Xbox One 控制器识别为标准游戏手柄,从而在不需要官方驱动的情况下实现兼容性。