Sie sind klein, sehr hell und in verschiedenen Anordnungen verfügbar. Darüber hinaus sind mehrere davon sehr leicht über nur eine Leitung ansteuerbar, auch was die Leuchtstärke betrifft und das in 16,7 Millionen Farben. Die Rede ist von Neopixel-LEDs. Neopixel-Ringe habe ich schon mehrfach in interessanten Schaltungen eingesetzt zum Beispiel beim Advents-Kranz-Kalender, beim Fahrrad-Bremslicht, beim Kompass, der Tischuhr mit Touchbedienung und bei den Spielen Bandit und Masterring. Das 16x16-Neopixel-Array hatte ich für ein Weihnachtsspecial gebraucht und zuletzt mit den Neopixel-Streifen eine lineare Uhr mit Abakus-Codierung gebaut. Wie Sie sehen, sind die Teile vielfältig einsetzbar.
Jetzt habe ich mit einem LED-Streifen mal wieder ein Spiel aufgebaut und in MicroPython programmiert. Es heißt RouLED und ist einem Roulette-Kessel nachgebildet. Welche Gedanken bei der Entwicklung eine wichtige Rolle spielten, welche Controller getestet wurden und wie die Play-Ware aussieht, das erfahren Sie jetzt in der Reihe
MicroPython auf dem ESP32 und Raspberry Pi Pico
heute
RouLED – Dem Glück auf der Spur
Dazu verwenden wir folgende Teile.
Hardware
1 |
ESP32 Dev Kit C unverlötet oder ESP32 Dev Kit C V4 unverlötet oder ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder |
oder 1 |
D1 Mini NodeMcu with ESP8266-12F WLAN module compatible with Arduino Oder NodeMCU Lua Amica Modul V2 ESP8266 ESP-12F WIFI Wifi Development Board mit CP2102 |
oder 1 |
|
1 |
|
1 |
Taste zum Beispiel KY-004 Taster Modul |
1 |
Optional für Raspberry Pi Pico und D1 Mini NodeMcu zum Programmabbruch: Taste zum Beispiel KY-004 Taster Modul |
1 |
|
1 |
|
2 |
Dreipolige Stiftleiste1 |
Optional |
|
Optional |
|
1 |
5V-Stecker-Netzteil oder |
Wir werden das finale Programm so bauen, dass es für alle drei Controllertypen ohne Änderung laufen wird. Doch bereits bei der Hardware weisen der D1 Mini NodeMcu und der Raspberry Pi Pico einen wesentlichen Unterschied auf, beide haben nämlich keine Flashtaste. Genau die benutze ich aber bei der Entwicklung gerne, um einen genau gesteuerten Programmabbruch zu erreichen. Wenn Sie dieses Feature nutzen möchten, brauchen Sie daher eine optionale Taste, falls Sie eines der beiden Entwicklungsboards verwenden wollen. Das ESP32-Board bietet für diesen Zweck die Flashtaste an GPIO0.
Von dem 1m-LED-Streifen mit 30 LEDs pro Meter brauchen wir 25 Stück für den Roulette-Kreis und eine für die Status Anzeige. Außerdem löten wir die störrischen Zuleitungen (rot, grün, weiß) am Streifeneingang ab und ersetzen die Kabel durch flexible, dünnere.

Abbildung 1: Anschluss des Streifens
Der Streifenabschnitt mit den 25 LED-Segmenten hat eine Länge von 100 cm / 30 x 25 = 83,3 cm. Als Träger des Streifens habe ich eine quadratische Spanplatte von 9 mm Stärke und 30cm Kantenlänge gewählt. Daraus wird ein Kreis von 83,3 cm / (2 x 3,1415) = 26,5 cm Durchmesser ausgeschnitten, damit der Streifen genau hineinpasst. Das Ende des Streifens wird mit Klebefilm isoliert. An das Ende des Zuleitungskabels wird eine dreipolige Stiftleiste gelötet.

