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 examples
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.
Das 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: 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 file
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
In 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: 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. Ein keyb.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.