Final Code

Published on 2020-09-11 in Dorsch 40k Keyboard.

In the process of moving the shift keys around, I have simplified the code somewhat, made it better at handling some corner cases, and fixed some bugs. Just in case someone wants to make a similar build, I’m putting it below. In the future it might grow into a proper CircuitPython library perhaps – then it will get its own repository.

———- more ———-

import board
import digitalio
import usb_hid
import time


COLS = (board.A6, board.A1, board.A4, board.A3, board.D6,
        board.SCL, board.SDA, board.D12, board.D10, board.D13)
ROWS = (board.MOSI, board.AREF, board.D11, board.D5)
LEDS = (board.A2, board.A5, board.TX, board.RX, board.D9)


class Keyboard:
    def __init__(self, matrix, cols=COLS, rows=ROWS):
        self.matrix = matrix
        self.cols = [digitalio.DigitalInOut(pin) for pin in cols]
        self.rows = [digitalio.DigitalInOut(pin) for pin in rows]
        for col in self.cols:
            col.switch_to_output(value=0)
        for row in self.rows:
            row.switch_to_input(pull=digitalio.Pull.DOWN)
        for self.device in usb_hid.devices:
            if self.device.usage == 0x06 and self.device.usage_page == 0x01:
                break
        else:
            raise RuntimeError("no HID keyboard device")
        self.debounce = bytearray(len(cols))
        self.last_state = bytearray(len(cols))
        self.current_layer = 0
        self.pressed_keys = set()
        self.last_held = 0
        self.release_next = 0

    def scan(self):
        if self.release_next:
            try:
                self.pressed_keys.remove(self.release_next)
            except KeyError:
                pass
            self.release_next = 0
        for x, col in enumerate(self.cols):
            col.value = 1
            debounce_bits = 0
            for y, row in enumerate(self.rows):
                state = row.value
                debounce_bits |= state << y
                if state != bool(self.debounce[x] & (1 << y)):
                    continue
                last_state = bool(self.last_state[x] & (1 << y))
                if state:
                    self.last_state[x] |= 1 << y
                else:
                    self.last_state[x] &= ~(1 << y)
                if state == last_state:
                    continue
                if state:
                    self.press(x, y)
                else:
                    self.release(x, y)
            col.value = 0
            self.debounce[x] = debounce_bits

    def press(self, x, y):
        if self.last_held:
            self.last_held = 0
        code = self.matrix[self.current_layer][y][x]
        if code & 0xff00:
            if (code & 0xff) != 0:
                self.last_held = code
            if code & 0xff00 == 0x0800:
                self.current_layer = 1
            else:
                self.pressed_keys.add(code & 0xff00)
            return
        code = self.matrix[self.current_layer][y][x]
        self.pressed_keys.add(code)

    def release_all(self, x, y):
        for layer in 0, 1:
            for mask in 0xffff, 0xff00, 0x00ff:
                code = self.matrix[layer][y][x] & mask
                try:
                    self.pressed_keys.remove(code)
                except KeyError:
                    pass
                if code & 0xff00 == 0x0800:
                    self.current_layer = 0


    def release(self, x, y):
        for layer in 0, 1:
            code = self.matrix[self.current_layer][y][x]
            if self.last_held == code:
                self.release_all(x, y)
                self.pressed_keys.add(code & 0xff)
                self.release_next = code & 0xff
                self.last_held = 0
                return
        self.release_all(x, y)

    def send_report(self, pressed_keys):
        report = bytearray(8)
        report_mod_keys = memoryview(report)[0:1]
        report_no_mod_keys = memoryview(report)[2:]
        keys = 0
        for code in pressed_keys:
            if code == 0:
                continue
            elif code == 0x0800:
                continue
            elif code & 0xff00 and code & 0xff == 0:
                modifier = (code >> 8) - 1
                report_mod_keys[0] |= 1 << modifier
            elif keys < 6:
                report_no_mod_keys[keys] = code
                keys += 1
        self.device.send_report(report)

    def run(self):
        last_pressed_keys = set()
        while True:
            self.scan()
            if self.pressed_keys != last_pressed_keys:
                self.send_report(self.pressed_keys)
                last_pressed_keys = set(self.pressed_keys)
            time.sleep(0.01)