LapBoard

Benodigdheden

  • 2 × Seeed XIAO RP2040-boards (of andere microcontrollers op basis van de RP2040)
  • Beide boards geflashed met CircuitPython (versie 10.2.1)

Opmerking: Momenteel wordt alleen terminal-input doorgestuurd. Het toetsenbord werkt niet systeembreed en het touchpad wordt nog niet ondersteund.

Credits: Dit project is samen ontwikkeld met Árni.

---

Spiraal 1 - Het toetsenbord werkend krijgen

De eerste stap was om het toetsenbord werkend te krijgen. Eén microcontroller vangt de USB-seriële gegevens van de laptop op en stuurt deze via UART door naar een tweede microcontroller. De tweede microcontroller fungeert als een USB HID-toetsenbord met behulp van de adafruit_hid-bibliotheek.

flowchart LR
L[Laptop]
T["scherm (terminal)"]
C["RP2040<br/>USB CDC"]
H["RP2040<br/>USB HID"]
D[Doelapparaat]
L --> T
T -->|USB CDC| C
C -->|UART| H
H -->|USB HID| D

Hardware-opstelling: Eén RP2040 fungeert als een USB CDC naar UART-bridge, terwijl de tweede zich presenteert als een USB HID-toetsenbord aan het doelapparaat.

De seriële microcontroller instellen

Maak eerst een boot.py-bestand aan op de microcontroller en voeg de volgende code toe:

import usb_cdc
usb_cdc.enable(console=True, data=True)

Voeg vervolgens de volgende code toe aan code.py:

import usb_cdc
import time
import busio
import board

uart = busio.UART(board.TX, board.RX, baudrate=115200)
time.sleep(0.5)
serial = usb_cdc.data

while True:
    if serial.in_waiting > 0:
        data = serial.read(serial.in_waiting)
        print("Sending", len(data), "bytes:", data)
        try:
            uart.write(data)
        except OSError as e:
            print("UART write error:", e)

De HID-microcontroller instellen

Gebruik op de tweede microcontroller de volgende code in code.py:

import busio
import board
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS

uart = busio.UART(board.TX, board.RX, baudrate=115200, timeout=0.01)
kbd = Keyboard(usb_hid.devices)
layout = KeyboardLayoutUS(kbd)

def tap(keycode):
    kbd.press(keycode)
    kbd.release(keycode)

while True:
    data = uart.read(1)
    if not data:
        continue
    b = data[0]
    if b == 0x1b:
        seq = uart.read(2)
        if seq and len(seq) == 2 and seq[0] == 0x5b:
            arrow = {
                0x41: Keycode.UP_ARROW,
                0x42: Keycode.DOWN_ARROW,
                0x43: Keycode.RIGHT_ARROW,
                0x44: Keycode.LEFT_ARROW,
            }.get(seq[1])
            if arrow:
                tap(arrow)
            else:
                tap(Keycode.ESCAPE)
    elif b in (0x0d, 0x0a):
        tap(Keycode.ENTER)
    elif b in (0x7f, 0x08):
        tap(Keycode.BACKSPACE)
    elif b == 0x09:
        tap(Keycode.TAB)
    elif 0x20 <= b <= 0x7e:
        layout.write(chr(b))

Het apparaat gebruiken

Om te beginnen met het vastleggen van toetsenbordinvoer en het doorsturen naar de HID-microcontroller, voer je uit:

screen /dev/ttyACM1 115200

Druk op Ctrl+C om het doorsturen te stoppen.

Testen van LapBoard tijdens Fab26: Toetsenbordinvoer van de ThinkPad (links) wordt via de RP2040-bridge doorgestuurd en verschijnt op de doel-laptop (rechts).

---

Spiraal 2 - Systeembrede keyboard capture

Stap 1

Eerst moest ik begrijpen hoe het laptoptoetsenbord werkte. Ik begon met het uitvoeren van:

cat /proc/bus/input/devices

Dit toonde alle inputapparaten die aan de laptop waren gekoppeld, inclusief de aan/uit-knop, de klepschakelaar, het touchpad en het toetsenbord.

AT Translated Set 2 keyboard Handlers=sysrq kbd event3 leds

Het toetsenbord stond vermeld als /dev/input/event3.

Vervolgens installeerde en startte ik evtest om te zien welke gegevens het toetsenbord produceerde.

sudo apt install evtest
sudo evtest

Ik kon nu de ruwe toetsgebeurtenissen (key events) zien. In tegenstelling tot het eerste prototype waren dit geen ASCII-gegevens, maar Linux input events.

