diff --git a/docs/en/boot.md b/docs/en/boot.md index c1631e17e..90df25abb 100644 --- a/docs/en/boot.md +++ b/docs/en/boot.md @@ -32,6 +32,7 @@ bootcfg( mouse: bool = True, nkro: bool = False, pan: bool = False, + six_axis: bool = False, storage: bool = True, usb_id: Optional[tuple[str, str]] = None, **kwargs, @@ -119,6 +120,11 @@ Enable panning, aka horizontal scrolling, for the pointing device, aka mouse, hid endpoint. +#### `six_axis` +Enable a HID endpoint for a six-axis spacemouse (with range +/-500) and change +the VID/PID to a SpaceMouse Compact. + + #### `storage` Disable storage if you don't want your computer to go "there's a new thumb drive I have to mount!" every time you plug in your keyboard. diff --git a/kmk/bootcfg.py b/kmk/bootcfg.py index 196f02110..49b73959c 100644 --- a/kmk/bootcfg.py +++ b/kmk/bootcfg.py @@ -21,6 +21,7 @@ def bootcfg( mouse: bool = True, nkro: bool = False, pan: bool = False, + six_axis: bool = False, storage: bool = True, usb_id: Optional[tuple[str, str]] = None, **kwargs, @@ -46,6 +47,37 @@ def bootcfg( # configure HID devices devices = [] + if (usb_id is not None) or six_axis: + import supervisor + + if hasattr(supervisor, 'set_usb_identification'): + usb_args = {} + if usb_id is not None: + usb_args['manufacturer'] = usb_id[0] + usb_args['product'] = usb_id[1] + if six_axis: + from kmk.hid_reports import six_axis + + usb_args['vid'] = 0x256F + usb_args['pid'] = 0xC635 # SpaceMouse Compact + + if keyboard: + if nkro: + devices.append(six_axis.NKRO_KEYBOARD) + else: + devices.append(six_axis.KEYBOARD) + keyboard = False + if mouse: + if pan: + devices.append(six_axis.POINTER) + else: + devices.append(six_axis.MOUSE) + mouse = False + if consumer_control: + devices.append(six_axis.CONSUMER_CONTROL) + consumer_control = False + devices.append(six_axis.SIX_AXIS) + supervisor.set_usb_identification(**usb_args) if keyboard: if nkro: from kmk.hid_reports import nkro_keyboard @@ -73,13 +105,6 @@ def bootcfg( usb_midi.disable() - # configure usb vendor and product id - if usb_id is not None: - import supervisor - - if hasattr(supervisor, 'set_usb_identification'): - supervisor.set_usb_identification(*usb_id) - # configure data serial if cdc_data: import usb_cdc diff --git a/kmk/hid.py b/kmk/hid.py index f201c14d3..381be74d5 100644 --- a/kmk/hid.py +++ b/kmk/hid.py @@ -4,7 +4,15 @@ from struct import pack, pack_into -from kmk.keys import Axis, ConsumerKey, KeyboardKey, ModifierKey, MouseKey +from kmk.keys import ( + Axis, + ConsumerKey, + KeyboardKey, + ModifierKey, + MouseKey, + SixAxis, + SpacemouseKey, +) from kmk.scheduler import cancel_task, create_task from kmk.utils import Debug, clamp @@ -32,11 +40,13 @@ class HIDModes: _USAGE_PAGE_CONSUMER = const(0x0C) _USAGE_PAGE_KEYBOARD = const(0x01) _USAGE_PAGE_MOUSE = const(0x01) +_USAGE_PAGE_SIXAXIS = const(0x01) _USAGE_PAGE_SYSCONTROL = const(0x01) _USAGE_CONSUMER = const(0x01) _USAGE_KEYBOARD = const(0x06) _USAGE_MOUSE = const(0x02) +_USAGE_SIXAXIS = const(0x08) _USAGE_SYSCONTROL = const(0x80) _REPORT_SIZE_CONSUMER = const(2) @@ -44,6 +54,8 @@ class HIDModes: _REPORT_SIZE_KEYBOARD_NKRO = const(16) _REPORT_SIZE_MOUSE = const(4) _REPORT_SIZE_MOUSE_HSCROLL = const(5) +_REPORT_SIZE_SIXAXIS = const(12) +_REPORT_SIZE_SIXAXIS_BUTTON = const(2) _REPORT_SIZE_SYSCONTROL = const(8) @@ -172,6 +184,42 @@ def __init__(self): super().__init__(_REPORT_SIZE_MOUSE_HSCROLL) +class SixAxisDeviceReport(Report): + def __init__(self, size=_REPORT_SIZE_SIXAXIS): + super().__init__(size) + + def move_six_axis(self, axis): + delta = clamp(axis.delta, -500, 500) + axis.delta -= delta + index = 2 * axis.code + try: + self.buffer[index] = 0xFF & delta + self.buffer[index + 1] = 0xFF & (delta >> 8) + self.pending = True + except IndexError: + if debug.enabled: + debug(axis, ' not supported') + + def get_action_map(self): + return {SixAxis: self.move_six_axis} + + +class SixAxisDeviceButtonReport(Report): + def __init__(self, size=_REPORT_SIZE_SIXAXIS_BUTTON): + super().__init__(size) + + def add_six_axis_button(self, key): + self.buffer[0] |= key.code + self.pending = True + + def remove_six_axis_button(self, key): + self.buffer[0] &= ~key.code + self.pending = True + + def get_action_map(self): + return {SpacemouseKey: self.add_six_axis_button} + + class AbstractHID: def __init__(self): self.report_map = {} @@ -192,7 +240,12 @@ def create_report(self, keys): def send(self): for report in self.device_map.keys(): if report.pending: - self.device_map[report].send_report(report.buffer) + if hasattr(report, 'move_six_axis'): + self.device_map[report].send_report(report.buffer, 1) + elif hasattr(report, 'add_six_axis_button'): + self.device_map[report].send_report(report.buffer, 3) + else: + self.device_map[report].send_report(report.buffer) report.pending = False def setup(self): @@ -203,6 +256,7 @@ def setup(self): self.setup_keyboard_hid() self.setup_consumer_control() self.setup_mouse_hid() + self.setup_sixaxis_hid() cancel_task(self._setup_task) self._setup_task = None @@ -243,6 +297,15 @@ def setup_mouse_hid(self): self.report_map.update(report.get_action_map()) self.device_map[report] = device + def setup_sixaxis_hid(self): + if device := find_device(self.devices, _USAGE_PAGE_SIXAXIS, _USAGE_SIXAXIS): + report = SixAxisDeviceReport() + self.report_map.update(report.get_action_map()) + self.device_map[report] = device + report = SixAxisDeviceButtonReport() + self.report_map.update(report.get_action_map()) + self.device_map[report] = device + def show_debug(self): for report in self.device_map.keys(): debug('use ', report.__class__.__name__) diff --git a/kmk/hid_reports/six_axis.py b/kmk/hid_reports/six_axis.py new file mode 100644 index 000000000..b289a125f --- /dev/null +++ b/kmk/hid_reports/six_axis.py @@ -0,0 +1,324 @@ +import usb_hid + +# fmt:off +report_descriptor = bytes( + ( + 0x05, 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, 0x08, # Usage (Multi-axis Controller) + 0xA1, 0x01, # Collection (Application) + 0xA1, 0x00, # Collection (Physical) + 0x85, 0x01, # Report ID (1) + 0x16, 0x0C, 0xFE, # Logical Minimum (-500) + 0x26, 0xF4, 0x01, # Logical Maximum (500) + 0x36, 0x00, 0x80, # Physical Minimum (-32768) + 0x46, 0xFF, 0x7F, # Physical Maximum (32767) + 0x55, 0x0C, # Unit Exponent (-4) + 0x65, 0x11, # Unit (System: SI Linear, Length: Centimeter) + 0x09, 0x30, # Usage (X) + 0x09, 0x31, # Usage (Y) + 0x09, 0x32, # Usage (Z) + 0x09, 0x33, # Usage (Rx) + 0x09, 0x34, # Usage (Ry) + 0x09, 0x35, # Usage (Rz) + 0x75, 0x10, # Report Size (16) + 0x95, 0x06, # Report Count (6) + 0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # End Collection + + 0xA1, 0x00, # Collection (Physical) + 0x85, 0x03, # Report ID (3) + 0x05, 0x09, # Usage Page (Button) + 0x19, 0x01, # Usage Minimum (1) + 0x29, 0x02, # Usage Maximum (2) + 0x15, 0x00, # Logical Minimum (0) + 0x25, 0x01, # Logical Maximum (1) + 0x35, 0x00, # Physical Minimum (0) + 0x45, 0x01, # Physical Maximum (1) + 0x75, 0x01, # Report Size (1) + 0x95, 0x02, # Report Count (2) + 0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, 0x0E, # Report Count (14) + 0x81, 0x03, # Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # End Collection + + 0xA1, 0x02, # Collection (Logical) + 0x85, 0x04, # Report ID (4) + 0x05, 0x08, # Usage Page (LEDs) + 0x09, 0x4B, # Usage (Generic Indicator) + 0x15, 0x00, # Logical Minimum (0) + 0x25, 0x01, # Logical Maximum (1) + 0x95, 0x01, # Report Count (1) + 0x75, 0x01, # Report Size (1) + 0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x95, 0x01, # Report Count (1) + 0x75, 0x07, # Report Size (7) + 0x91, 0x03, # Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0xC0, # End Collection + 0xC0, # End Collection + ) +) +# fmt:on + +SIX_AXIS = usb_hid.Device( + report_descriptor=report_descriptor, + usage_page=0x01, + usage=0x08, + report_ids=( + 0x01, + 0x03, + 0x04, + ), + in_report_lengths=( + 12, + 2, + 0, + ), + out_report_lengths=( + 0, + 0, + 1, + ), +) + + +# Keyboard descriptors using Report ID 5 + +# fmt:off +report_descriptor = bytes( + ( + 0x05, 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, 0x06, # Usage (Keyboard) + 0xA1, 0x01, # Collection (Application) + 0x85, 0x05, # Report ID (5) + + 0x05, 0x07, # Usage Page (Kbrd/Keypad) + 0x19, 0xE0, # Usage Minimum (224) + 0x29, 0xE7, # Usage Maximum (231) + 0x15, 0x00, # Logical Minimum (0) + 0x25, 0x01, # Logical Maximum (1) + 0x75, 0x01, # Report Size (1) + 0x95, 0x08, # Report Count (8) + 0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, 0x01, # Report Count (1) + 0x75, 0x08, # Report Size (8) + 0x81, 0x01, # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, 0x05, # Report Count (5) + 0x75, 0x01, # Report Size (1) + + 0x05, 0x08, # Usage Page (LEDs) + 0x19, 0x01, # Usage Minimum (Num Lock) + 0x29, 0x05, # Usage Maximum (Kana) + 0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x95, 0x01, # Report Count (1) + 0x75, 0x03, # Report Size (3) + 0x91, 0x01, # Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x95, 0x06, # Report Count (6) + 0x75, 0x08, # Report Size (8) + 0x15, 0x00, # Logical Minimum (0) + 0x26, 0xFF, 0x00, # Logical Maximum (255) + + 0x05, 0x07, # Usage Page (Kbrd/Keypad) + 0x19, 0x00, # Usage Minimum (0) + 0x2A, 0xFF, 0x00, # Usage Maximum (255) + 0x81, 0x00, # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # End Collection + ) +) +# fmt:on + +KEYBOARD = usb_hid.Device( + report_descriptor=report_descriptor, + usage_page=0x01, + usage=0x06, + report_ids=(0x05,), + in_report_lengths=(8,), + out_report_lengths=(1,), +) + + +# fmt:off +report_descriptor = bytes( + ( + 0x05, 0x01, # Usage Page (Generic Desktop Ctrls), + 0x09, 0x06, # Usage (Keyboard), + 0xA1, 0x01, # Collection (Application), + 0x85, 0x05, # Report ID (5) + # Modifiers + 0x05, 0x07, # Usage Page (Key Codes), + 0x19, 0xE0, # Usage Minimum (224), + 0x29, 0xE7, # Usage Maximum (231), + 0x15, 0x00, # Logical Minimum (0), + 0x25, 0x01, # Logical Maximum (1), + 0x75, 0x01, # Report Size (1), + 0x95, 0x08, # Report Count (8), + 0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + # LEDs + 0x05, 0x08, # Usage Page (LEDs), + 0x19, 0x01, # Usage Minimum (1), + 0x29, 0x05, # Usage Maximum (5), + 0x95, 0x05, # Report Count (5), + 0x75, 0x01, # Report Size (1), + 0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non- olatile) + 0x95, 0x01, # Report Count (1), + 0x75, 0x03, # Report Size (3), + 0x91, 0x01, # Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,N n-volatile) + # Keys + 0x05, 0x07, # Usage Page (Kbrd/Keypad), + 0x19, 0x00, # Usage Minimum (0), + 0x29, 0x77, # Usage Maximum (119), + 0x15, 0x00, # Logical Minimum (0), + 0x25, 0x01, # Logical Maximum(1), + 0x95, 0x78, # Report Count (120), + 0x75, 0x01, # Report Size (1), + 0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # End Collection + ) +) +# fmt:on + +NKRO_KEYBOARD = usb_hid.Device( + report_descriptor=report_descriptor, + usage_page=0x01, + usage=0x06, + report_ids=(0x05,), + in_report_lengths=(16,), + out_report_lengths=(1,), +) + + +# Mouse descriptors using Report ID 6 + +# fmt:off +report_descriptor = bytes( + ( + 0x05, 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, 0x02, # Usage (Mouse) + 0xA1, 0x01, # Collection (Application) + 0x09, 0x01, # Usage (Pointer) + 0xA1, 0x00, # Collection (Physical) + 0x85, 0x06, # Report ID (6) + + 0x05, 0x09, # Usage Page (Button) + 0x19, 0x01, # Usage Minimum (1) + 0x29, 0x05, # Usage Maximum (5) + 0x15, 0x00, # Logical Minimum (0) + 0x25, 0x01, # Logical Maximum (1) + 0x95, 0x05, # Report Count (5) + 0x75, 0x01, # Report Size (1) + 0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, 0x01, # Report Count (1) + 0x75, 0x03, # Report Size (3) + 0x81, 0x01, # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + + 0x05, 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, 0x30, # Usage (X) + 0x09, 0x31, # Usage (Y) + 0x09, 0x38, # Usage (Wheel) + 0x15, 0x81, # Logical Minimum (-127) + 0x25, 0x7F, # Logical Maximum (127) + 0x95, 0x03, # Report Count (3) + 0x75, 0x08, # Report Size (8) + 0x81, 0x06, # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # End Collection + 0xC0, # End Collection + ) +) +# fmt:on + + +MOUSE = usb_hid.Device( + report_descriptor=report_descriptor, + usage_page=0x01, + usage=0x02, + report_ids=(0x06,), + in_report_lengths=(4,), + out_report_lengths=(0,), +) + + +# fmt:off +report_descriptor = bytes( + ( + 0x05, 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, 0x02, # Usage (Mouse) + 0xA1, 0x01, # Collection (Application) + 0x09, 0x01, # Usage (Pointer) + 0xA1, 0x00, # Collection (Physical) + 0x85, 0x06, # Report ID (6) + + 0x05, 0x09, # Usage Page (Button) + 0x19, 0x01, # Usage Minimum (1) + 0x29, 0x05, # Usage Maximum (5) + 0x15, 0x00, # Logical Minimum (0) + 0x25, 0x01, # Logical Maximum (1) + 0x95, 0x05, # Report Count (5) + 0x75, 0x01, # Report Size (1) + 0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, 0x01, # Report Count (1) + 0x75, 0x03, # Report Size (3) + 0x81, 0x01, # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + + 0x05, 0x01, # Usage Page (Generic Desktop Ctrls) + 0x09, 0x30, # Usage (X) + 0x09, 0x31, # Usage (Y) + 0x09, 0x38, # Usage (Wheel) + 0x15, 0x81, # Logical Minimum (-127) + 0x25, 0x7F, # Logical Maximum (127) + 0x95, 0x03, # Report Count (3) + 0x75, 0x08, # Report Size (8) + 0x81, 0x06, # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) + + 0x05, 0x0C, # Usage Page (Consumer Devices) + 0x0A, 0x38, 0x02, # Usage (AC Pan) + 0x15, 0x81, # Logical Minimum (-127) + 0x25, 0x7F, # Logical Maximum (127) + 0x95, 0x01, # Report Count (1) + 0x75, 0x08, # Report Size (8) + 0x81, 0x06, # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # End Collection + 0xC0, # End Collection + ) +) +# fmt:on + + +POINTER = usb_hid.Device( + report_descriptor=report_descriptor, + usage_page=0x01, + usage=0x02, + report_ids=(0x06,), + in_report_lengths=(5,), + out_report_lengths=(0,), +) + + +# Consumer Control descriptor using Report ID 7 + +# fmt:off +report_descriptor = bytes( + ( + 0x05, 0x0C, # Usage Page (Consumer) + 0x09, 0x01, # Usage (Consumer Control) + 0xA1, 0x01, # Collection (Application) + 0x85, 0x07, # Report ID (7) + 0x75, 0x10, # Report Size (16) + 0x95, 0x01, # Report Count (1) + 0x15, 0x01, # Logical Minimum (1) + 0x26, 0x8C, 0x02, # Logical Maximum (652) + 0x19, 0x01, # Usage Minimum (Consumer Control) + 0x2A, 0x8C, 0x02, # Usage Maximum (AC Send) + 0x81, 0x00, # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0xC0, # End Collection + ) +) +# fmt:on + + +CONSUMER_CONTROL = usb_hid.Device( + report_descriptor=report_descriptor, + usage_page=0x0C, + usage=0x01, + report_ids=(0x07,), + in_report_lengths=(2,), + out_report_lengths=(0,), +) diff --git a/kmk/keys.py b/kmk/keys.py index df4959ee9..af59ba914 100644 --- a/kmk/keys.py +++ b/kmk/keys.py @@ -36,6 +36,11 @@ def move(self, keyboard: Keyboard, delta: int): keyboard.keys_pressed.discard(self) +class SixAxis(Axis): + def __repr__(self) -> str: + return f'SixAxis(code={self.code}, delta={self.delta})' + + class AX: P = Axis(3) W = Axis(2) @@ -43,6 +48,15 @@ class AX: Y = Axis(1) +class SM: + A = SixAxis(3) + B = SixAxis(4) + C = SixAxis(5) + X = SixAxis(0) + Y = SixAxis(1) + Z = SixAxis(2) + + def maybe_make_key( names: Tuple[str, ...], *args, @@ -528,6 +542,10 @@ class MouseKey(_DefaultKey): pass +class SpacemouseKey(_DefaultKey): + pass + + def make_key( names: Tuple[str, ...], constructor: Key = Key, diff --git a/kmk/kmk_keyboard.py b/kmk/kmk_keyboard.py index bc61d8bd1..f5557bd6b 100644 --- a/kmk/kmk_keyboard.py +++ b/kmk/kmk_keyboard.py @@ -7,7 +7,7 @@ from keypad import Event as KeyEvent from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes -from kmk.keys import KC, Axis, Key +from kmk.keys import KC, Axis, Key, SixAxis from kmk.modules import Module from kmk.scanners.keypad import MatrixScanner from kmk.scheduler import Task, cancel_task, create_task, get_due_task @@ -93,7 +93,7 @@ def _send_hid(self) -> None: self.hid_pending = False for key in self.keys_pressed: - if isinstance(key, Axis): + if isinstance(key, Axis) or isinstance(key, SixAxis): key.move(self, 0) def _handle_matrix_report(self, kevent: KeyEvent) -> None: