2022-08-02 22:59:19 +02:00
|
|
|
import supervisor
|
2019-07-25 09:32:20 +02:00
|
|
|
import usb_hid
|
2020-10-21 21:19:42 +02:00
|
|
|
from micropython import const
|
2019-07-25 09:32:20 +02:00
|
|
|
|
2020-10-21 21:19:42 +02:00
|
|
|
from storage import getmount
|
|
|
|
|
2021-06-20 22:59:59 +02:00
|
|
|
from kmk.keys import FIRST_KMK_INTERNAL_KEY, ConsumerKey, ModifierKey
|
|
|
|
|
2020-10-21 21:19:42 +02:00
|
|
|
try:
|
|
|
|
from adafruit_ble import BLERadio
|
|
|
|
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
|
|
|
|
from adafruit_ble.services.standard.hid import HIDService
|
|
|
|
except ImportError:
|
|
|
|
# BLE not supported on this platform
|
|
|
|
pass
|
2018-10-16 13:04:39 +02:00
|
|
|
|
|
|
|
|
Continue to shuffle and burn stuff
- Remove the concept of "mcus". With only one target platform
(CircuitPython), it no longer makes a bunch of sense and has been kept
around for "what if" reasons, complicating our import chains and eating
up RAM for pointless subclasses. If you're a `board`, you derive from
`KeyboardConfig`. If you're a handwire, the user will derive from
`KeyboardConfig`. The end. As part of this, `kmk.hid` was refactored
heavily to emphasize that CircuitPython is our only supported HID stack,
with stubs for future HID implementations (`USB_HID` becomes
`AbstractHID`, probably only usable for testing purposes,
`CircuitPython_USB_HID` becomes `USBHID`, and `BLEHID` is added with an
immediate `NotImplementedError` on instantiation)
- `KeyboardConfig` can now take a HID type at runtime. The NRF52840
boards will happily run in either configuration once CircuitPython
support is in place, and a completely separate `mcu` subclass for each
mode made no sense. This also potentially allows runtime *swaps* of HID
driver down the line, but no code has been added to this effect. The
default, and only functional value, for this is `HIDModes.USB`
- Most consts have been moved to more logical homes - often, the main
or, often only, component that uses them. `DiodeOrientation` moved to
`kmk.matrix`, and anything HID-related moved to `kmk.hid`
2019-07-25 09:58:23 +02:00
|
|
|
class HIDModes:
|
|
|
|
NOOP = 0 # currently unused; for testing?
|
|
|
|
USB = 1
|
2020-10-21 21:19:42 +02:00
|
|
|
BLE = 2
|
Continue to shuffle and burn stuff
- Remove the concept of "mcus". With only one target platform
(CircuitPython), it no longer makes a bunch of sense and has been kept
around for "what if" reasons, complicating our import chains and eating
up RAM for pointless subclasses. If you're a `board`, you derive from
`KeyboardConfig`. If you're a handwire, the user will derive from
`KeyboardConfig`. The end. As part of this, `kmk.hid` was refactored
heavily to emphasize that CircuitPython is our only supported HID stack,
with stubs for future HID implementations (`USB_HID` becomes
`AbstractHID`, probably only usable for testing purposes,
`CircuitPython_USB_HID` becomes `USBHID`, and `BLEHID` is added with an
immediate `NotImplementedError` on instantiation)
- `KeyboardConfig` can now take a HID type at runtime. The NRF52840
boards will happily run in either configuration once CircuitPython
support is in place, and a completely separate `mcu` subclass for each
mode made no sense. This also potentially allows runtime *swaps* of HID
driver down the line, but no code has been added to this effect. The
default, and only functional value, for this is `HIDModes.USB`
- Most consts have been moved to more logical homes - often, the main
or, often only, component that uses them. `DiodeOrientation` moved to
`kmk.matrix`, and anything HID-related moved to `kmk.hid`
2019-07-25 09:58:23 +02:00
|
|
|
|
|
|
|
ALL_MODES = (NOOP, USB, BLE)
|
|
|
|
|
|
|
|
|
|
|
|
class HIDReportTypes:
|
|
|
|
KEYBOARD = 1
|
|
|
|
MOUSE = 2
|
|
|
|
CONSUMER = 3
|
|
|
|
SYSCONTROL = 4
|
|
|
|
|
|
|
|
|
|
|
|
class HIDUsage:
|
|
|
|
KEYBOARD = 0x06
|
|
|
|
MOUSE = 0x02
|
|
|
|
CONSUMER = 0x01
|
|
|
|
SYSCONTROL = 0x80
|
|
|
|
|
|
|
|
|
|
|
|
class HIDUsagePage:
|
|
|
|
CONSUMER = 0x0C
|
|
|
|
KEYBOARD = MOUSE = SYSCONTROL = 0x01
|
|
|
|
|
|
|
|
|
|
|
|
HID_REPORT_SIZES = {
|
|
|
|
HIDReportTypes.KEYBOARD: 8,
|
|
|
|
HIDReportTypes.MOUSE: 4,
|
|
|
|
HIDReportTypes.CONSUMER: 2,
|
|
|
|
HIDReportTypes.SYSCONTROL: 8, # TODO find the correct value for this
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class AbstractHID:
|
2018-10-16 13:04:39 +02:00
|
|
|
REPORT_BYTES = 8
|
|
|
|
|
2021-09-17 15:47:41 +02:00
|
|
|
def __init__(self, **kwargs):
|
2021-12-05 15:24:51 +01:00
|
|
|
self._prev_evt = bytearray(self.REPORT_BYTES)
|
2018-10-16 13:04:39 +02:00
|
|
|
self._evt = bytearray(self.REPORT_BYTES)
|
|
|
|
self.report_device = memoryview(self._evt)[0:1]
|
|
|
|
self.report_device[0] = HIDReportTypes.KEYBOARD
|
|
|
|
|
|
|
|
# Landmine alert for HIDReportTypes.KEYBOARD: byte index 1 of this view
|
|
|
|
# is "reserved" and evidently (mostly?) unused. However, other modes (or
|
|
|
|
# at least consumer, so far) will use this byte, which is the main reason
|
|
|
|
# this view exists. For KEYBOARD, use report_mods and report_non_mods
|
|
|
|
self.report_keys = memoryview(self._evt)[1:]
|
|
|
|
|
|
|
|
self.report_mods = memoryview(self._evt)[1:2]
|
|
|
|
self.report_non_mods = memoryview(self._evt)[3:]
|
|
|
|
|
2020-10-21 21:19:42 +02:00
|
|
|
self.post_init()
|
2018-10-16 13:04:39 +02:00
|
|
|
|
2019-07-13 00:16:33 +02:00
|
|
|
def __repr__(self):
|
2022-06-11 23:54:01 +02:00
|
|
|
return f'{self.__class__.__name__}(REPORT_BYTES={self.REPORT_BYTES})'
|
2019-07-13 00:16:33 +02:00
|
|
|
|
2018-10-16 13:04:39 +02:00
|
|
|
def post_init(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def create_report(self, keys_pressed):
|
|
|
|
self.clear_all()
|
|
|
|
|
|
|
|
consumer_key = None
|
|
|
|
for key in keys_pressed:
|
2018-12-30 00:29:11 +01:00
|
|
|
if isinstance(key, ConsumerKey):
|
2018-10-16 13:04:39 +02:00
|
|
|
consumer_key = key
|
|
|
|
break
|
|
|
|
|
|
|
|
reporting_device = self.report_device[0]
|
|
|
|
needed_reporting_device = HIDReportTypes.KEYBOARD
|
|
|
|
|
|
|
|
if consumer_key:
|
|
|
|
needed_reporting_device = HIDReportTypes.CONSUMER
|
|
|
|
|
|
|
|
if reporting_device != needed_reporting_device:
|
|
|
|
# If we are about to change reporting devices, release
|
|
|
|
# all keys and close our proverbial tab on the existing
|
|
|
|
# device, or keys will get stuck (mostly when releasing
|
|
|
|
# media/consumer keys)
|
|
|
|
self.send()
|
|
|
|
|
|
|
|
self.report_device[0] = needed_reporting_device
|
|
|
|
|
|
|
|
if consumer_key:
|
|
|
|
self.add_key(consumer_key)
|
|
|
|
else:
|
|
|
|
for key in keys_pressed:
|
2018-12-30 00:29:11 +01:00
|
|
|
if key.code >= FIRST_KMK_INTERNAL_KEY:
|
2018-10-16 13:04:39 +02:00
|
|
|
continue
|
|
|
|
|
2018-12-30 00:29:11 +01:00
|
|
|
if isinstance(key, ModifierKey):
|
2018-10-16 13:04:39 +02:00
|
|
|
self.add_modifier(key)
|
|
|
|
else:
|
|
|
|
self.add_key(key)
|
|
|
|
|
|
|
|
if key.has_modifiers:
|
|
|
|
for mod in key.has_modifiers:
|
|
|
|
self.add_modifier(mod)
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
def hid_send(self, evt):
|
|
|
|
# Don't raise a NotImplementedError so this can serve as our "dummy" HID
|
|
|
|
# when MCU/board doesn't define one to use (which should almost always be
|
|
|
|
# the CircuitPython-targeting one, except when unit testing or doing
|
|
|
|
# something truly bizarre. This will likely change eventually when Bluetooth
|
|
|
|
# is added)
|
|
|
|
pass
|
|
|
|
|
|
|
|
def send(self):
|
2022-02-02 22:50:45 +01:00
|
|
|
if self._evt != self._prev_evt:
|
2021-12-05 15:24:51 +01:00
|
|
|
self._prev_evt[:] = self._evt
|
|
|
|
self.hid_send(self._evt)
|
2018-10-16 13:04:39 +02:00
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
def clear_all(self):
|
|
|
|
for idx, _ in enumerate(self.report_keys):
|
|
|
|
self.report_keys[idx] = 0x00
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
def clear_non_modifiers(self):
|
|
|
|
for idx, _ in enumerate(self.report_non_mods):
|
|
|
|
self.report_non_mods[idx] = 0x00
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
def add_modifier(self, modifier):
|
2018-12-30 00:29:11 +01:00
|
|
|
if isinstance(modifier, ModifierKey):
|
|
|
|
if modifier.code == ModifierKey.FAKE_CODE:
|
2018-10-16 13:04:39 +02:00
|
|
|
for mod in modifier.has_modifiers:
|
|
|
|
self.report_mods[0] |= mod
|
|
|
|
else:
|
|
|
|
self.report_mods[0] |= modifier.code
|
|
|
|
else:
|
|
|
|
self.report_mods[0] |= modifier
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
def remove_modifier(self, modifier):
|
2018-12-30 00:29:11 +01:00
|
|
|
if isinstance(modifier, ModifierKey):
|
|
|
|
if modifier.code == ModifierKey.FAKE_CODE:
|
2018-10-16 13:04:39 +02:00
|
|
|
for mod in modifier.has_modifiers:
|
|
|
|
self.report_mods[0] ^= mod
|
|
|
|
else:
|
|
|
|
self.report_mods[0] ^= modifier.code
|
|
|
|
else:
|
|
|
|
self.report_mods[0] ^= modifier
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
def add_key(self, key):
|
|
|
|
# Try to find the first empty slot in the key report, and fill it
|
|
|
|
placed = False
|
|
|
|
|
|
|
|
where_to_place = self.report_non_mods
|
|
|
|
|
|
|
|
if self.report_device[0] == HIDReportTypes.CONSUMER:
|
|
|
|
where_to_place = self.report_keys
|
|
|
|
|
|
|
|
for idx, _ in enumerate(where_to_place):
|
|
|
|
if where_to_place[idx] == 0x00:
|
|
|
|
where_to_place[idx] = key.code
|
|
|
|
placed = True
|
|
|
|
break
|
|
|
|
|
|
|
|
if not placed:
|
|
|
|
# TODO what do we do here?......
|
|
|
|
pass
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
def remove_key(self, key):
|
|
|
|
where_to_place = self.report_non_mods
|
|
|
|
|
|
|
|
if self.report_device[0] == HIDReportTypes.CONSUMER:
|
|
|
|
where_to_place = self.report_keys
|
|
|
|
|
|
|
|
for idx, _ in enumerate(where_to_place):
|
|
|
|
if where_to_place[idx] == key.code:
|
|
|
|
where_to_place[idx] = 0x00
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
Continue to shuffle and burn stuff
- Remove the concept of "mcus". With only one target platform
(CircuitPython), it no longer makes a bunch of sense and has been kept
around for "what if" reasons, complicating our import chains and eating
up RAM for pointless subclasses. If you're a `board`, you derive from
`KeyboardConfig`. If you're a handwire, the user will derive from
`KeyboardConfig`. The end. As part of this, `kmk.hid` was refactored
heavily to emphasize that CircuitPython is our only supported HID stack,
with stubs for future HID implementations (`USB_HID` becomes
`AbstractHID`, probably only usable for testing purposes,
`CircuitPython_USB_HID` becomes `USBHID`, and `BLEHID` is added with an
immediate `NotImplementedError` on instantiation)
- `KeyboardConfig` can now take a HID type at runtime. The NRF52840
boards will happily run in either configuration once CircuitPython
support is in place, and a completely separate `mcu` subclass for each
mode made no sense. This also potentially allows runtime *swaps* of HID
driver down the line, but no code has been added to this effect. The
default, and only functional value, for this is `HIDModes.USB`
- Most consts have been moved to more logical homes - often, the main
or, often only, component that uses them. `DiodeOrientation` moved to
`kmk.matrix`, and anything HID-related moved to `kmk.hid`
2019-07-25 09:58:23 +02:00
|
|
|
class USBHID(AbstractHID):
|
2019-07-25 09:32:20 +02:00
|
|
|
REPORT_BYTES = 9
|
2019-07-25 07:57:11 +02:00
|
|
|
|
2020-10-21 21:19:42 +02:00
|
|
|
def post_init(self):
|
2019-07-25 09:32:20 +02:00
|
|
|
self.devices = {}
|
2019-07-25 07:57:11 +02:00
|
|
|
|
2019-07-25 09:32:20 +02:00
|
|
|
for device in usb_hid.devices:
|
|
|
|
us = device.usage
|
|
|
|
up = device.usage_page
|
2018-10-16 13:04:39 +02:00
|
|
|
|
2019-07-25 09:32:20 +02:00
|
|
|
if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER:
|
|
|
|
self.devices[HIDReportTypes.CONSUMER] = device
|
|
|
|
continue
|
2018-10-16 13:04:39 +02:00
|
|
|
|
2019-07-25 09:32:20 +02:00
|
|
|
if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD:
|
|
|
|
self.devices[HIDReportTypes.KEYBOARD] = device
|
|
|
|
continue
|
2018-10-16 13:04:39 +02:00
|
|
|
|
2019-07-25 09:32:20 +02:00
|
|
|
if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE:
|
|
|
|
self.devices[HIDReportTypes.MOUSE] = device
|
|
|
|
continue
|
2018-10-16 13:04:39 +02:00
|
|
|
|
2019-07-25 09:32:20 +02:00
|
|
|
if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL:
|
|
|
|
self.devices[HIDReportTypes.SYSCONTROL] = device
|
|
|
|
continue
|
2018-10-16 13:04:39 +02:00
|
|
|
|
2019-07-25 09:32:20 +02:00
|
|
|
def hid_send(self, evt):
|
2022-08-02 22:59:19 +02:00
|
|
|
if not supervisor.runtime.usb_connected:
|
|
|
|
return
|
|
|
|
|
2019-07-25 09:32:20 +02:00
|
|
|
# int, can be looked up in HIDReportTypes
|
2021-12-05 17:36:20 +01:00
|
|
|
reporting_device_const = evt[0]
|
2018-10-16 13:04:39 +02:00
|
|
|
|
2019-07-25 09:32:20 +02:00
|
|
|
return self.devices[reporting_device_const].send_report(
|
|
|
|
evt[1 : HID_REPORT_SIZES[reporting_device_const] + 1]
|
|
|
|
)
|
2020-10-21 21:19:42 +02:00
|
|
|
|
|
|
|
|
|
|
|
class BLEHID(AbstractHID):
|
|
|
|
BLE_APPEARANCE_HID_KEYBOARD = const(961)
|
|
|
|
# Hardcoded in CPy
|
|
|
|
MAX_CONNECTIONS = const(2)
|
|
|
|
|
2021-09-17 15:47:41 +02:00
|
|
|
def __init__(self, ble_name=str(getmount('/').label), **kwargs):
|
|
|
|
self.ble_name = ble_name
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
def post_init(self):
|
2020-10-21 21:19:42 +02:00
|
|
|
self.ble = BLERadio()
|
2021-09-17 15:47:41 +02:00
|
|
|
self.ble.name = self.ble_name
|
2020-10-21 21:19:42 +02:00
|
|
|
self.hid = HIDService()
|
|
|
|
self.hid.protocol_mode = 0 # Boot protocol
|
|
|
|
|
|
|
|
# Security-wise this is not right. While you're away someone turns
|
|
|
|
# on your keyboard and they can pair with it nice and clean and then
|
|
|
|
# listen to keystrokes.
|
|
|
|
# On the other hand we don't have LESC so it's like shouting your
|
|
|
|
# keystrokes in the air
|
|
|
|
if not self.ble.connected or not self.hid.devices:
|
|
|
|
self.start_advertising()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def devices(self):
|
|
|
|
'''Search through the provided list of devices to find the ones with the
|
2021-06-20 22:59:59 +02:00
|
|
|
send_report attribute.'''
|
2020-10-21 21:19:42 +02:00
|
|
|
if not self.ble.connected:
|
2021-10-01 15:37:43 +02:00
|
|
|
return {}
|
2020-10-21 21:19:42 +02:00
|
|
|
|
2021-10-01 15:37:43 +02:00
|
|
|
result = {}
|
|
|
|
|
|
|
|
for device in self.hid.devices:
|
|
|
|
if not hasattr(device, 'send_report'):
|
|
|
|
continue
|
|
|
|
us = device.usage
|
|
|
|
up = device.usage_page
|
2020-10-21 21:19:42 +02:00
|
|
|
|
2021-10-01 15:37:43 +02:00
|
|
|
if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER:
|
|
|
|
result[HIDReportTypes.CONSUMER] = device
|
|
|
|
continue
|
2020-10-21 21:19:42 +02:00
|
|
|
|
2021-10-01 15:37:43 +02:00
|
|
|
if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD:
|
|
|
|
result[HIDReportTypes.KEYBOARD] = device
|
|
|
|
continue
|
2020-10-21 21:19:42 +02:00
|
|
|
|
2021-10-01 15:37:43 +02:00
|
|
|
if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE:
|
|
|
|
result[HIDReportTypes.MOUSE] = device
|
|
|
|
continue
|
2020-10-21 21:19:42 +02:00
|
|
|
|
2021-10-01 15:37:43 +02:00
|
|
|
if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL:
|
|
|
|
result[HIDReportTypes.SYSCONTROL] = device
|
|
|
|
continue
|
2020-10-21 21:19:42 +02:00
|
|
|
|
2021-10-01 15:37:43 +02:00
|
|
|
return result
|
2020-10-21 21:19:42 +02:00
|
|
|
|
|
|
|
def hid_send(self, evt):
|
2021-10-01 15:37:43 +02:00
|
|
|
if not self.ble.connected:
|
2020-10-21 21:19:42 +02:00
|
|
|
return
|
|
|
|
|
2021-10-01 15:37:43 +02:00
|
|
|
# int, can be looked up in HIDReportTypes
|
2021-12-05 17:36:20 +01:00
|
|
|
reporting_device_const = evt[0]
|
2021-10-01 15:37:43 +02:00
|
|
|
|
|
|
|
device = self.devices[reporting_device_const]
|
2020-10-21 21:19:42 +02:00
|
|
|
|
2021-10-01 15:37:43 +02:00
|
|
|
report_size = len(device._characteristic.value)
|
|
|
|
while len(evt) < report_size + 1:
|
2020-10-21 21:19:42 +02:00
|
|
|
evt.append(0)
|
|
|
|
|
2021-10-01 15:37:43 +02:00
|
|
|
return device.send_report(evt[1 : report_size + 1])
|
2020-10-21 21:19:42 +02:00
|
|
|
|
|
|
|
def clear_bonds(self):
|
|
|
|
import _bleio
|
|
|
|
|
|
|
|
_bleio.adapter.erase_bonding()
|
|
|
|
|
|
|
|
def start_advertising(self):
|
2022-03-17 18:57:35 +01:00
|
|
|
if not self.ble.advertising:
|
|
|
|
advertisement = ProvideServicesAdvertisement(self.hid)
|
|
|
|
advertisement.appearance = self.BLE_APPEARANCE_HID_KEYBOARD
|
2020-10-21 21:19:42 +02:00
|
|
|
|
2022-03-17 18:57:35 +01:00
|
|
|
self.ble.start_advertising(advertisement)
|
2020-10-21 21:19:42 +02:00
|
|
|
|
|
|
|
def stop_advertising(self):
|
|
|
|
self.ble.stop_advertising()
|