From 4b0e459d627cb4b00b164f010f3aeb5d8769d693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20B=C3=B6sel?= Date: Tue, 17 May 2022 17:26:48 +0200 Subject: [PATCH] Add layers to pimoroni trackball --- docs/pimoroni_trackball.md | 18 ++++ kmk/modules/pimoroni_trackball.py | 152 +++++++++++++++++++++--------- 2 files changed, 124 insertions(+), 46 deletions(-) diff --git a/docs/pimoroni_trackball.md b/docs/pimoroni_trackball.md index 16f207e..10a183d 100644 --- a/docs/pimoroni_trackball.md +++ b/docs/pimoroni_trackball.md @@ -19,6 +19,24 @@ keyboard.modules.append(trackball) Module will also work when you cannot use `busio` and do `import bitbangio as io` instead. +### Key inputs + +If you have used this thing on a mobile device, you will know it excels at cursor movement. You can get the same for any of your Layer: + + +```python + +trackball = Trackball(i2c, mode=TrackballMode.MOUSE_MODE, layers=[ + # act like an encoder, input arrow keys + KeyLayer(KC.UP, KC.RIGHT, KC.DOWN, KC.LEFT, KC.ENTER), + # on layer 1 and above use the default pointing behavior + PointingLayer() +]) +``` + + +### Backlight + Setup backlight color using below commands: ```python diff --git a/kmk/modules/pimoroni_trackball.py b/kmk/modules/pimoroni_trackball.py index eec22ca..6c9201a 100644 --- a/kmk/modules/pimoroni_trackball.py +++ b/kmk/modules/pimoroni_trackball.py @@ -55,16 +55,95 @@ class TrackballMode: SCROLL_MODE = const(1) +class TrackballLayer(): + '''Just to show an interface for layers''' + def handle(self, keyboard, trackball, x, y, switch, state): + raise NotImplementedError + + +class PointingLayer(TrackballLayer): + '''the default behavior: act as trackball''' + def handle(self, keyboard, trackball, x, y, switch, state): + if trackball.mode == TrackballMode.MOUSE_MODE: + if x >= 0: + trackball.pointing_device.report_x[0] = x + else: + trackball.pointing_device.report_x[0] = 0xFF & x + if y >= 0: + trackball.pointing_device.report_y[0] = y + else: + trackball.pointing_device.report_y[0] = 0xFF & y + trackball.pointing_device.hid_pending = x != 0 or y != 0 + else: # SCROLL_MODE + trackball.pointing_device.report_w[0] = y + trackball.pointing_device.hid_pending = y != 0 + + if switch == 1: # Button pressed + trackball.pointing_device.button_status[0] |= trackball.pointing_device.MB_LMB + trackball.pointing_device.hid_pending = True + + if not state and trackball.previous_state is True: # Button released + trackball.pointing_device.button_status[0] &= ~trackball.pointing_device.MB_LMB + trackball.pointing_device.hid_pending = True + + trackball.previous_state = state + + +class KeyLayer(TrackballLayer): + '''Act like an encoder. ''' + + x = 0 + y = 0 + + def __init__(self, up, right, down, left, press, axis_snap=.25, steps=8): + self.up = up + self.right = right + self.down = down + self.left = left + self.press = press + '''snap to axis. the higher the value, the more we snap to the dominant direction''' + self.axis_snap = axis_snap + '''how many px we move to trigger a key tap, less means more key taps''' + self.steps = steps + + def handle(self, keyboard, trackball, x, y, switch, state): + if y and abs(x / y) < self.axis_snap: + x = 0 + if x and abs(y / x) < self.axis_snap: + y = 0 + + self.x += x + self.y += y + x_taps = self.x // self.steps + y_taps = self.y // self.steps + self.x %= self.steps + self.y %= self.steps + for i in range(x_taps, 0, 1): + keyboard.tap_key(self.left) + for i in range(x_taps, 0, -1): + keyboard.tap_key(self.right) + for i in range(y_taps, 0, 1): + keyboard.tap_key(self.up) + for i in range(y_taps, 0, -1): + keyboard.tap_key(self.down) + if switch and state: + keyboard.tap_key(self.press) + + class Trackball(Module): '''Module handles usage of Trackball Breakout by Pimoroni''' - def __init__(self, i2c, mode=TrackballMode.MOUSE_MODE, address=I2C_ADDRESS): + def __init__(self, i2c, mode=TrackballMode.MOUSE_MODE, address=I2C_ADDRESS, angle_offset=ANGLE_OFFSET, layers=None): + self.angle_offset = angle_offset + if layers is None: + layers = [PointingLayer()] self._i2c_address = address self._i2c_bus = i2c self.pointing_device = PointingDevice() self.mode = mode self.previous_state = False # click state + self.layers = layers self.polling_interval = 20 chip_id = struct.unpack('= 0: - self.pointing_device.report_x[0] = x_axis - else: - self.pointing_device.report_x[0] = 0xFF & x_axis - if y_axis >= 0: - self.pointing_device.report_y[0] = y_axis - else: - self.pointing_device.report_y[0] = 0xFF & y_axis - self.pointing_device.hid_pending = x_axis != 0 or y_axis != 0 - else: # SCROLL_MODE - if up >= 0: - self.pointing_device.report_w[0] = up - if down > 0: - self.pointing_device.report_w[0] = 0xFF & (0 - down) - self.pointing_device.hid_pending = up != 0 or down != 0 + x, y = self._calculate_movement(right - left, down - up) - if switch == 1: # Button pressed - self.pointing_device.button_status[0] |= self.pointing_device.MB_LMB - self.pointing_device.hid_pending = True + layer = self.layers[-1] + active_layer = keyboard.active_layers[0] + if len(self.layers) > active_layer: + layer = self.layers[active_layer] + layer.handle(keyboard, self, x, y, switch, state) - if not state and self.previous_state is True: # Button released - self.pointing_device.button_status[0] &= ~self.pointing_device.MB_LMB - self.pointing_device.hid_pending = True - - self.previous_state = state return def after_matrix_scan(self, keyboard): @@ -174,26 +234,6 @@ class Trackball(Module): ) return up, down, left, right, switch, switch_state - def _calculate_movement(self, raw_x, raw_y): - '''Calculate accelerated movement vector from raw data''' - if raw_x == 0 and raw_y == 0: - return 0, 0 - - var_accel = 1 - power = 2.5 - - angle_rad = math.atan2(raw_y, raw_x) + ANGLE_OFFSET - vector_length = math.sqrt(pow(raw_x, 2) + pow(raw_y, 2)) - vector_length = pow(vector_length * var_accel, power) - x = math.floor(vector_length * math.cos(angle_rad)) - y = math.floor(vector_length * math.sin(angle_rad)) - - limit = 127 # hid size limit - x_clamped = max(min(limit, x), -limit) - y_clamped = max(min(limit, y), -limit) - - return x_clamped, y_clamped - def _i2c_rdwr(self, data, length=0): '''Write and optionally read I2C data.''' while not self._i2c_bus.try_lock(): @@ -216,3 +256,23 @@ class Trackball(Module): def _tb_mode_press(self, key, keyboard, *args, **kwargs): self.mode = not self.mode + + def _calculate_movement(self, raw_x, raw_y): + '''Calculate accelerated movement vector from raw data''' + if raw_x == 0 and raw_y == 0: + return 0, 0 + + var_accel = 1 + power = 2.5 + + angle_rad = math.atan2(raw_y, raw_x) + self.angle_offset + vector_length = math.sqrt(pow(raw_x, 2) + pow(raw_y, 2)) + vector_length = pow(vector_length * var_accel, power) + x = math.floor(vector_length * math.cos(angle_rad)) + y = math.floor(vector_length * math.sin(angle_rad)) + + limit = 127 # hid size limit + x_clamped = max(min(limit, x), -limit) + y_clamped = max(min(limit, y), -limit) + + return x_clamped, y_clamped