Xbox One 控制器轉換為 macOS HID 設備的工作原理分析
源代碼在 https://github.com/guilhermearaujo/xboxonecontrollerenabler.git
這個工程的核心功能是將 Xbox One 控制器(macOS 原生不支持的設備)轉換為 macOS 可識別的 HID 設備。這里通過分析代碼,詳細解釋其工作原理、設備描述和報告描述符的實現。
整體架構
該項目由三個主要部分組成:
- Xbox 控制器通信層:通過 IOKit 框架與 Xbox One 控制器進行 USB 通信
- 虛擬 HID 設備層:使用 VHID 框架創建虛擬 HID 設備
- 系統集成層:使用 WirtualJoy 框架將虛擬設備注冊到 macOS 系統
Xbox One 控制器設備描述
Xbox One 控制器的設備描述在代碼中通過 XboxOneButtonMap
結構體定義:
typedef struct {bool sync;bool dummy; // Always 0.bool menu; // Not entirely sure what these arebool view; // called on the new controllerbool 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 控制器通信:
- 通過 USB 供應商 ID (0x045e) 和產品 ID (0x02d1) 識別 Xbox One 控制器
- 使用 IOKit 框架打開設備并配置接口
- 初始化控制器并開始輪詢數據
- 在
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 設備,它通過組合 VHIDButtonCollection
和 VHIDPointerCollection
來管理按鈕和指針(搖桿)狀態:
- (id)initWithType:(VHIDDeviceType)typepointerCount:(NSUInteger)pointerCountbuttonCount:(NSUInteger)buttonCountisRelative:(BOOL)isRelative
{self = [super init];m_Type = type;m_Buttons = [[VHIDButtonCollection alloc] initWithButtonCount:buttonCount];m_Pointers = [[VHIDPointerCollection alloc] initWithPointerCount:pointerCountisRelative: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_COLLECTIONreturn result;
}
這個方法創建了一個標準的 HID 報告描述符,定義了設備類型(游戲手柄)、按鈕和指針(搖桿)的布局。
系統集成
WJoyDevice
和 WJoyDeviceImpl
類負責將虛擬 HID 設備注冊到 macOS 系統:
- 加載內核驅動程序
- 創建與驅動程序的連接
- 設置設備屬性(產品名稱、供應商 ID、產品 ID 等)
- 啟用設備并更新 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;
}
數據流轉換過程
整個數據流轉換過程如下:
GAXboxControllerCommunication
從 Xbox One 控制器讀取原始 USB 數據- 數據被解析為
XboxOneButtonMap
結構體 GAXboxController
處理這些數據并提供高級訪問方法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];// ... 設置其他指針
}
VHIDDevice
更新其內部狀態并通知代理WJoyDevice
將更新后的 HID 狀態發送到系統
報告提交流程
-
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]; }
-
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];elsedata[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]; }
-
GAMainViewController.m 中的代理方法:
當VHIDDevice狀態變化時,通過代理方法將狀態傳遞給WJoyDevice:- (void)VHIDDevice:(VHIDDevice *)device stateChanged:(NSData *)state {[_virtualDevice updateHIDState:state]; }
-
WJoyDevice.m 中的更新方法:
最后,WJoyDevice將HID狀態報告提交給系統:- (BOOL)updateHIDState:(NSData*)HIDState {return [m_Impl updateState:HIDState]; }
總結
這個工程通過以下步驟將 Xbox One 控制器轉換為 macOS 可識別的 HID 設備:
- 使用 IOKit 框架與 Xbox One 控制器通信,讀取原始輸入數據
- 將這些數據解析為結構化的按鈕和搖桿狀態
- 創建一個虛擬 HID 設備,生成標準的 HID 報告描述符
- 將控制器狀態映射到虛擬 HID 設備狀態
- 通過內核驅動程序將虛擬設備注冊到系統
這種方法允許 macOS 將 Xbox One 控制器識別為標準游戲手柄,從而在不需要官方驅動的情況下實現兼容性。