From ac89e51ff17970ba49f01c7e1b09881b2f742667 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Wed, 11 Nov 2020 11:19:57 -0800 Subject: [PATCH] HID fixup --- kmk/extensions/layers.py | 85 +++++++++----------- kmk/hid.py | 162 +++++++++++++++++++-------------------- kmk/kmk_keyboard.py | 4 - 3 files changed, 119 insertions(+), 132 deletions(-) diff --git a/kmk/extensions/layers.py b/kmk/extensions/layers.py index 2ff3761..aaf4840 100644 --- a/kmk/extensions/layers.py +++ b/kmk/extensions/layers.py @@ -91,23 +91,21 @@ class Layers(Extension): return @staticmethod - def _df_pressed(key, state, *args, **kwargs): + def _df_pressed(key, keyboard, *args, **kwargs): ''' Switches the default layer ''' - state.active_layers[-1] = key.meta.layer - return state + keyboard.active_layers[-1] = key.meta.layer @staticmethod - def _mo_pressed(key, state, *args, **kwargs): + def _mo_pressed(key, keyboard, *args, **kwargs): ''' Momentarily activates layer, switches off when you let go ''' - state.active_layers.insert(0, key.meta.layer) - return state + keyboard.active_layers.insert(0, key.meta.layer) @staticmethod - def _mo_released(key, state, KC, *args, **kwargs): + def _mo_released(key, keyboard, KC, *args, **kwargs): # remove the first instance of the target layer # from the active list # under almost all normal use cases, this will @@ -117,95 +115,88 @@ class Layers(Extension): # triggered by MO() and then defaulting to the MO()'s layer # would result in no layers active try: - del_idx = state.active_layers.index(key.meta.layer) - del state.active_layers[del_idx] + del_idx = keyboard.active_layers.index(key.meta.layer) + del keyboard.active_layers[del_idx] except ValueError: pass - return state - - def _lm_pressed(self, key, state, *args, **kwargs): + def _lm_pressed(self, key, keyboard, *args, **kwargs): ''' As MO(layer) but with mod active ''' - state.hid_pending = True + keyboard.hid_pending = True # Sets the timer start and acts like MO otherwise - state.keys_pressed.add(key.meta.kc) - return self._mo_pressed(key, state, *args, **kwargs) + keyboard.keys_pressed.add(key.meta.kc) + self._mo_pressed(key, keyboard, *args, **kwargs) - def _lm_released(self, key, state, *args, **kwargs): + def _lm_released(self, key, keyboard, *args, **kwargs): ''' As MO(layer) but with mod active ''' - state.hid_pending = True - state.keys_pressed.discard(key.meta.kc) - return self._mo_released(key, state, *args, **kwargs) + keyboard.hid_pending = True + keyboard.keys_pressed.discard(key.meta.kc) + self._mo_released(key, keyboard, *args, **kwargs) - def _lt_pressed(self, key, state, *args, **kwargs): + def _lt_pressed(self, key, keyboard, *args, **kwargs): # Sets the timer start and acts like MO otherwise self.start_time[LayerType.LT] = accurate_ticks() - return self._mo_pressed(key, state, *args, **kwargs) + self._mo_pressed(key, keyboard, *args, **kwargs) - def _lt_released(self, key, state, *args, **kwargs): + def _lt_released(self, key, keyboard, *args, **kwargs): # On keyup, check timer, and press key if needed. if self.start_time[LayerType.LT] and ( accurate_ticks_diff( - accurate_ticks(), self.start_time[LayerType.LT], state.tap_time + accurate_ticks(), self.start_time[LayerType.LT], keyboard.tap_time ) ): - state.hid_pending = True - state.tap_key(key.meta.kc) + keyboard.hid_pending = True + keyboard.tap_key(key.meta.kc) - self._mo_released(key, state, *args, **kwargs) + self._mo_released(key, keyboard, *args, **kwargs) self.start_time[LayerType.LT] = None - return state @staticmethod - def _tg_pressed(key, state, *args, **kwargs): + def _tg_pressed(key, keyboard, *args, **kwargs): ''' Toggles the layer (enables it if not active, and vise versa) ''' # See mo_released for implementation details around this try: - del_idx = state.active_layers.index(key.meta.layer) - del state.active_layers[del_idx] + del_idx = keyboard.active_layers.index(key.meta.layer) + del keyboard.active_layers[del_idx] except ValueError: - state.active_layers.insert(0, key.meta.layer) - - return state + keyboard.active_layers.insert(0, key.meta.layer) @staticmethod - def _to_pressed(key, state, *args, **kwargs): + def _to_pressed(key, keyboard, *args, **kwargs): ''' Activates layer and deactivates all other layers ''' - state.active_layers.clear() - state.active_layers.insert(0, key.meta.layer) + keyboard.active_layers.clear() + keyboard.active_layers.insert(0, key.meta.layer) - return state - - def _tt_pressed(self, key, state, *args, **kwargs): + def _tt_pressed(self, key, keyboard, *args, **kwargs): ''' Momentarily activates layer if held, toggles it if tapped repeatedly ''' if self.start_time[LayerType.TT] is None: # Sets the timer start and acts like MO otherwise self.start_time[LayerType.TT] = accurate_ticks() - return self._mo_pressed(key, state, *args, **kwargs) + self._mo_pressed(key, keyboard, *args, **kwargs) + return elif accurate_ticks_diff( - accurate_ticks(), self.start_time[LayerType.TT], state.tap_time + accurate_ticks(), self.start_time[LayerType.TT], keyboard.tap_time ): self.start_time[LayerType.TT] = None - return self._tg_pressed(key, state, *args, **kwargs) + self._tg_pressed(key, keyboard, *args, **kwargs) + return return None - def _tt_released(self, key, state, *args, **kwargs): + def _tt_released(self, key, keyboard, *args, **kwargs): if self.start_time[LayerType.TT] is None or not accurate_ticks_diff( - accurate_ticks(), self.start_time[LayerType.TT], state.tap_time + accurate_ticks(), self.start_time[LayerType.TT], keyboard.tap_time ): # On first press, works like MO. On second press, does nothing unless let up within # time window, then acts like TG. self.start_time[LayerType.TT] = None - return self._mo_released(key, state, *args, **kwargs) - - return state + self._mo_released(key, keyboard, *args, **kwargs) diff --git a/kmk/hid.py b/kmk/hid.py index 92e857a..810ba79 100644 --- a/kmk/hid.py +++ b/kmk/hid.py @@ -3,11 +3,20 @@ from micropython import const 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 + from storage import getmount +except ImportError: + # BLE not supported on this platform + pass + class HIDModes: NOOP = 0 # currently unused; for testing? USB = 1 - BLE = 2 # currently unused; for bluetooth + BLE = 2 ALL_MODES = (NOOP, USB, BLE) @@ -225,106 +234,97 @@ class USBHID(AbstractHID): class BLEHID(AbstractHID): - try: - from adafruit_ble import BLERadio - from adafruit_ble.advertising.standard import ProvideServicesAdvertisement - from adafruit_ble.services.standard.hid import HIDService - from storage import getmount + BLE_APPEARANCE_HID_KEYBOARD = const(961) + # Hardcoded in CPy + MAX_CONNECTIONS = const(2) - BLE_APPEARANCE_HID_KEYBOARD = const(961) - # Hardcoded in CPy - MAX_CONNECTIONS = const(2) + def post_init(self, ble_name=str(getmount('/').label), **kwargs): + self.conn_id = -1 - def post_init(self, ble_name=str(getmount('/').label), **kwargs): - self.conn_id = -1 + self.ble = BLERadio() + self.ble.name = ble_name + self.hid = HIDService() + self.hid.protocol_mode = 0 # Boot protocol - self.ble = self.BLERadio() - self.ble.name = ble_name - self.hid = self.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() - # 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() + self.conn_id = 0 - self.conn_id = 0 + @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 [] - @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 = [] + # Security issue: + # This introduces a race condition. Let's say you have 2 active + # connections: Alice and Bob - Alice is connection 1 and Bob 2. + # Now Chuck who has already paired with the device in the past + # (this assumption is needed only in the case of LESC) + # wants to gather the keystrokes you send to Alice. You have + # selected right now to talk to Alice (1) and you're typing a secret. + # If Chuck kicks Alice off and is quick enough to connect to you, + # which means quicker than the running interval of this function, + # he'll be earlier in the `self.hid.devices` so will take over the + # selected 1 position in the resulted array. + # If no LESC is in place, Chuck can sniff the keystrokes anyway + for device in self.hid.devices: + if hasattr(device, 'send_report'): + result.append(device) - result = [] - # Security issue: - # This introduces a race condition. Let's say you have 2 active - # connections: Alice and Bob - Alice is connection 1 and Bob 2. - # Now Chuck who has already paired with the device in the past - # (this assumption is needed only in the case of LESC) - # wants to gather the keystrokes you send to Alice. You have - # selected right now to talk to Alice (1) and you're typing a secret. - # If Chuck kicks Alice off and is quick enough to connect to you, - # which means quicker than the running interval of this function, - # he'll be earlier in the `self.hid.devices` so will take over the - # selected 1 position in the resulted array. - # If no LESC is in place, Chuck can sniff the keystrokes anyway - for device in self.hid.devices: - if hasattr(device, 'send_report'): - result.append(device) + return result - return result + def _check_connection(self): + devices = self.devices + if not devices: + return False - def _check_connection(self): - devices = self.devices - if not devices: - return False + if self.conn_id >= len(devices): + self.conn_id = len(devices) - 1 - if self.conn_id >= len(devices): - self.conn_id = len(devices) - 1 + if self.conn_id < 0: + return False - if self.conn_id < 0: - return False + if not devices[self.conn_id]: + return False - if not devices[self.conn_id]: - return False + return True - return True + def hid_send(self, evt): + if not self._check_connection(): + return - def hid_send(self, evt): - if not self._check_connection(): - return + device = self.devices[self.conn_id] - device = self.devices[self.conn_id] + while len(evt) < len(device._characteristic.value) + 1: + evt.append(0) - while len(evt) < len(device._characteristic.value) + 1: - evt.append(0) + return device.send_report(evt[1:]) - return device.send_report(evt[1:]) + def clear_bonds(self): + import _bleio - def clear_bonds(self): - import _bleio + _bleio.adapter.erase_bonding() - _bleio.adapter.erase_bonding() + def next_connection(self): + self.conn_id = (self.conn_id + 1) % len(self.devices) - def next_connection(self): - self.conn_id = (self.conn_id + 1) % len(self.devices) + def previous_connection(self): + self.conn_id = (self.conn_id - 1) % len(self.devices) - def previous_connection(self): - self.conn_id = (self.conn_id - 1) % len(self.devices) + def start_advertising(self): + advertisement = ProvideServicesAdvertisement(self.hid) + advertisement.appearance = self.BLE_APPEARANCE_HID_KEYBOARD - def start_advertising(self): - advertisement = self.ProvideServicesAdvertisement(self.hid) - advertisement.appearance = self.BLE_APPEARANCE_HID_KEYBOARD + self.ble.start_advertising(advertisement) - self.ble.start_advertising(advertisement) - - def stop_advertising(self): - self.ble.stop_advertising() - - except ImportError: - print('Bluetooth unsupported') + def stop_advertising(self): + self.ble.stop_advertising() diff --git a/kmk/kmk_keyboard.py b/kmk/kmk_keyboard.py index cf155a0..6df6519 100644 --- a/kmk/kmk_keyboard.py +++ b/kmk/kmk_keyboard.py @@ -335,10 +335,6 @@ class KMKKeyboard: return self - # Only one GC to allow for extentions to have room. There are random memory allocations - # issues due to some devices not properly cleaning memory on reset - gc.collect() - def go(self, hid_type=HIDModes.USB, **kwargs): self.hid_type = hid_type