# SPDX-FileCopyrightText: 2021 Sandy Macdonald # # SPDX-License-Identifier: MIT """ `Keybow 2040 CircuitPython library` ==================================================== CircuitPython driver for the Pimoroni Keybow 2040. Drop the keybow2040.py file into your `lib` folder on your `CIRCUITPY` drive. * Author: Sandy Macdonald Notes -------------------- **Hardware:** * Pimoroni Keybow 2040 _ **Software and Dependencies:** * Adafruit CircuitPython firmware for Keybow 2040: _ * Adafruit CircuitPython IS31FL3731 library: _ """ import time import board from adafruit_is31fl3731.keybow2040 import Keybow2040 as Display from digitalio import DigitalInOut, Direction, Pull # These are the 16 switches on Keybow, with their board-defined names. _PINS = [board.SW0, board.SW1, board.SW2, board.SW3, board.SW4, board.SW5, board.SW6, board.SW7, board.SW8, board.SW9, board.SW10, board.SW11, board.SW12, board.SW13, board.SW14, board.SW15] NUM_KEYS = 16 class Keybow2040(object): """ Represents a Keybow 2040 and hence a set of Key instances with associated LEDs and key behaviours. :param i2c: the I2C bus for Keybow 2040 """ def __init__(self, i2c): self.pins = _PINS self.display = Display(i2c) self.keys = [] self.time_of_last_press = time.monotonic() self.time_since_last_press = None self.led_sleep_enabled = False self.led_sleep_time = 60 self.sleeping = False self.was_asleep = False self.last_led_states = None # self.rotation = 0 for i in range(len(self.pins)): _key = Key(i, self.pins[i], self.display) self.keys.append(_key) def update(self): # Call this in each iteration of your while loop to update # to update everything's state, e.g. `keybow.update()` for _key in self.keys: _key.update() # Used to work out the sleep behaviour, by keeping track # of the time of the last key press. if self.any_pressed(): self.time_of_last_press = time.monotonic() self.sleeping = False self.time_since_last_press = time.monotonic() - self.time_of_last_press # If LED sleep is enabled, but not engaged, check if enough time # has elapsed to engage sleep. If engaged, record the state of the # LEDs, so it can be restored on wake. if self.led_sleep_enabled and not self.sleeping: if time.monotonic() - self.time_of_last_press > self.led_sleep_time: self.sleeping = True self.last_led_states = [k.rgb if k.lit else [0, 0, 0] for k in self.keys] self.set_all(0, 0, 0) self.was_asleep = True # If it was sleeping, but is no longer, then restore LED states. if not self.sleeping and self.was_asleep: for k in range(len(self.keys)): self.keys[k].set_led(*self.last_led_states[k]) self.was_asleep = False def set_led(self, number, r, g, b): # Set an individual key's LED to an RGB value by its number. self.keys[number].set_led(r, g, b) def set_all(self, r, g, b): # Set all of Keybow's LEDs to an RGB value. if not self.sleeping: for _key in self.keys: _key.set_led(r, g, b) else: for _key in self.keys: _key.led_off() def get_states(self): # Returns a Boolean list of Keybow's key states # (0=not pressed, 1=pressed). _states = [_key.state for _key in self.keys] return _states def get_pressed(self): # Returns a list of key numbers currently pressed. _pressed = [_key.number for _key in self.keys if _key.state == True] return _pressed def any_pressed(self): # Returns True if any key is pressed, False if none are pressed. if any(self.get_states()): return True else: return False def none_pressed(self): # Returns True if none of the keys are pressed, False is any key # is pressed. if not any(self.get_states()): return True else: return False def on_press(self, _key, handler=None): # Attaches a press function to a key, via a decorator. This is stored as # `key.press_function` in the key's attributes, and run if necessary # as part of the key's update function (and hence Keybow's update # function). It can be attached as follows: # @keybow.on_press(key) # def press_handler(key, pressed): # if pressed: # do something # else: # do something else if _key is None: return def attach_handler(handler): _key.press_function = handler if handler is not None: attach_handler(handler) else: return attach_handler def on_release(self, _key, handler=None): # Attaches a release function to a key, via a decorator. This is stored # as `key.release_function` in the key's attributes, and run if # necessary as part of the key's update function (and hence Keybow's # update function). It can be attached as follows: # @keybow.on_release(key) # def release_handler(key): # do something if _key is None: return def attach_handler(handler): _key.release_function = handler if handler is not None: attach_handler(handler) else: return attach_handler def on_hold(self, _key, handler=None): # Attaches a hold unction to a key, via a decorator. This is stored as # `key.hold_function` in the key's attributes, and run if necessary # as part of the key's update function (and hence Keybow's update # function). It can be attached as follows: # @keybow.on_hold(key) # def hold_handler(key): # do something if _key is None: return def attach_handler(handler): _key.hold_function = handler if handler is not None: attach_handler(handler) else: return attach_handler # def rotate(self, degrees): # # Rotates all of Keybow's keys by a number of degrees, clamped to # # the closest multiple of 90 degrees. Because it shuffles the order # # of the Key instances, all of the associated attributes of the key # # are retained. The x/y coordinate of the keys are rotated also. It # # also handles negative degrees, e.g. -90 to rotate 90 degrees anti- # # clockwise. # # Rotate as follows: `keybow.rotate(270)` # self.rotation = degrees # num_rotations = degrees // 90 # if num_rotations == 0: # return # if num_rotations < 1: # num_rotations = 4 + num_rotations # matrix = [[(x * 4) + y for y in range(4)] for x in range(4)] # for r in range(num_rotations): # matrix = zip(*matrix[::-1]) # matrix = [list(x) for x in list(matrix)] # flat_matrix = [x for y in matrix for x in y] # for i in range(len(self.keys)): # self.keys[i].number = flat_matrix[i] # self.keys = sorted(self.keys, key=lambda x:x.number) class Key: """ Represents a key on Keybow 2040, with associated switch and LED behaviours. :param number: the key number (0-15) to associate with the key :param pin: the pin object for the key, e.g. board.SW0 :param display: the IS31FL3731 matrix instance for the LEDs """ def __init__(self, number, pin, display): self.pin = pin self.number = number self.switch = DigitalInOut(self.pin) self.switch.direction = Direction.INPUT self.switch.pull = Pull.UP self.state = 0 self.pressed = 0 self.last_state = None self.time_of_last_press = time.monotonic() self.time_since_last_press = None self.time_held_for = 0 self.held = False self.hold_time = 0.75 self.modifier = False self.rgb = [0, 0, 0] self.lit = False self.xy = self.get_xy() self.x, self.y = self.xy self.display = display self.led_off() self.press_function = None self.release_function = None self.hold_function = None self.press_func_fired = False self.hold_func_fired = False self.debounce = 0.125 self.key_locked = False def get_state(self): # Returns the state of the key (0=not pressed, 1=pressed). return int(not self.switch.value) def update(self): # Updates the state of the key and updates all of its # attributes. self.time_since_last_press = time.monotonic() - self.time_of_last_press # Keys get locked during the debounce time. if self.time_since_last_press < self.debounce: self.key_locked = True else: self.key_locked = False self.state = self.get_state() self.pressed = self.state update_time = time.monotonic() # If there's a `press_function` attached, then call it, # returning the key object and the pressed state. if self.press_function is not None and self.pressed and not self.press_func_fired and not self.key_locked: self.press_function(self) self.press_func_fired = True # time.sleep(0.05) # A little debounce # If the key has been pressed and releases, then call # the `release_function`, if one is attached. if not self.pressed and self.last_state == True: if self.release_function is not None: self.release_function(self) self.last_state = False self.press_func_fired = False if not self.pressed: self.time_held_for = 0 self.last_state = False # If the key has just been pressed, then record the # `time_of_last_press`, and update last_state. elif self.pressed and self.last_state == False: self.time_of_last_press = update_time self.last_state = True # If the key is pressed and held, then update the # `time_held_for` variable. elif self.pressed and self.last_state == True: self.time_held_for = update_time - self.time_of_last_press self.last_state = True # If the `hold_time` theshold is crossed, then call the # `hold_function` if one is attached. The `hold_func_fired` # ensures that the function is only called once. if self.time_held_for > self.hold_time: self.held = True if self.hold_function is not None and not self.hold_func_fired: self.hold_function(self) self.hold_func_fired = True else: self.held = False self.hold_func_fired = False def get_xy(self): # Returns the x/y coordinate of a key from 0,0 to 3,3. return number_to_xy(self.number) def get_number(self): # Returns the key number, from 0 to 15. return xy_to_number(self.x, self.y) def is_modifier(self): # Designates a modifier key, so you can hold the modifier # and tap another key to trigger additional behaviours. if self.modifier: return True else: return False def set_led(self, r, g, b): # Set this key's LED to an RGB value. if [r, g, b] == [0, 0, 0]: self.lit = False else: self.lit = True self.rgb = [r, g, b] self.display.pixelrgb(self.x, self.y, r, g, b) def led_on(self): # Turn the LED on, using its current RGB value. r, g, b = self.rgb self.set_led(r, g, b) def led_off(self): # Turn the LED off. self.set_led(0, 0, 0) def led_state(self, state): # Set the LED's state (0=off, 1=on) state = int(state) if state == 0: self.led_off() elif state == 1: self.led_on() else: return def toggle_led(self, rgb=None): # Toggle the LED's state, retaining its RGB value for when it's toggled # back on. Can also be passed an RGB tuple to set the colour as part of # the toggle. if rgb is not None: self.rgb = rgb if self.lit: self.led_off() else: self.led_on() def __str__(self): # When printed, show the key's state (0 or 1). return self.state def xy_to_number(x, y): # Convert an x/y coordinate to key number. return x + (y * 4) def number_to_xy(number): # Convert a number to an x/y coordinate. x = number % 4 y = number // 4 return (x, y) def hsv_to_rgb(h, s, v): # Convert an HSV (0.0-1.0) colour to RGB (0-255) if s == 0.0: rgb = [v, v, v] i = int(h * 6.0) f = (h*6.)-i; p,q,t = v*(1.-s), v*(1.-s*f), v*(1.-s*(1.-f)); i%=6 if i == 0: rgb = [v, t, p] if i == 1: rgb = [q, v, p] if i == 2: rgb = [p, v, t] if i == 3: rgb = [p, q, v] if i == 4: rgb = [t, p, v] if i == 5: rgb = [v, p, q] rgb = tuple(int(c * 255) for c in rgb) return rgb