Calliope mini Pinball Controller

In diesem Projekt sind Anleitungen und Code um einen Pinball Controller mit einem Calliope mini V3 zu bauen, welcher mit Pinball Videospielen wie zum Beispiel Pro Pinball: Timeshock! oder dem Visual Pinball Simulator benutzt werden kann. Dieser Pinball Controller besitzt 2 Flipper Knöpfe, einen Start Knopf, 1 Digitalen Flipper Kolben (zum Schießen der Kugel) und löst beim Schütteln über das Accelerometer "Stoß-Events" (TILT) aus. Der Pinball Controller versendet serielle Daten über die UART-Schnittstelle an den PC, welcher diese über ein Python Skript in Tastaturanschläge übersetzt, die dann die Komponenten des virtuellen Flippers betätigen :)
Demovideo
Hardware Aufbau

genutztes Material für diesen Aufbau
1 x Holz Schachtel etwa 20 x 8.5 x 75 cm
2 x beleuchteter Arcade Taster (groß) Ø 44mm
1 x Arcade Taster (klein)
1 x Taster (e.g. 12x12mm)
2 x lange M4 Schrauben
1 x M4 Innengewindestange
1 x Feder für M4 Schraube
1 x Calliope mini V3
1 x Servobard Calliope mini V3 (nicht unbedingt notwendig, macht die Verkabelung aber einfacher)
8 x Crimp-Kontakte
12 x Jumper-Kabel
1 x USB-C-Kabel
1 x PC
1 x Bildschirm (welcher horizontal gestellt werden kann, damit man einen großen Flipper hat)






Benutztes Werkzeug
1 x Akkuschrauber
1 x Heißkleber
1 x 5mm Holzbohrer
1 x 25mm Holzbohrer z.B. Forstner Bohrer