Daarna maakte ik een klein Python-programma om te experimenteren met het direct uitlezen van het toetsenbord. Ik maakte een virtuele omgeving aan en installeerde evdev.

python3 -m venv .venv
source .venv/bin/activate
pip install evdev
pip freeze > requirements.txt

Ik schreef vervolgens het volgende programma:

from evdev import InputDevice, categorize, ecodes

kbd = InputDevice("/dev/input/event3")
print(kbd)

for event in kbd.read_loop():
    if event.type == ecodes.EV_KEY:
        print(categorize(event))

Om het uit te voeren: sudo .venv/bin/python keyboard.py

Het programma detecteerde succesvol toetsaanslagen en het loslaten van toetsen, inclusief modificatietoetsen zoals Shift, Ctrl en Alt, alsook functietoetsen en pijltjestoetsen. Dit bevestigde dat ik toetsenbordgebeurtenissen systeembreed kon vastleggen, wat de basis vormt voor het doorsturen naar de RP2040 in de volgende stap.

Stap 2

Vervolgens installeerde ik pyserial:

pip install pyserial

Ik paste keyboard.py aan zodat Linux-toetsgebeurtenissen via USB CDC naar de eerste RP2040 werden verzonden:

from evdev import InputDevice, ecodes
import serial

kbd = InputDevice("/dev/input/event3")
ser = serial.Serial("/dev/ttyACM1", 115200, timeout=1)

print(kbd)
print("Sending to", ser.port)

try:
    for event in kbd.read_loop():
        if event.type != ecodes.EV_KEY:
            continue
        key = ecodes.KEY.get(event.code, str(event.code))
        message = f"{event.value},{key}\n"
        ser.write(message.encode())
        print(message, end="")
except KeyboardInterrupt:
    print("\nStopped.")
finally:
    ser.close()

Om te verifiëren dat de eerste RP2040 de gegevens ontving, opende ik de CircuitPython-console ervan:

screen /dev/ttyACM0 115200

Ik kon nu zien dat de toetsgebeurtenissen via USB CDC op de RP2040 arriveerden.

Stap 3

De volgende stap was om de tweede RP2040 de toetsgebeurtenissen via UART te laten ontvangen en deze om te zetten in USB HID-toetsenbordgebeurtenissen.

Ik paste de code op de HID RP2040 als volgt aan:

import busio
import board
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode

uart = busio.UART(
    board.TX,
    board.RX,
    baudrate=115200,
    timeout=0.01)

kbd = Keyboard(usb_hid.devices)

KEYMAP = {
    "KEY_A": Keycode.A,
    "KEY_B": Keycode.B,
    "KEY_C": Keycode.C,
    "KEY_D": Keycode.D,
    "KEY_E": Keycode.E,
    "KEY_F": Keycode.F,
    "KEY_G": Keycode.G,
    "KEY_H": Keycode.H,
    "KEY_I": Keycode.I,
    "KEY_J": Keycode.J,
    "KEY_K": Keycode.K,
    "KEY_L": Keycode.L,
    "KEY_M": Keycode.M,
    "KEY_N": Keycode.N,
    "KEY_O": Keycode.O,
    "KEY_P": Keycode.P,
    "KEY_Q": Keycode.Q,
    "KEY_R": Keycode.R,
    "KEY_S": Keycode.S,
    "KEY_T": Keycode.T,
    "KEY_U": Keycode.U,
    "KEY_V": Keycode.V,
    "KEY_W": Keycode.W,
    "KEY_X": Keycode.X,
    "KEY_Y": Keycode.Y,
    "KEY_Z": Keycode.Z,
    "KEY_SPACE": Keycode.SPACE,
    "KEY_ENTER": Keycode.ENTER,
    "KEY_TAB": Keycode.TAB,
    "KEY_BACKSPACE": Keycode.BACKSPACE,
    "KEY_ESC": Keycode.ESCAPE,
    "KEY_LEFTSHIFT": Keycode.LEFT_SHIFT,
    "KEY_RIGHTSHIFT": Keycode.RIGHT_SHIFT,
    "KEY_LEFTCTRL": Keycode.LEFT_CONTROL,
    "KEY_RIGHTCTRL": Keycode.RIGHT_CONTROL,
    "KEY_LEFTALT": Keycode.LEFT_ALT,
    "KEY_RIGHTALT": Keycode.RIGHT_ALT,
    "KEY_UP": Keycode.UP_ARROW,
    "KEY_DOWN": Keycode.DOWN_ARROW,
    "KEY_LEFT": Keycode.LEFT_ARROW,
    "KEY_RIGHT": Keycode.RIGHT_ARROW,
}

buffer = b""

