Raspberry Pi Pico: Mit CircuitPython Tastatur, Maus und Gamepad emulieren
Nachdem ich im ersten Artikel über den Raspberry Pi Pico die Hardware, die technischen Daten und das Pinout des Pico vorgestellt und mit der STM32 Bluepill, dem Arduino Nano und dem ESP8266 verglichen haben, blieb die Frage im Raum stehen, wie einfach sich der Pico programmieren lässt und ob er ein ernstzunehmender Konkurrent zu den etablierten Mikrocontrollern ist.Im zweiten Artikel ging es dann um die Installation des UF2-Files für MicroPython und die erste Programmierung darin über Putty, was sich als wenig komfortabel herausgestellt hatte.
In dritten Artikel habe ich die Entwicklungsumgebung Thonny für MicroPython auf dem Raspberry Pi Pico vorgestellt und außerdem ein paar Python-Programme geschrieben: Um die interne LED zum Blinken zu bringen, den internen Temperatursensor auszulesen und die Temperatur auf einem OLED-Display anzuzeigen. Auch der Paketmanager und die Suche nach Libraries wurde kurz erklärt. Auch wenn schlussendlich alles lief, ging mir die instabile serielle Verbindung auf die Nerven.
Im vierten Artikel ging es um eine weitere Entwicklungsumgebung: CircuitPython. Das ist ein Ableger von MicroPython, der von Adafruit weiterentwickelt wurde und gegenüber Thonny so einige Vorteile hat. Das Hochladen ist zum Beispiel komfortabler, auf der anderen Seite fehlt ein Paket-Manager.
Und CircuitPython bindet gleich ein paar USB-Geräte ein, darüber soll auch eine Emulation von HID-Devices (Human Interface Devices wie Tastaturen, Mäuse etc.) gehen. Das wollen wir heute ausprobieren.
Unser Ziel soll sein, ein USB-Gerät zu bauen, das eine Reihe von Tasten simulieren kann, das Mausrad per Jogdial (Drehgeber) bewegen und vielleicht noch angeschlossene, alte Retro-Joysticks mit 9 poligem SUB-D Joytickanschluss (digital wie Commodore C64) und 25 poligem SUB-D Gameport (digital/analog wie PC) auf USB emuliert. Denn ich habe noch eine ganze Reihe alter Joysticks wie den legendären Competition Pro oder den Gravis Joystick, die noch funktionieren sollten und beim Retro-Gaming das original Feeling aufkommen lassen sollten. Ob wir das auch noch unterkriegen, kommt natürlich auch ein wenig auf die vorhandenen freien Leitungen des Pico an und ob die ausreichen.
Zunächst wollen wir uns aber erst einmal einen simplen Prototypen basteln, der uns mit der USB-HID-Programmierung vertraut macht, mit dem wir das Thema testen können und auf dem wir aufbauen können.
Versuchsaufbau Hardware
Wir wollen Tastatur, Maus und Gamepad-Funktionalität erst einmal auf einem Breadboard testen. Also brauchen wir ein paar einfache Eingabegeräte: Einen Taster zur Simulation eines Tastendruckes, einen Taster zur Simulation einer Maustaste und dann noch einen Drehgeber, mit dem wir das Mausrad emulieren wollen, links herum gedreht soll er Texte rauf scrollen und rechts runter. Diese schließen wir wie folgt an; auch das OLED führe ich noch einmal auf. Von links nach rechts sind das:Bauteil, Bauteil-Pin Pico-Pin
Reset-Taster
Seite links RUN (Pin 30)
rechts GND
OLED
GND GND
VCC 3.3V
SCL GP17 (Pin 22, I2C0 SCL)
SDA GP16 (Pin 21, I2C0 SDA)
Taster 1 (Tastatur)
Seite links 3.3V
rechts GP22 (Pin 29)
Taster 2 (Maustaste)
Seite links 3.3V
rechts GP21 (Pin 27)
Drehgeber (Mausrad)
CLK (Clock) GP20 (Pin 26)
DT (Data) GP19 (Pin 25)
SW (Switch) GP18 (Pin 24)
+ 3.3V
GND GND
Aufgebaut sieht das dann so aus:Den Aufbau habe ich auch spaßeshalber nocheinmal als Wokwi-Projekt angelegt. Was dann wie folgt aussieht. Ist auch nur bedingt übersichtlicher als das Foto oben. Aber wenn ihr auf den Link klickt, könnt ihr das Projekt öffnen und mit der Maus über die Pins gehen und genau nachschauen, welche Verbindung wohin führt.
Normalerweise kann Wowik auch Schaltungen simulieren. Die Simulation von Reset-Taster und das Einbinden der Libraries für OLED und HID funktionierten leider in Wokwi nicht, so dass eine Simulation für dieses Projekt nicht funktionierte.
Source-Code
code.py (klicken, um diesen Abschnitt aufzuklappen)
# (C) 2022 by Oliver Kuhlemann
# Bei Verwendung freue ich mich um Namensnennung,
# Quellenangabe und Verlinkung
# Quelle: http://cool-web.de/raspberry/
import board
import digitalio
from time import sleep
import displayio
import adafruit_displayio_ssd1306
import terminalio
from adafruit_display_text import label
import busio
import microcontroller
import rotaryio
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode
from adafruit_hid.mouse import Mouse
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode
from hid_gamepad import Gamepad
led = digitalio.DigitalInOut(board.GP25)
led.direction = digitalio.Direction.OUTPUT
######## Keyboard, Mouse, GamePad
keyb = Keyboard(usb_hid.devices)
keyb_layout = KeyboardLayoutUS(keyb)
mouse = Mouse(usb_hid.devices)
gp = Gamepad(usb_hid.devices)
######## ggf. Sondertasten
#cc = ConsumerControl(usb_hid.devices)
######## Buttons
btn1 = digitalio.DigitalInOut(board.GP22)
btn2 = digitalio.DigitalInOut(board.GP21)
dg_btn = digitalio.DigitalInOut(board.GP18)
#dg_clk = digitalio.DigitalInOut(board.GP20)
#dg_data = digitalio.DigitalInOut(board.GP19)
btn1.direction = digitalio.Direction.INPUT
btn1.pull = digitalio.Pull.DOWN
btn2.direction = digitalio.Direction.INPUT
btn2.pull = digitalio.Pull.DOWN
dg_btn.direction = digitalio.Direction.INPUT
dg_btn.pull = digitalio.Pull.DOWN
######## Drehgeber
dg = rotaryio.IncrementalEncoder(board.GP20, board.GP19)
######## OLED
displayio.release_displays()
i2c = busio.I2C(scl=board.GP17, sda=board.GP16, frequency=400000)
display_bus = displayio.I2CDisplay(i2c, device_address=0x3C)
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64)
display.brightness=0.0 # 0.0..1.0
# Tastatur:
# keyb.press/release(Keycode.x); keyb_layout.write("string")
# Keycode.x: A..Z; ONE..ZERO; F1..F12; SHIFT, ALT, CONTROL, GUI (Win-Key); SPACE, TAB
# ermitteln: print (layout.keycodes('$'))
# Maus:
# mouse.move(wheel = 1[up] / -1[dw])
# m.move(x=-100, y=-100)
# m.press(Mouse.LEFT_BUTTON)
# m.move(x=50, y=20)
# m.release_all()
# Mouse.x: LEFT_BUTTON, RIGHT_BUTTON, MIDDLE_BUTTON;
# GamePad:
# gp.press_buttons(1)
# gp.move_joysticks(x = -127..127, y = -127,127)
# ConsumerControl:
# cc.send(ConsumerControlCode.BRIGHTNESS_INCREMENT); MUTE, etc.
dg_last_pos = None
while True:
# Button 1 simuliert die Taste "A"
if btn1.value:
keyb.press(Keycode.GUI, Keycode.SHIFT, Keycode.ALT, Keycode.CONTROL, Keycode.F1)
print ("btn 1")
while btn1.value: # warten auf loslassen
sleep (.01)
keyb.release(Keycode.GUI, Keycode.SHIFT, Keycode.ALT, Keycode.CONTROL, Keycode.F1)
# Button 2 simuliert die linke Maustaste
if btn2.value:
mouse.press(Mouse.LEFT_BUTTON)
print ("btn 2")
while btn2.value: # warten auf loslassen
sleep (.01)
mouse.release(Mouse.LEFT_BUTTON)
# Drehgeber-Button simuliert den Gamepad Button A
if dg_btn.value == 0:
print ("dg_btn")
led.value = 1
gp.press_buttons(1)
while dg_btn.value == 0: # warten auf loslassen
sleep (.01)
gp.release_buttons(1)
led.value = 0
# Drehgeber simuliert Mausrad
dg_pos=dg.position
if dg_last_pos is not None:
if dg_pos != dg_last_pos:
print ("dg:" , dg_last_pos, "->", dg_pos)
if dg_pos > dg_last_pos:
print ("wheel down")
mouse.move(wheel=-1)
if dg_pos < dg_last_pos:
print ("wheel up")
mouse.move(wheel=1)
dg_last_pos = dg_pos
sleep (.01)
Am Anfang des Source-Codes kommen erst einmal jede Menge Import-Statements. Die werden bei euch höchstwahrscheinlich zum jetzigen Zeitpunkt ein paar Fehlermeldungen produzieren. Zur Installation der entsprechenden Libraries aber später mehr. Gehen wir erstmal grob die Funktionalität durch, auch wenn eigentlich alles gut dokumentiert sein sollte.
Am Anfang definieren wir unsere virtuellen HID-Geräte keyb und keyb_layout für die Tastatur, mouse für die Maus und gb für das Gamepad, damit wir später darauf zugreifen können. Ausgeklammert habe ich erst einmal cc, das ConsumerControl-Gerät, mit dem man Sondertasten für Helligkeit +/-, LAutstärke +/-, Mutimedia Play/Pause etc. senden kann. Für diese Sondertasten können wir nicht keyb hernehmen, sondern brauchen ein eigenes Objekt.
Danach definieren wir die GPIO-Port für unsere Taster und den Drehgeber. Den Teil mit dem OLED kennen wir ja schon aus dem letzten Artikel. Dann folgen ein paar Kommentarzeilen, die komprimiert die Kommandos zur Steuerung der virtuellen HID-Geräte erklären.
Und dann kommt die große Endlosschleife, in der wir die Buttons und den Drehgeber immer wieder abfragen und etwas tun, falls ein Button gedrückt oder der Drehgeber gedreht wird: Der 1. Button (btn1) sendet die Tastenkombination Windows+STRG+ALT+SHIFT+F1. Warum so kompliziert werdet ihr euch fragen? Nun, zum einen als Beispiel für die Umschalttasten und zum anderen will ich 8 oder 12 F-Tasten einbauen, die dann F1 bis F12 eben mit allen Umschalttasten sendet. Die sind garantiert nirgends belegt, wer würde solche Fingerbrecher auch benutzen? Aber auf der anderen Seite, im Windows-System lasse ich das mächtige und kostenlose AutoHotkey laufen. Das ist eine Makrosprache, um Tastendrücke abzufangen und umzuwandlen, Mausklicks zu emulieren, Fenster anzuzeigen etc. pp.
Und mit zum Beispiel
#^+!F1::
msgbox "Pico-Taste F1"
return
kann ich dort auf diese Wahnsinns-Tastenkombination einfach reagieren und wie hier zum Beispiel ein Meldungs-Fenster anzeigen. Das kann ich auch noch abhängig vom laufenden Programm machen, hätte dann also für jedes Programm eigene 12 Funktionstasten, je nachdem, was gerade läuft. Im einen Programm, vielleicht einem Grafikprogramm, könnte Pico-F1 also für "um 90° nach rechts drehen" stehen und in einem anderen, einem HTML-Editor stünde Pico-F1 vielleicht für das Einfügen des Textblocks für eine HTML-Tabelle.Ob ich die jeweils gültigen Tastenkombinationen auf dem Windows-Arbeitsplatzmonitor anzeige, oder auf einem zusätzlich eingebautem Display am Pico muss ich noch überlegen. Im zweiten Fall müsste ich über die serielle Schnittstelle zum Pico jeweils die neue Funktionstastenbelegung schicken, damit der sein Display aktualisieren kann. Auch kein großes Problem. Auf der anderen Seite wird man die Tasten eh im Windows-System benutzen und in Richtung Windows-Monitor schauen... wie gesagt, ich überlege noch.
Der 2. Button (btn2) drückt die linke Maustaste herunter, und zwar solange, wie ich auch btn2 gedrückt halte. Ich könnte damit als auch Text markieren. Aber das soll ja nur ein Test sein, ob und wie die Maussteuerung funktioniert.
Der 3. Button, derjenige, der im Drehgeber eingebaut ist (dg_btn), soll den ersten GamePad-Button drücken, das dürfte (A) auf meinem XBOX-360-Controller sein.
Und der Drehgeber selbst soll das Mausrad simulieren und damit durch Text hoch- und runterscrollen. Für den Drehgeber gibt es bereits fertigen Code für CircuitPython, den wir uns mit import rotaryio bereits geladen haben. Das ist mit CircuitPython ein wenig einfacher als ich damals mein Multi Function Shield mit einem Drehgeber erweitert habe. Da musste ich das unter dem Arduino noch selbst programmieren.
Das war auch schon der ganze Zauber. Schauen wir uns mal einen Test in einem Video an:
Video Demonstration
Installation der Libraries
Außer den Libraries, die wir schon in der vorhergehenden Projekten installiert haben, brauchen wir jetzt noch die HID-Libraries. Im letzten Artikel hatte ich bereits erklärt, wie man die CircuitPython-Libraries herunterlädt und installiert.Die Library für das GamePad ist in Version 5.00 herausgeflogen, wie hier nachzulesen ist.
move gamepad.py to examplesDas heißt, dass wir das gamepad.py aus den examples heraussuchen müssen und das auch auf unser CircuitPy-Laufwerk kopieren müssen. Das sollte dann in etwa wie folgt "bestückt" sein:
dhalbert commented on 4 May 2021
In CircuitPython 7.0.0, there will no longer be a builtin Gamepad HID device. Instead, you will be able to supply your own HID descriptors for whatever kind of gamepad you want.
This removes gamepad.py from the library, and move it to examples/, with very slight changes. Once dynamic USB descriptors is in a 7.0.0 alpha or beta release, I will update the example to include details of creating an example gamepad usb_hid.Device, or else move it to a new library.
I looked in the Learn Guides, and there are none that use the current Gamepad functionality. So I think it's safe to remove the functionality from the current library. It will receive a major version increment. The current Gamepad only works on Windows. It may work slightly on MacOS, but it can interfere with the mouse. It does not work on Linux.
Removing Gamepad has the additional advantage of saving 2.6kB in frozen library space on those boards that freeze HID. This space will be very helpful in compensating for the additional firmware space needed for the new Circuitpyon 7.0.0 dynamic USB code.
Datenträger in Laufwerk J: ist CIRCUITPY
Volumeseriennummer: 5021-0000
Verzeichnis von J:\
01.01.2020 00:00 <DIR> .fseventsd
01.01.2020 00:00 0 .metadata_never_index
01.01.2020 00:00 0 .Trashes
01.01.2020 00:00 <DIR> lib
01.01.2020 00:00 121 boot_out.txt
08.01.2022 15:18 4.206 code.py
07.01.2022 17:10 1.661 boot.py
5 Datei(en), 5.988 Bytes
Verzeichnis von J:\.fseventsd
01.01.2020 00:00 <DIR> .
01.01.2020 00:00 <DIR> ..
01.01.2020 00:00 0 no_log
1 Datei(en), 0 Bytes
Verzeichnis von J:\lib
01.01.2020 00:00 <DIR> .
01.01.2020 00:00 <DIR> ..
05.01.2022 05:09 750 adafruit_displayio_ssd1306.mpy
05.01.2022 15:28 <DIR> adafruit_display_text
06.01.2022 19:43 <DIR> adafruit_hid
07.01.2022 17:17 5.334 hid_gamepad.py
2 Datei(en), 6.084 Bytes
Verzeichnis von J:\lib\adafruit_display_text
05.01.2022 15:28 <DIR> .
05.01.2022 15:28 <DIR> ..
05.01.2022 05:09 4.012 bitmap_label.mpy
05.01.2022 05:09 3.939 label.mpy
05.01.2022 05:09 4.721 __init__.mpy
3 Datei(en), 12.672 Bytes
Verzeichnis von J:\lib\adafruit_hid
06.01.2022 19:43 <DIR> .
06.01.2022 19:43 <DIR> ..
05.01.2022 05:09 659 consumer_control.mpy
05.01.2022 05:09 354 consumer_control_code.mpy
05.01.2022 05:09 1.190 keyboard.mpy
05.01.2022 05:09 1.223 keyboard_layout_base.mpy
05.01.2022 05:09 330 keyboard_layout_us.mpy
05.01.2022 05:09 1.764 keycode.mpy
05.01.2022 05:09 843 mouse.mpy
05.01.2022 05:09 389 __init__.mpy
8 Datei(en), 6.752 Bytes
Anzahl der angezeigten Dateien:
19 Datei(en), 31.496 Bytes
12 Verzeichnis(se), 989.696 Bytes frei
hid_gamepad.py kann man natürlich auch in den adafruit_hid-Unterordner in Lib kopieren, dann muss man das Import-Statement entsprechend anpassen.boot.py anpassen für GamePad
Damit sind wir aber noch nicht ganz fertig. Denn im gamepad.py finden wir dann folgende Anweisungen:4# You must add a gamepad HID device inside your boot.py fileIn Kürze, für alle, die sich nicht durch ellenlange Anweisungen für ein "Custom HID Device" durchquälen wollen, kopiert einfach folgenden Code in eure booty.py auf eurem CircuitPy-Laufwerk:
5# in order to use this example.
6# See this Learn Guide for details:
7# https://learn.adafruit.com/customizing-usb-devices-in-circuitpython/hid-devices#custom-hid-devices-3096614-9
import usb_hid
# This is only one example of a gamepad descriptor, and may not suit your needs.
GAMEPAD_REPORT_DESCRIPTOR = bytes((
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
0x09, 0x05, # Usage (Game Pad)
0xA1, 0x01, # Collection (Application)
0x85, 0x04, # Report ID (4)
0x05, 0x09, # Usage Page (Button)
0x19, 0x01, # Usage Minimum (Button 1)
0x29, 0x10, # Usage Maximum (Button 16)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x01, # Logical Maximum (1)
0x75, 0x01, # Report Size (1)
0x95, 0x10, # Report Count (16)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
0x15, 0x81, # Logical Minimum (-127)
0x25, 0x7F, # Logical Maximum (127)
0x09, 0x30, # Usage (X)
0x09, 0x31, # Usage (Y)
0x09, 0x32, # Usage (Z)
0x09, 0x35, # Usage (Rz)
0x75, 0x08, # Report Size (8)
0x95, 0x04, # Report Count (4)
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, # End Collection
))
gamepad = usb_hid.Device(
report_descriptor=GAMEPAD_REPORT_DESCRIPTOR,
usage_page=0x01, # Generic Desktop Control
usage=0x05, # Gamepad
report_ids=(4,), # Descriptor uses report ID 4.
in_report_lengths=(6,), # This gamepad sends 6 bytes in its report.
out_report_lengths=(0,), # It does not receive any reports.
)
usb_hid.enable(
(usb_hid.Device.KEYBOARD,
usb_hid.Device.MOUSE,
usb_hid.Device.CONSUMER_CONTROL,
gamepad)
)
Beim Booten von CircuitPython wird dann ein HID-GamePad-Device vom Pico für Windows zur Verfügung gestellt. Für Windows sieht das dann aus wie ein normaler Joystick / GamePad, dessen Eingaben durch Windows ausgewertet werden.Testen der Schaltung / der Eingabegeräte
Für das Tastatur-Device gibt es übrigens bisher nur das "KeyboardLayoutUS", ein deutsches Layout gibt es nicht. Wenn man in Windows aber eine deutsche Tastatur eingestellt hat und dann ein ";" sendet, dann wird ein "ö" bei Windows herauskommen. Und zwar deshalb, weil normalerweise auf US-amerikanischen Tastatur das ";" dort liegt, wo bei den deutschen das "ö" liegt. Es ist so, als ob man eine US-Tastatur angeschlossen hätte, aber einen deutschen Tastaturtreiber benutzt. Einkeyb.press(Keycode.SEMICOLON)
keyb.release(Keycode.SEMICOLON)
ergibt also ein "ö".Übrigens: will man mehrere Tastendrücke gleichzeitig möglich machen, muss man im Soruce-Code natürlich erst alle keyb.press() für alle mögliche Tasten abfragen und setzen und erst dann zum Schluss alle keyb.release() für alle mögliche Tasten durchführen.
Der weitere Test zeigt: Auch die emulierte Maustaste und das Mausrad funktionieren wie sie sollen. Beweis im Video oben.
GamePad-Test
Das GamePad einzubinden war komplizierter. Das benötigt darum einen besonders gründlichen Test. Wie geben "controller" im Windows-Suchfenster (erscheint beim Druck auf die Windows-Taste) ein und wählen "USB-Gamecontroller einrichten" aus. Dann bekommen wir folgenden Dialog:Wunderbar. Da ist also schon einmal das angemeldete HID-Gerät. Nun muss nur noch der Feuerbutton funktionieren. Den hatte wir ja auf den Drehgeber-Button gelegt und wollen damit GamePad-Button 1 simulieren. Wir drücken also auf den Drehgeber und siehe da:
Funktioniert. Wunderbar.
Weitere Aussichten
Jetzt haben wir getestet, ob alles was wir möchten mit dem Pico machbar ist. Und ja, ist es: Tastatur, Maus, GamePad: Es lässt sich alles emulieren. Nun muss ich mir einen Plan machen, wieviele Tasten ich emulieren will, was von der Maus, was an Joysticks. Es sind zwar viele GPIO-Pins auf dem Pico vorhanden, aber auch nicht unendlich. Ich werde 74HC165-PISO-Schieberegister benutzen müssen, um Leitungen einzusparen. Mal schauen, wieviele Tasten etc. ich damit abfragen und unterbringen kann.Und dann hatte ich noch so etwas wie "Rapid Fire" und "Auto Fire" für Joystick und Maustasten im Sinn... aber eins nach dem anderen.
Ich werde hier schreiben, wenn es einen neuen Artikel gibt, der meinen Fortschritt mit diesem Projekt zeigt.
Das Projekt wird fortgesetzt im Artikel Raspberry Pi Pico: Fertiger Prototyp des Tastatur/Maus/GamePad-Emulators mit CircuitPython. Da baue ich ein Multifunktions-Eingabegerät.