kmk_firmware/kmk/hid.py
crazyiop 23fd3a9bed [usb] Allow kb to lost usb connection without getting lost
When the usb connection is lost (for example when plugged into a pc that
keep the usb powered when shut down) the kb is lost and need to be
unpluged to be rebooted.

My guess is that the failling `hid_report.send()` change some internal
state that could not recover by itself.

This prevent even trying to send the report when the usb is not
connected. This fix has been tested working on me and another discord
user so far. The result seems consistent even if the test period has not
been very long yet.
2022-08-05 18:35:16 +00:00

329 lines
9.5 KiB
Python

import supervisor
import usb_hid
from micropython import const
from storage import getmount
from kmk.keys import FIRST_KMK_INTERNAL_KEY, ConsumerKey, ModifierKey
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
class HIDModes:
NOOP = 0 # currently unused; for testing?
USB = 1
BLE = 2
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:
REPORT_BYTES = 8
def __init__(self, **kwargs):
self._prev_evt = bytearray(self.REPORT_BYTES)
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:]
self.post_init()
def __repr__(self):
return f'{self.__class__.__name__}(REPORT_BYTES={self.REPORT_BYTES})'
def post_init(self):
pass
def create_report(self, keys_pressed):
self.clear_all()
consumer_key = None
for key in keys_pressed:
if isinstance(key, ConsumerKey):
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:
if key.code >= FIRST_KMK_INTERNAL_KEY:
continue
if isinstance(key, ModifierKey):
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):
if self._evt != self._prev_evt:
self._prev_evt[:] = self._evt
self.hid_send(self._evt)
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):
if isinstance(modifier, ModifierKey):
if modifier.code == ModifierKey.FAKE_CODE:
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):
if isinstance(modifier, ModifierKey):
if modifier.code == ModifierKey.FAKE_CODE:
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
class USBHID(AbstractHID):
REPORT_BYTES = 9
def post_init(self):
self.devices = {}
for device in usb_hid.devices:
us = device.usage
up = device.usage_page
if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER:
self.devices[HIDReportTypes.CONSUMER] = device
continue
if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD:
self.devices[HIDReportTypes.KEYBOARD] = device
continue
if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE:
self.devices[HIDReportTypes.MOUSE] = device
continue
if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL:
self.devices[HIDReportTypes.SYSCONTROL] = device
continue
def hid_send(self, evt):
if not supervisor.runtime.usb_connected:
return
# int, can be looked up in HIDReportTypes
reporting_device_const = evt[0]
return self.devices[reporting_device_const].send_report(
evt[1 : HID_REPORT_SIZES[reporting_device_const] + 1]
)
class BLEHID(AbstractHID):
BLE_APPEARANCE_HID_KEYBOARD = const(961)
# Hardcoded in CPy
MAX_CONNECTIONS = const(2)
def __init__(self, ble_name=str(getmount('/').label), **kwargs):
self.ble_name = ble_name
super().__init__()
def post_init(self):
self.ble = BLERadio()
self.ble.name = self.ble_name
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
send_report attribute.'''
if not self.ble.connected:
return {}
result = {}
for device in self.hid.devices:
if not hasattr(device, 'send_report'):
continue
us = device.usage
up = device.usage_page
if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER:
result[HIDReportTypes.CONSUMER] = device
continue
if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD:
result[HIDReportTypes.KEYBOARD] = device
continue
if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE:
result[HIDReportTypes.MOUSE] = device
continue
if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL:
result[HIDReportTypes.SYSCONTROL] = device
continue
return result
def hid_send(self, evt):
if not self.ble.connected:
return
# int, can be looked up in HIDReportTypes
reporting_device_const = evt[0]
device = self.devices[reporting_device_const]
report_size = len(device._characteristic.value)
while len(evt) < report_size + 1:
evt.append(0)
return device.send_report(evt[1 : report_size + 1])
def clear_bonds(self):
import _bleio
_bleio.adapter.erase_bonding()
def start_advertising(self):
if not self.ble.advertising:
advertisement = ProvideServicesAdvertisement(self.hid)
advertisement.appearance = self.BLE_APPEARANCE_HID_KEYBOARD
self.ble.start_advertising(advertisement)
def stop_advertising(self):
self.ble.stop_advertising()