while True:
    data = uart.read(32)
    if not data:
        continue
    buffer += data
    while b"\n" in buffer:
        line, buffer = buffer.split(b"\n", 1)
        try:
            decoded = line.decode("utf-8")
            value_text, key_name = decoded.split(",", 1)
            value = int(value_text)
        except (ValueError, UnicodeError):
            continue
        keycode = KEYMAP.get(key_name)
        if keycode is None:
            print("Unknown key:", key_name)
            continue
        if value == 1:
            kbd.press(keycode)
        elif value == 0:
            kbd.release(keycode)

Op dit punt heb ik LapBoard getest met een Windows-computer en het werkte. Het laptoptoetsenbord werd via de twee RP2040-boards doorgestuurd en verscheen als een normaal USB-toetsenbord op de doelcomputer.

---

Spiraal 2.5 - Het toetsenbord exclusief overnemen

LapBoard werkte tot nu toe vrij goed, behalve dat het toetsenbord van mijn eigen laptop nog steeds actief was. Dit betekende dat als ik Alt+Tab indrukte, de applicatie op de doelcomputer versprong, maar dat ook gebeurde op mijn eigen laptop. Hetzelfde gold voor andere sneltoetsen.

De oplossing was eenvoudig: ik paste keyboard.py aan zodat het toetsenbord exclusief werd overgenomen met behulp van evdev. Daarnaast voegde ik Ctrl+Alt+Esc toe als ontsnappingssequentie om LapBoard te stoppen en de controle over het toetsenbord terug te geven aan de laptop.

from evdev import InputDevice, ecodes
import serial

kbd = InputDevice("/dev/input/event3")
ser = serial.Serial("/dev/ttyACM1", 115200, timeout=1)

print(kbd)
print("Sending to", ser.port)
print("Press Ctrl+Alt+Esc to stop LapBoard")

pressed = set()
kbd.grab()

try:
    for event in kbd.read_loop():
        if event.type != ecodes.EV_KEY:
            continue
        if event.value == 1:
            pressed.add(event.code)
        elif event.value == 0:
            pressed.discard(event.code)

        # Ctrl + Alt + Esc beëindigt LapBoard
        ctrl_pressed = (
            ecodes.KEY_LEFTCTRL in pressed or
            ecodes.KEY_RIGHTCTRL in pressed
        )
        alt_pressed = (
            ecodes.KEY_LEFTALT in pressed or
            ecodes.KEY_RIGHTALT in pressed
        )

        if (
            ctrl_pressed
            and alt_pressed
            and event.code == ecodes.KEY_ESC
            and event.value == 1
        ):
            print("\nCtrl+Alt+Esc detected. Stopping LapBoard.")
            break

        key = ecodes.KEY.get(event.code, str(event.code))
        message = f"{event.value},{key}\n"
        ser.write(message.encode())
finally:
    ser.write(b"0,KEY_LEFTCTRL\n")
    ser.write(b"0,KEY_RIGHTCTRL\n")
    ser.write(b"0,KEY_LEFTALT\n")
    ser.write(b"0,KEY_RIGHTALT\n")
    ser.write(b"0,KEY_LEFTSHIFT\n")
    ser.write(b"0,KEY_RIGHTSHIFT\n")
    kbd.ungrab()
    ser.close()
    print("Keyboard released.")

Er waren ook enkele leestekens die ik nog niet had toegewezen. Deze konden eenvoudig aan de KEYMAP op de HID RP2040 worden toegevoegd:

"KEY_DOT": Keycode.PERIOD,
"KEY_COMMA": Keycode.COMMA,
"KEY_SLASH": Keycode.FORWARD_SLASH,
"KEY_SEMICOLON": Keycode.SEMICOLON,
"KEY_APOSTROPHE": Keycode.QUOTE,
"KEY_MINUS": Keycode.MINUS,
"KEY_EQUAL": Keycode.EQUALS,
"KEY_LEFTBRACE": Keycode.LEFT_BRACKET,
"KEY_RIGHTBRACE": Keycode.RIGHT_BRACKET,
"KEY_BACKSLASH": Keycode.BACKSLASH,
"KEY_GRAVE": Keycode.GRAVE_ACCENT,

Op dit moment is LapBoard een functionele tool geworden die zeer handig is tijdens het reizen. Ik kan mijn laptop gebruiken als toetsenbord voor een andere computer of Raspberry Pi zonder dat ik een reserve-toetsenbord hoef mee te nemen of te zoeken.

---

Spiraal 3 - Touchpad ondersteuning en behuizing

Nog niet geïmplementeerd.

Spiraal 4 - Bluetooth of draadloze bridge

Nog niet geïmplementeerd.