Bau Anleitung
- Bohre Löcher für die Arcade-Taster in die Box. Sei vorsichtig mit dem Loch oben für den roten Knopf, da der Schachteldeckel sehr dünn ist, lege auf der anderen Seite stärkeres Holz unter.
- Stecke das Servoboard auf den Calliope mini, dieses bietet genügend GND Pins für alle Schalter
- Bereite Kabel in passender Länge vor, löte Krimp Kontakte an Buchsen-Jumper-Drähte.
- Setze Taster ein und schließe Kabel testweise an
- Bohre Loch für das USB-Kabel
- Finde die Stelle, an der die Schraube für den Kolben eingesetzt werden kann. Bohre ein Loch für die Schraube und baue eine kleine Führung aus Holz (siehe Bild des Hardware Aufbau in der rechten oberen Ecke). Achte darauf, dass die Holzführung nicht den großen Arcade Taster behindert.
- Klebe Taster mit Heißkleber an passender Stelle fest
- Befestige Führung mit Heißkleber oder einer kleinen Holzschraube
- Füge Schraube, Feder und Innengewinde ein und schraube sie zusammen, um den Flipper Kolben zusammenzubauen
- Setze alle Taster ein
- Lege den Calliope mini an die passende Stelle, wie auf dem Bild "Hardware Aufbau" gezeigt
- Schließe die Kabel gemäß der Tabelle unten an
Anschluss der Taster
Taster Pin | Calliope mini Pin | Tastaturanschlag |
---|---|---|
Arcade Knopf 1 Pin 1 | GND | Links Shift |
Arcade Knopf 1 Pin 2 | C8 | Links Shift |
Arcade Knopf 1 LED Pin 1 | M0+ | - |
Arcade Knopf 1 LED Pin 2 | M0- | - |
Arcade Knopf 2 Pin 1 | GND | Rechts Shift |
Arcade Knopf 2 Pin 2 | C9 | Rechts Shift |
Arcade Knopf 2 LED Pin 1 | M1+ | - |
Arcade Knopf 2 LED Pin 2 | M1- | - |
Arcade Knopf(klein) Pin 1 | GND | Taste "s" |
Arcade Knopf(klein) Pin 2 | C13 | Taste "s" |
Taster Pin 1 | GND | Enter |
Taster Pin 2 | C12 | Enter |
- | Beschleunigung Y+ | Links Alt |
- | Beschleunigung Y- | Rechts Alt |
- | Beschleunigung Z+ | Leertaste |
Code
Dieser Aufbau erfordert zum einen Code, der auf dem Calliope mini läuft, um serielle Daten entsprechend dem Tastendruck und den erkannten Gesten zu senden. Zum anderen läuft Code auf dem Host-Computer, welcher die seriellen Daten empfängt und entsprechende Tastaturanschläge auslöst, welche dann im Pinball Videospiel die verschiedenen Komponenten betätigen. Beide wurden in Python implementiert. Auf dem Calliope mini läuft Micropython, welches mit dem [Python Editor] (https://python.calliope.cc/) geflasht werden kann, auf dem Host-Computer läuft ein Python 3.8-Skript, für das die folgenden Abhängigkeiten installiert sein müssen. Der Code für den Hostcomputer wurde auf einem Windows und einem MacOS (Apple Silicon) System getestet.
Python Abhängigkeiten
- pynput==1.8.1
- pyserial==3.5
Installiere diese beiden mit pip für Python, benutze gegebenenfalls eine Virtuelle Umgebung. Mit dem Befehl
pip install -r requirements.txt
oder
pip install pynput pyserial
werden die Abhängigkeiten installiert.
Erklärung des Codes
Der erste Ansatz für den Code dieser Erweiterung basierte auf der Bluetooth-HID-Bibliothek für den Calliope mini, die ohne ein zusätzliches Skript auf dem Host-Computer funktioniert hätte. Leider war die Latenzzeit dieses Ansatzes zu groß für ein spaßbringendes Gameplay.
Innerhalb der Code Dateien gibt es Kommentare zur Erklärung, für ein erstes Verständnis erkläre ich hier grob den Ablauf.
Calliope mini Code
Der Quellcode des Python Programms für den Calliope mini befindet sich in der Datei mini-buttons2serial.py. In diesem Programm wird alle 10ms eine while-Schleife ausgeführt, die den aktuellen Status der Ereignisse per UART an den Host-Computer sendet. Alle Taster sind mit internen Pull-Up-Widerständen verbunden und werden bei Tastendruck auf GND gezogen, daher werden die Signale oft invertiert. Der Kolbenmechanismus hört auf Änderungen des Tastenzustands, wenn zwei Änderungen auftreten, wird das dazugehörige Ereignis ausgelöst. Für die Stoß-Erkennung (TILT) werden die Accelerometer Werte abgefragt und bei Überschreiten eines bestimmten Grenzwertes entsprechende Ereignisse ausgelöst.
Bei vielen Ereignisse wird eine Verzögerung eingesetzt, damit sie nicht mehrmals, sondern nur einmal über einen längeren Zeitraum ausgelöst werden.
Host Computer Code
Der Quellcode des Python-Programms des PCs befindet sich in der Datei serial2keyboard.py. Dieses Skript wartet auf neue Eingaben an der seriellen Schnittstelle, prüft die Integrität der seriellen Daten und löst Tastendrücke aus, wenn Ereignisse empfangen werden. Die entsprechende Taste wird gedrückt, wenn das Ereignis zum ersten Mal empfangen wird, und losgelassen, sobald es nicht mehr empfangen wird. Eine kleine Verzögerung am Ende des Codes sorgt dafür, dass die CPU nicht blockiert wird.
Wichtig: In Zeile 5 des Codes für den Hostcomputer muss der Serial Port des angesteckten Calliope mini eingetragen werden, dieser lässt sich über ein Serial Terminal Tool wie hterm oder über den Geräte-Manager herausfinden.
Code
Der gesamte Code befindet sich in einem Github Repository und ist außerdem unten aufgeführt:
Calliope mini Code:
# Imports go at the top
from calliopemini import *
# Prepare pins to read digital
pin8.set_pull(pin8.PULL_UP)
pin9.set_pull(pin8.PULL_UP)
pin12.set_pull(pin8.PULL_UP)
pin13.set_pull(pin8.PULL_UP)
# Light up arcade buttons
pin_M_MODE.write_digital(1)
pin_M1_DIR.write_digital(1) # Direction depends on wiring
pin_M1_SPEED.write_digital(1)
pin_M0_DIR.write_digital(0) # Direction depends on wiring
pin_M0_SPEED.write_digital(1)
# Variables for plunger / shooter
plunger_button, plunger_button_previous = 0, 0
plunger_ready, shoot = 0, 0
# Variables for bump detection
acc_x, acc_y, acc_z = 0, 0, 0
bump_left, bump_right, bump_up = 0, 0, 0
bump_detected = 0
# Variable for player button
player_button = 0
# Code in a 'while True:' loop repeats forever
while True:
plunger_button = int(pin12.read_digital())
if plunger_button != plunger_button_previous: # If there was a change in the plunger button
if not plunger_ready:
plunger_ready = 1 # Enable plunger to shoot when there is the next change in the plunger button
elif plunger_ready:
plunger_ready = 0
shoot = 50 # This variable is decremented by time and keeps the button pressed for 500ms
if not pin13.read_digital():
player_button = 20 # This variable is decremented by time and keeps the button pressed for 200ms
if shoot:
shoot -= 1
if player_button:
player_button -= 1
if bump_detected:
bump_detected -= 1
if bump_detected == 1: # Resets all bump values when bump
bump_left = 0
bump_right = 0
bump_up = 0
acc_x, acc_y, acc_z = accelerometer.get_values()
if acc_x > 1200 and not bump_detected and not shoot:
bump_up = 1
bump_detected = 20 # Space bar creates tilt faster
if acc_y > 700 and not bump_detected and not shoot:
bump_left = 1
bump_detected = 50
if acc_y < -700 and not bump_detected and not shoot:
bump_right = 1
bump_detected = 50
print(
int(not pin8.read_digital()), # Left arcade button (Left shift)
int(not pin9.read_digital()), # Right arcade button (Right shift)
int(bool(player_button)), # Player Button ('s')
int(bool(shoot)), # Activate Plunger (Enter)
int(bump_left), # Bump Table to left (Left Alt)
int(bump_right), # Bump Table to right (Right Alt)
int(bump_up) # Bump Table up (Spacebar)
)
plunger_button_previous = plunger_button
sleep(10)
Host Computer Code
import serial
from pynput.keyboard import Controller, Key
import time
# Set up the serial port (adjust your COM port accordingly)
serial_port = 'COM5' # Adjust for your operating system (e.g., COMx on Windows)
baud_rate = 115200 # Baud rate (adjust as needed)
# Initialize serial connection
ser = serial.Serial(serial_port, baud_rate, timeout=1)
ser.flushInput()
# Initialize the keyboard controller to simulate key presses
keyboard = Controller()
key_state = {'a': False, 'b': False, 'c': False, 'd': False, 'e': False, 'f': False, 'g': False}
# For momentary press tracking
signal2_previous = 0
signal3_previous = 0
# Main loop
while True:
if ser.in_waiting > 0: # Check if data is available on the serial port
data = ser.readline().decode('utf-8').strip() # Read the input from serial
# Expecting a string like "0 0 0 0 0 0 0"
signals_string = data.split(' ')
# Only proceed if we get exactly 7 values
if len(signals_string) == 7:
try:
# Convert the list of strings to integers (with added check to avoid empty strings)
signals = [int(signal) if signal != '' else 0 for signal in signals_string]
# Regular press and hold behavior for signals 0, 1, 4, 5
if signals[0] and not key_state['a']: # If 'a' should be pressed
keyboard.press(Key.shift_l) # Left Shift
key_state['a'] = True
elif not signals[0] and key_state['a']: # If 'a' should be released
keyboard.release(Key.shift_l)
key_state['a'] = False
if signals[1] and not key_state['b']: # If 'b' should be pressed
keyboard.press(Key.shift_r)
key_state['b'] = True
elif not signals[1] and key_state['b']: # If 'b' should be released
keyboard.release(Key.shift_r)
key_state['b'] = False
if signals[2] and not key_state['c']: # If 'b' should be pressed
keyboard.press('s')
key_state['c'] = True
elif not signals[2] and key_state['c']: # If 'b' should be released
keyboard.release('s')
key_state['c'] = False
if signals[3] and not key_state['d']: # If 'b' should be pressed
keyboard.press(Key.enter)
key_state['d'] = True
elif not signals[3] and key_state['d']: # If 'b' should be released
keyboard.release(Key.enter)
key_state['d'] = False
if signals[4] and not key_state['e']: # If 'e' should be pressed
keyboard.press(Key.alt_r)
key_state['e'] = True
elif not signals[4] and key_state['e']: # If 'e' should be released
keyboard.release(Key.alt_r)
key_state['e'] = False
if signals[5] and not key_state['f']: # If 'f' should be pressed
keyboard.press(Key.alt_l)
key_state['f'] = True
elif not signals[5] and key_state['f']: # If 'f' should be released
keyboard.release(Key.alt_l)
key_state['f'] = False
# Added new signal for space bar
if signals[6] and not key_state['g']: # If 'g' should be pressed
keyboard.press(Key.space)
key_state['g'] = True
elif not signals[6] and key_state['g']: # If 'g' should be released
keyboard.release(Key.space)
key_state['g'] = False
except ValueError:
print(f"Error converting data: {data}")
else:
print(f"Received unexpected number of values: {data}")
time.sleep(0.0001) # Small delay to avoid high CPU usage