Abbildung 2: Rahmenmaße 300mm x 300mm
Zur Abdeckung des Rahmens habe ich mir auf zwei DIN A 4- Blättern eine Rosette mit den Zahlen von 0 bis 24 ausgedruckt, die zusammengeklebt und auf dem Rahmen befestigt werden (Teil 1, Teil 2). Ich habe mich übrigens für die Reduktion der ursprünglichen Zahlen von 0 bis 36, wie sie beim Original-Roulette zu finden sind, auf 0 bis 24 entschlossen, weil ich dabei mit einem einzigen LED-Streifen von 1m Länge auskomme und das Ganze nicht so groß wird (26,5 cm statt knappe 40 cm Kreisdurchmesser!)
An das Streifensegment mit der einzelnen LED habe ich ebenfalls eine dreipolige Stiftleiste gelötet. Zu sehen in Abbildung 3 links unter dem ESP32. Auf den zusammengesteckten Breadboards sind alle drei Controller, mit denen ich die Schaltung getestet habe, friedlich vereint. Der Diensthabende ist gerade der ESP8266-Amica rechts unten.

Abbildung 3: Testaufbau mit den drei Controllern
Zu einem Roulette gehört natürlich auch ein Spieltisch. Hier ist eine PDF-Vorlage, bidddeschööön (Teil 1, Teil 2).

