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.
Groetjes,