vita_gamepad/vitapad/backends/macos.py

182 lines
6.6 KiB
Python

from __future__ import annotations
import ctypes
import ctypes.util
import struct
from vitapad.protocol import Buttons, InputState
# Report 1: 16 buttons, 8-way hat, four 16-bit axes and two 8-bit triggers.
REPORT_DESCRIPTOR = bytes(
[
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01,
0x05, 0x09, 0x19, 0x01, 0x29, 0x10, 0x15, 0x00,
0x25, 0x01, 0x75, 0x01, 0x95, 0x10, 0x81, 0x02,
0x05, 0x01, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07,
0x35, 0x00, 0x46, 0x3B, 0x01, 0x65, 0x14, 0x75,
0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00, 0x75,
0x04, 0x95, 0x01, 0x81, 0x03, 0x09, 0x30, 0x09,
0x31, 0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26,
0xFF, 0xFF, 0x75, 0x10, 0x95, 0x04, 0x81, 0x02,
0x09, 0x32, 0x09, 0x35, 0x15, 0x00, 0x26, 0xFF,
0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x02, 0xC0,
]
)
BUTTON_ORDER = (
Buttons.A, Buttons.B, Buttons.X, Buttons.Y,
Buttons.LB, Buttons.RB, Buttons.BACK, Buttons.START,
Buttons.L3, Buttons.R3,
)
def _hat(buttons: int) -> int:
up, down = bool(buttons & Buttons.UP), bool(buttons & Buttons.DOWN)
left, right = bool(buttons & Buttons.LEFT), bool(buttons & Buttons.RIGHT)
if up and right:
return 1
if right and down:
return 3
if down and left:
return 5
if left and up:
return 7
if up and not down:
return 0
if right and not left:
return 2
if down and not up:
return 4
if left and not right:
return 6
return 8
class MacOSGamepad:
name = "Generic HID gamepad (IOHIDUserDevice)"
def __init__(self) -> None:
cf_path = ctypes.util.find_library("CoreFoundation")
io_path = ctypes.util.find_library("IOKit")
if not cf_path or not io_path:
raise RuntimeError("找不到 macOS CoreFoundation/IOKit")
self._cf = ctypes.CDLL(cf_path)
self._io = ctypes.CDLL(io_path)
self._configure_functions()
properties = self._create_properties()
self._device = self._io.IOHIDUserDeviceCreate(None, properties)
self._cf.CFRelease(properties)
if not self._device:
raise RuntimeError(
"无法创建 macOS 虚拟 HID 手柄;请尝试使用 sudo 运行"
)
def _configure_functions(self) -> None:
self._cf.CFStringCreateWithCString.restype = ctypes.c_void_p
self._cf.CFStringCreateWithCString.argtypes = [
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32
]
self._cf.CFDataCreate.restype = ctypes.c_void_p
self._cf.CFDataCreate.argtypes = [
ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_long
]
self._cf.CFNumberCreate.restype = ctypes.c_void_p
self._cf.CFNumberCreate.argtypes = [
ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p
]
self._cf.CFDictionaryCreateMutable.restype = ctypes.c_void_p
self._cf.CFDictionaryCreateMutable.argtypes = [
ctypes.c_void_p, ctypes.c_long, ctypes.c_void_p, ctypes.c_void_p
]
self._cf.CFDictionarySetValue.argtypes = [
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p
]
self._cf.CFRelease.argtypes = [ctypes.c_void_p]
self._io.IOHIDUserDeviceCreate.restype = ctypes.c_void_p
self._io.IOHIDUserDeviceCreate.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
self._io.IOHIDUserDeviceHandleReport.argtypes = [
ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_long
]
self._io.IOHIDUserDeviceHandleReport.restype = ctypes.c_int
self._dictionary_key_callbacks = ctypes.c_byte.in_dll(
self._cf, "kCFTypeDictionaryKeyCallBacks"
)
self._dictionary_value_callbacks = ctypes.c_byte.in_dll(
self._cf, "kCFTypeDictionaryValueCallBacks"
)
def _string(self, value: str) -> int:
# kCFStringEncodingUTF8
return self._cf.CFStringCreateWithCString(None, value.encode(), 0x08000100)
def _create_properties(self) -> int:
dictionary = self._cf.CFDictionaryCreateMutable(
None,
0,
ctypes.byref(self._dictionary_key_callbacks),
ctypes.byref(self._dictionary_value_callbacks),
)
retained: list[int] = []
def put_number(key: str, value: int) -> None:
key_ref = self._string(key)
raw = ctypes.c_int(value)
value_ref = self._cf.CFNumberCreate(None, 9, ctypes.byref(raw))
self._cf.CFDictionarySetValue(dictionary, key_ref, value_ref)
retained.extend((key_ref, value_ref))
def put_string(key: str, value: str) -> None:
key_ref, value_ref = self._string(key), self._string(value)
self._cf.CFDictionarySetValue(dictionary, key_ref, value_ref)
retained.extend((key_ref, value_ref))
descriptor_array = (ctypes.c_uint8 * len(REPORT_DESCRIPTOR)).from_buffer_copy(
REPORT_DESCRIPTOR
)
descriptor = self._cf.CFDataCreate(
None, descriptor_array, len(REPORT_DESCRIPTOR)
)
descriptor_key = self._string("ReportDescriptor")
self._cf.CFDictionarySetValue(dictionary, descriptor_key, descriptor)
retained.extend((descriptor_key, descriptor))
put_number("VendorID", 0x054C)
put_number("ProductID", 0x09CC)
put_number("VersionNumber", 0x0100)
put_number("PrimaryUsagePage", 0x01)
put_number("PrimaryUsage", 0x05)
put_string("Manufacturer", "Vita Gamepad")
put_string("Product", "PS Vita Network Gamepad")
for item in retained:
self._cf.CFRelease(item)
return dictionary
def update(self, state: InputState) -> None:
hid_buttons = 0
for index, mask in enumerate(BUTTON_ORDER):
if state.buttons & mask:
hid_buttons |= 1 << index
report = struct.pack(
"<BHBHHHHBB",
1,
hid_buttons,
_hat(state.buttons),
state.lx * 257,
state.ly * 257,
state.rx * 257,
state.ry * 257,
state.lt,
state.rt,
)
buffer = (ctypes.c_uint8 * len(report)).from_buffer_copy(report)
result = self._io.IOHIDUserDeviceHandleReport(
self._device, buffer, len(report)
)
if result != 0:
raise RuntimeError(f"IOHIDUserDeviceHandleReport 失败: 0x{result:08x}")
def close(self) -> None:
if self._device:
self.update(InputState.neutral())
self._cf.CFRelease(self._device)
self._device = None