Abbildung 4: Spieltisch
Die Schaltung ist nicht sehr anspruchsvoll und für die ESP32- ESP8266- und Raspberry Pi Pico- Familien lauffähig. Wenn der Aufbau auch ohne PC-Anschluss laufen soll, brauchen wir natürlich noch eine Spannungsquelle von 5V für den Betrieb. Das kann ein Steckernetzteil sein oder zum Beispiel ein Batteriehalter mit Lithium-Akku vom Typ 18650.
Die Software
Fürs Flashen und die Programmierung des ESP32:
Thonny oder
Zum Darstellen von Bussignalen
SALEAE – Logic-Analyzer-Software (64 Bit) für Windows 8, 10, 11
Verwendete Firmware für den ESP32:
Verwendete Firmware für den Raspberry Pi Pico (W):
Die MicroPython-Programme zum Projekt:
rouled.py: Eine erste Annäherung an das Projekt
rouled2.py: Das Programm für den ESP32
rouled3.py: Die universelle Lösung
timeout.py: Softwaretimer-Modul
MicroPython - Sprache - Module und Programme
Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 25.01.2024) auf den ESP-Chip gebrannt wird. Wie Sie den Raspberry Pi Pico einsatzbereit kriegen, finden Sie hier.
MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.
Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.
Autostart
Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter main.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.
Programme testen
Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.
Zwischendurch doch mal wieder Arduino-IDE?
Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.
Der Neopixelstreifen
Neopixel-LEDs vom Typ WS2812 enthalten drei einzelne Pixel, die rotes, grünes oder blaues Licht abgeben. Angesprochen werden sie von einem Controller, der seine Anweisungen über eine Art Bussystem erhält, das mit 800kHz getaktet wird.
Beim I2C-Bus oder beim SPI-Bus erreichen die Signale vom Controller, zum Beispiel einem ESP32, alle Slaves am Bus in gleicher Weise, alle sehen alles. Bei den WS2812-Bausteinen ist das anders. Jeder Baustein hat einen Dateneingang und einen Datenausgang. Mehrere Bausteine können kaskadiert werden, indem man den Dateneingang jedes weiteren Bausteins an den Datenausgang des Vorgängers anschließt. Der erste Baustein in der Kette bekommt vom ESP32 eine Pulskette von drei Bytes für R, G und B zugespielt, die er selbst futtert, also vom gesamten Pulszug entfernt. Alle nachfolgenden Impulse werden durchgewunken und am Datenausgang abgegeben. Jeder Baustein in der Kette holt sich auf dieselbe Weise seinen Anteil und gibt den Rest weiter. So wird es möglich, dass jeder Baustein in der Kette zeitversetzt individuell angesteuert werden kann. Die Intensität jeder Farbe kann in 256 Stufen variiert werden, je nach dem Wert des empfangenen Bytes für die einzelnen Farben. Wir geben den Farbcode in Form eines Tupels für jeden WS2812 als Element einer Liste an. Zuerst werden die Klassen Pin und NeoPixel importiert. Ich erzeuge ein Pin-Objekt als Ausgang und instanziiere damit ein Neopixel-Objekt mit 25 Bausteinen. Die NeoPixel-Instanz neo enthält ein Bytearray buf und die Methode write(), mit welcher der Inhalt des Bytearrays zum Neopixelring übertragen wird.
>>> from machine import Pin
>>> from neopixel import NeoPixel
>>> np=Pin(14,Pin.OUT) # D5
>>> neo=NeoPixel(np,25)
>>> neo[0]=(0xe0,0x07,0x3c)
>>> neo[1]=(0xf0,0xf0,0xf0)
>>> neo.write()
>>> neo.buf
bytearray(b'\x07\xe0<\xf0\xf0\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
Mit neo[0] spreche ich die ersten drei Elemente des Arrays an und übergebe die Werte für rot, grün und blau, 0xe0, 0x07 und 0x3c. Intern macht das die private Funktion __setitem__(). In den Buffer werden die Werte in veränderter Reihenfolge eingetragen. Wie wir gleich am Kurvenzug sehen werden, den ich mit Hilfe des Logic Analyzers aufgezeichnet habe. Die Werte werden so gesendet, wie sie im Puffer stehen. Einer 0 entspricht ein schmaler Impuls von ca. 250ns Breite gefolgt von einer Pause von ca. 1000ns. Die 1 wird durch einen Puls von 750ns und einer Pause von 500ns gesendet.

Abbildung 12: Pulsfolge für RGB = 0xE0, 0x07, 0x3C
Am Ausgang der ersten WS2812 fehlen die drei Bytes 0xe0, 0x07 und 0x3c, die dieser Baustein gefressen hat. Stattdessen kommt der Code 0xff, 0xff, 0xff heraus, der Code für den zweiten Baustein. Der Eingang für Kanal 1 des Logic Analyzers war für diese Messung am Eingang der ersten LED, Kanal 2 am Ausgang derselben angeschlossen.
Jetzt wissen wir wie das Känguru beim WS2812 läuft und können uns dem Programm für das Roulette-Projekt zuwenden.
Das RouLED-Projekt
Einige Fragen haben mich vor dem Beginn der Programmentwicklung zum RouLED-Projekt beschäftigt. Mit welcher Frequenz ist es möglich, eine bestimmte LED in dem Streifen von 25 Neopixel-LEDs anzusteuern? Und wie kann man das auslaufende Verhalten der Kugel in einem echten Roulette-Pool durch ein Programm nachempfinden? Gibt es Unterschiede im Verhalten der MicroPython-Dialekte bei den angepeilten Controllern ESP32, ESP8266 und Raspberry Pi Pico? Neben den bereits behandelten Hardwareproblemen habe ich tatsächlich auch Unterschiede in der MicroPython-Firmware gefunden. Dazu kommen wir später, kümmern wir uns doch erst einmal um ein funktionierendes Programm für den ESP32, der die besten Voraussetzungen dafür mitbringt.
Die LED-Folge-Frequenz und das erste Programm
Wie lange dauert der Pulse Train (aka Impulsfolge) für die 25 LEDs des Streifens? Nun, die Frequenz auf dem NeoPixel-Bus beträgt, wie oben schon gesagt, f = 800kHz. Die Übertragung eines Bits passiert also in t = 1 / f = 1 / 800000kHz = 1,25µs. Pro Pixel müssen wir 3 • 8 Bits = 24 Bits übertragen. Und für 25 Pixel brauchen wir dafür t = 1,25 µs • 24 • 25 = 750µs, mindestens, von systembedingten Eventualfällen abgesehen. Die Übertragung von Zuständen für die 25 Neopixel-LEDs kann also nicht schneller als mit ca. 1ms Folgezeit stattfinden. Das ist einmal der erste Grenzwert.
Dann wissen wir, dass eine Kugel, die in einer runden Schale tangential eingeworfen wird, sich eine Zeit lang mit relativ hoher Geschwindigkeit über die Innenfläche der Schale bewegt, um sich dann irgendwann langsamer werdend dem unteren Bereich zu nähern. Beim Roulette stößt die Kugel dann an Hindernisse, die den Lauf der Kugel weiter stören. Sie beginnt zu springen und verheddert sich schließlich in einem der nummerierten Felder, wo sie endgültig zu liegen kommt. Dieses Verhalten muss jetzt nachgebildet werden.
Die Energiedissipation (Verlust an Bewegungsenergie) durch Reibung soll in unserer Modellierung exponentiell erfolgen. Das heißt nichts anderes, als dass die Zeitdauer, welche die Kugel zum Erreichen des nächsten Zahlensegments in der Rotationsschale benötigt, immer mehr anwächst, je länger die „Kugel“ läuft. Die Zeitdifferenz für das Erreichen der nächsten Winkelposition folgt also einer Exponentialfunktion. Wir werden versuchen, den Lauf der Kugel vom Start bis zum Einrasten auf die Zielposition so realitätsnah wie möglich zu gestalten. Die Zeitdauer, die sich die Kugel in einem Winkelsegment aufhält ist am Anfang gering (hohe Geschwindigkeit) und wächst nur langsam an. Erst gegen das Ende zu wird die Aufenthaltsdauer der Kugel je Segment rasch größer. Sie kommt zur Ruhe, wenn ein vorgegebenes Limit der Zeit überschritten wird. Die Zielposition muss natürlich in irgendeiner Form der "Zufall" festlegen.
Das erste Programm rouled.py entstand nach diesen Vorgaben. Schauen wir es uns an. Es läuft auf dem ESP32.
from machine import Pin
from neopixel import NeoPixel
from time import sleep_ms
from sys import exit
import random
Pin und NeoPixel brauchen wir für LED-Streifen und Abbruchtaste. Mit sleep_ms legen wir die Verweildauer fest. Die Funktion exit() brauchen wir für den gezielten Programmabbruch und das Modul random liefert die Methode randint() zur Erzeugung von "Zufallszahlen" in vorgegebenen Bereichen.
An GPIO14 schließen wir die Steuerleitung des LED-Streifens mit den 25 Neopixel-LEDs an. Der Konstruktor erzeugt das gewünschte Neopixel-Objekt n.
np=Pin(14, Pin.OUT)
nbrOfLeds=25
n=NeoPixel(np, nbrOfLeds)
Als Abbruchtaste nehmen wir die Flash-Taste des Boards an GPIO0.
taste=Pin(0,Pin.IN,Pin.PULL_UP)
Die Funktion ledsOff() schaltet nbrOfLeds LEDs dunkel, indem wir deren Puffer mit dem RGB-Tupel (0,0,0) beschreiben und im Anschluss den Pufferinhalt mit write() an den Streifen schicken.
def ledsOff():
for i in range(nbrOfLeds):
n[i]=(0,0,0)
n.write()
Wir definieren einige Variablen, die wir in der Hauptschleife gleich wiederfinden werden. old und new sind die Indizes in das Array der Farbwerte der Neopixel-LEDs. Mit delay geben wir die Verzögerungszeit zum Segmentwechsel an. offset und limit werden mit ganzen Zahlen aus den Bereichen 4 bis 9 beziehungsweise 100 bis 4999 per Zufallsgenerator belegt. Mit even und odd legen wir die Farb-Tupel für gerade und ungerade Indizes in die Liste der LEDs fest. faktor ist der Wert, der die Dynamik der Steigerung der Verzögerungszeit festlegt. Nahe 1 bekommen wir einen Anstieg in kleinen Stufen, während die Kurve ab 1,2 einen erst flachen und gegen Ende einen sehr steilen Anstieg zeigt. Entsprechend der sich ergebenden Top-Werte müssen wir den oberen Wert des Bereichs für das Limit wählen. Für Evaluierungszwecke lassen wir uns limit und offset in REPL ausgeben. Wir löschen alle LEDs.
old=0
new=0
delay=3
offset=random.randint(4,10)
limit=random.randint(100,5000)
odd=(0x40,0,0)
even=(0,0,0x40)
faktor=1.2
print(limit, offset)
ledsOff()

Abbildung 5: faktor = 1,03

Abbildung 6: faktor=1,2
Dann geht es in die Main Loop, die Hauptschleife. Zuerst machen wir die LED mit der Nummer old aus und die LED new entweder in Rot an, wenn der LED-Index ungerade ist, oder in Blau bei geradzahligem Index. Rot und Blau wechseln sich also in der Folge ab. Einen Sonderstatus hat die LED mit der Nummer 0, dort leuchtet die LED in Weiß – aber erst, wenn nachfolgend der write()-Befehl abgeschickt wird.
while 1:
n[old]=(0,0,0)
n[new]=odd if new % 2 else even
if new == 0:
n[new]=(0x30,0x30,0x30)
n.write()
Jetzt merken wir uns die neue Position in old und erhöhen die Position in new. Der Teilungsrest modulo 25 sorgt dafür, dass die Zählung innerhalb des Rings von 0 bis 25 bleibt. Eine Verzögerung um delay Millisekunden schließt sich an.
old=new
new +=1
new %= 25
sleep_ms(delay)
Wir berechnen das neue offset, die Steigerung der Verzögerungszeit und addieren den Wert zum bisherigen Stand. Die Methode sleep_ms() nimmt nur Ganzzahlen, daher schneiden wir mit int() den Bruchanteil ab.
offset=offset*faktor
delay = int(delay+offset)
Sobald delay das Limit erreicht oder überschritten hat, brechen wir die Schleife ab. Das passiert auch, wenn die Flash-Taste gedrückt wird.
if delay >= limit:
break
if taste() == 0:
ledsOff()
exit()
Wie es besser funktioniert
Das war schon mal ein Anfang, aber auch nicht viel mehr. Je nach den eingestellten Werten für offset und limit läuft die Kugel zu kurz oder viel zu lang und dann ist der Moment des Stillstands schwierig einzuschätzen. Also feilen wir noch ein wenig an der Performance und an der Architektur des Programms. Das wird zwangsweise komplexer und daher ist es sinnvoll, die einzelnen Teile in Funktionen auszulagern, die dann nur noch in der Hauptschleife aufgerufen werden müssen (Modularisierung). Es folgt rouled2.py, das aber immer noch an den ESP32 gebunden ist. Zu den Importen gesellen sich zwei weitere Methoden, pow() und TimeOutMs(). Mit pow() berechnen wir Potenzwerte, und TimeOutMs() erzeugt einen nichtblockierenden Softweare-Timer, während dessen Ablauf auch noch andere Dinge passieren können.
from machine import Pin
from neopixel import NeoPixel
from time import sleep_us,sleep
from sys import exit
import random
from math import pow
from timeout import TimeOutMs
Eine Status-LED wird uns künftig den Zustand des Systems mitteilen. Eine einzelne Neopixel-LED von dem Streifen symbolisiert in Grün die Startbereitschaft, in Blau, dass die Starttaste gedrückt ist und in Rot, dass das Spiel läuft und nichts mehr gesetzt werden darf. Ein erneutes Grün zeigt das Ende des Spiels und die neue Startbereitschaft an. Trotz der drei Farben brauchen wir nur eine Steuerleitung für das zweite Neopixel-Objekt an GPIO12. Wir definieren zusätzlich die Farb-Tupel für blue und off und verschieben odd und even hierher.
np=Pin(14, Pin.OUT)
nbrOfLeds=25
n=NeoPixel(np, nbrOfLeds)
num=1
statePin=Pin(12, Pin.OUT)
state=NeoPixel(statePin,num)
red=(0x20,0,0)
green=(0,0x20,0)
blue=(0,0,0x20)
off=(0,0,0)
odd=(0x80,0,0)
even=(0,0,0x80)
An GPIO13 schließen wir die Taste für den Start eines neuen Durchlaufs an, denn es ist übel, wenn man für jede Spielrunde das Programm neu starten muss. Ohne PC ginge das eh nicht.
taste=Pin(0,Pin.IN,Pin.PULL_UP)
start=Pin(13,Pin.IN,Pin.PULL_UP)
Die Funktion setState() schickt das übergebene Farb-Tupel an die Status-LED.
def setState(col):
state[0]=col
state.write()
def ledsOff():
for i in range(nbr):
n[i]=(0,0,0)
n.write()
Für einen flüssigeren Ablauf der Runden für die Kugel habe ich die Strategie verbessert. Der Croupier (Spielleiter) soll die Möglichkeit erhalten, die Kugel mit mehr oder weniger Kraft zu starten. Das macht er durch mehr oder weniger langes Drücken des Startbuttons. Die Philosophie, die hinter dem neuen Ansatz steckt, zielt darüber hinaus auf einen flüssigeren Spielablauf. Die Funktion getRounds() ermittelt nun lediglich die Anzahl ganzer Runden und hat noch nichts mit der zufälligen Auswahl der Zielposition zu tun. Die wird erst in der allerletzten Runde angesteuert.
Per Default werden 8 Voll-Runden als Maximum vorgelegt, wenn beim Start der Funktion kein anderer Wert übergeben wird. Als Minimum legen wir in val den Wert 2 vor. Dann warten wir auf die Betätigung der Start-Taste, deren Zustand im 0,2-Sekunden-Rhythmus abgefragt wird. In dieser Schleife berücksichtigen wir auch die Abbruchtaste. Wird sie gedrückt, dann gehen erst die LEDs aus, auch die Status-LED, bevor das Programm beendet wird.
Ohne Abbruch schalten wir die Status-LED auf Blau und die LEDs im Streifen aus. Nach 0,2 Sekunden Wartezeit starten wir den nichtblockierenden Softwaretimer. Die Ablaufzeit ergibt sich aus dem um 2 verringerten Rundenmaximum multipliziert mit der halben Taktfrequenz des Timers. Der Timer selbst ist eine Closure. Näheres dazu finden Sie im verlinkten Artikel. Das Wesentliche ist, dass TimeOutMs() die Referenz auf eine Funktion compare() zurückgibt, die ihrem Funktionskörper deklariert ist. Diese Referenz merken wir uns unter dem Bezeichner done. done() gibt False zurück, solange die an TimeOutMs() übergebene Zeit in Millisekunden noch nicht abgelaufen ist. Danach ist der Rückgabewert True.
def getRounds(rounds=8):
val=2
while start() != 0:
sleep(0.2)
if taste() == 0:
ledsOff()
setState(off)
exit()
setState(blue)
ledsOff()
sleep(0.2) # Starttaste entprellen
done=TimeOutMs((rounds-2)*500)
while not done():
val += 1
sleep(0.5)
if start():
setState(off)
return val
setState(off)
return random.randint(3,8)
Am Beginn der while-Schleife fragen wir den Rückgabewert ab. val erhöhen wir nun im Abstand von einer halben Sekunde so lange, bis die Start-Taste losgelassen wird. Dann schalten wir die Status-LED aus und geben den Runden-Wert in val zurück.
Ist der Timer abgelaufen bevor die Start-Taste losgelassen wird, dann wird die Status-LED ebenfalls ausgeschaltet und für die Rundenzahl ein Zufallswert von 3 bis 7 zurückgegeben.
Die Modellierung der Kugelbewegung habe ich auch aus der Hauptschleife herausgenommen und in die Funktion go() verpackt. Der Segmentzähler wird auf 0 gesetzt und der erste Verzögerungswert in Millisekunden berechnet. Weil irgendwas hoch 0 stets 1 als Ergebnis hat, hätte ich delay auch gleich auf 1 setzen können. An den nächsten Zeilen wurde fast nix geändert, nur die Zero wird jetzt statt weiß, grün aufleuchten und die 25 wurde durch die Konstante nbrOfLeds ersetzt. Das ist für die Programmpflege übersichtlicher, weil der Wert nur an einer einzigen Stelle im Programm gepflegt werden muss und so Redundanzen vermieden werden.
def go():
segment=0
delay=int(pow(basis,segment))
print(rounds, layers, basis, delay)
old=0
new=0
while 1:
n[old]=(0,0,0)
n[new]=odd if new % 2 else even
if new == 0:
n[new]=(0,0xC0,0)
n.write()
old=new
new +=1
new %= nbrOfLeds
sleep_us(delay)
segment+=1
delay=startDelay+int(pow(basis,segment))
if segment >= layers:
break
if taste() == 0:
ledsOff()
break
Die Berechnung der Verzögerungszeit wurde neu codiert. Wir hangeln uns jetzt direkt von Segment zu Segment und berechnen mit der Segmentnummer segment als Exponent zur Basis basis direkt die Verzögerungszeit delay in Microsekunden unter Berücksichtigung der minimalen Verzögerung von startDelay ms. Wir erreichen damit eine bessere Auflösung und einen realistischeren Kugellauf, weil wir schon vor Ablauf der Spielzeit die Anzahl der Rundendurchläufe kennen. In der globalen Variablen layers steckt die maximale Anzahl an zu durchlaufenden Segmenten. Ist der Wert erreicht, wird die while-Schleife und damit die Funktion verlassen. Das ist auch jederzeit mittels der Abbruch-Taste möglich.
Wir setzen die maximale Verzögerungszeit auf 800 000µs = 800ms und das minimale Delay auf 20ms. Mit diesen Werten können wir den Kugellauf modellieren. Startsignal auf Grün, wir können loslegen.
lastDelay=800000 # us
startDelay=20000
setState(green)
Die Main Loop enthält jetzt nur noch die wesentlichen Bereiche, zusammengefasst in Funktionsaufrufen.
rounds setzt sich zusammen aus den ganzen Runden, die wir mit getRounds() ermitteln plus einem Bruchanteil, den wir durch den Aufruf von random.random() als zufälligen Part ermitteln. Die Gesamtanzahl von zu durchlaufenden Segmenten erhalten wir als Ganzzahl layers anhand des Produktwerts aus rounds und der Anzahl von LEDs im Streifen. Aus rounds = 4,21358 wird so zum Beispiel layers = 105.
Die Exponentialfunktion, mit der wir die Verzögerung berechnen, hat die folgende Form. startDelay ist im Programm vorgegeben und segment ist die Laufvariable. Es fehlt also noch der Wert für basis.

Abbildung 7: So berechnen wir die Verzögerung
Den erhalten wir durch die maximale Verzögerungszeit lastDelay und Auflösen der Funktionsgleichung nach basis.

Abbildung 8: Der Wert für basis variiert von Fall zu Fall
Den basis-Wert müssen wir bei jedem Spieldurchlauf neu berechnen. Dann geht die Status-LED auf Rot und der Lauf der Kugel kann starten, go()!
while 1:
rounds= getRounds() + random.random()
layers=int(rounds*nbrOfLeds)
basis=pow(lastDelay-startDelay,1/layers)
setState(red)
go()
setState(green)
if taste() == 0:
ledsOff()
setState(off)
exit()
Nach der Rückkehr geben wir grünes Licht für eine neue Spielrunde, wenn wir nicht das Programm abbrechen.
Für den ESP32 könnten wir jetzt gut mit dem Ergebnis leben.
Frage an Radio Eriwan:
Kann man das Programm rouled2.py auch für den ESP8266 und den Raspberry Pi Pico zum Laufen kriegen?
Antwort:
Im Prinzip ja, man muss nur die Unterschiede in den MicroPython-Dialekten von ESP32, ESP8266 und Raspberry Pi Pico beseitigen.
Und wie sehen die denn aus? Die Antwort gibt der Object Inspector von Thonny. Das Modul random stellt für die drei Controller-Familien unterschiedliche Methoden bereit.
ESP32:

Abbildung 9: Methoden des Moduls random beim ESP32

Abbildung 10: Methoden des Moduls random beim ESP8266

Abbildung 11: Methoden des Moduls random beim RPP
Da ergibt sich also das Problem, dass die MicroPython-Dialekte für die drei Familien gerade bei der Erzeugung von Pseudo-Zufallszahlen voneinander abweichen. Dieses Problem bekommen wir in den Griff, wenn wir schon einmal eine automatische Erkennung des eingesetzten Controllers durchführen. Das gelingt mit dem Attribut platform aus dem Modul sys. Während ESP32 und Raspberry Pi Pico weitgehend die gleichen Methoden anbieten, müssen wir beim ESP8266 nachhelfen. Wir definieren für den ESP8266 einfach die beiden fehlenden Methoden als Funktionen. random() soll, wie beim ESP32 und beim Raspberry Pi Pico, eine Fließkommazahl von 0 bis 0,9999… liefern und randint() eine Ganzzahl in einem vorgegebenen Bereich. getrandbits(n) liefert eine Ganzzahl deren Wert bis zu n Bits in der binären Darstellung bedarf.
from machine import Pin
from neopixel import NeoPixel
from time import sleep_us,sleep
from sys import exit, platform
import random
from math import pow
from timeout import TimeOutMs
if platform == "esp32":
from random import random, randint
elif platform == "esp8266":
from random import getrandbits
def random():
return getrandbits(24)/(2**24)
def randint(von,bis):
return von+int(getrandbits(24)*(bis-von)/(2**24))
elif platform == "rp2":
from random import random, randint
else:
print("Nicht unterstuetzter Controller")
exit()
Im Programm müssen jetzt noch zwei Stellen angepasst werden.
def getRounds(rounds=8):
val=2
while start() != 0:
sleep(0.2)
if taste() == 0:
ledsOff()
setState(off)
exit()
setState(blue)
ledsOff()
sleep(0.2) # Starttaste entprellen
done=TimeOutMs((rounds-2)*500)
while not done():
val += 1
sleep(0.5)
if start():
setState(off)
return val
setState(off)
return randint(3,8) # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
while 1:
rounds= getRounds() + random() # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
layers=int(rounds*nbrOfLeds)
basis=pow(lastDelay-startDelay,1/layers)
setState(red)
go()
setState(green)
if taste() == 0:
ledsOff()
setState(off)
exit()
Das erweiterte Programm wird jetzt unter rouled3.py abgespeichert und läuft auf allen drei Controller-Familien ohne weitere Anpassungen.
Damit das auch ohne USB-Verbindung zum PC funktioniert, müssen wir das Programm unter dem Namen main.py in den Flash des Controllers hochladen. Beim nächsten Neustart oder Reset startet der Controller autonom.
Einen Spieldurchlauf können Sie auf dem Video anschauen. Viel Vergnügen und Freude mit Ihrem neuen RouLED.