vorweg: Ich komme beruflich aus dem JAVA Umfeld, ein Mikrokontroller ist für mich also immer eine starke Einschränkung der Möglichkeiten
in einem (für mich) größeren Projekt mit über 30 Relais, einigen Sensoren und anderem Gedöns, die alle möglichst parallel bedient werden möchten, habe ich mich mit Zustandsautomaten (State Machines) beschäftigt.
Die Anzahl der anzusteuernden Elemente machte das Modellieren als einen großen Automaten für mich schnell sehr unübersichtlich. Letztlich ist dann eine kleine Bibliothek entstanden, die das Implementieren von StateMachines als jeweils eigene Klasse und ein Zusammenspiel der verschiedenen Automaten ermöglicht.
Den Code habe ich auf github veröffentlicht:
Die Dokumentation habe ich aus Gewohnheit auf Englisch geschrieben, ich hoffe das hält euch hier nicht ab.
Vermutlich ist das ganze etwas "over engineered", aber ich hatte Spaß. Für mich funktioniert das so ganz gut, ich hätte jetzt aber gern Feedback von erfahreneren Arduino Nutzern.
Taugt das was?
Hättest ihr etwas (komplett?) anders gelöst?
Ideen für weitere Features? Ich denke z.B. an die Möglichkeit, das Warten auf etwas durch einen Funktionsaufruf vorzeitig zu beenden. Benötigt habe ich das aber bislang nicht
Einen logischen Fehler habe ich gefunden. millis() + irgendwas funktioniert bis fast zum Überlauf, und dann kracht es. Es gibt Abhilfe, ohne Mehraufwand.
Dann vermute ich mal, dass die ganzen this-> ersatzlos gestrichen werden dürfen....
Kann man tun, muss aber nicht.
Ja, ich schaue mir das noch mal genauer an!
(vielleicht kann ich ja was klauen)
PS:
Was mir auf den ersten Blick nicht so schmeckt, ist das static. Das hat sich in meinen Anwendungen/Libs bisher (viel zu oft) als sperrig erwiesen.
Ich weiß nicht, wie genau Du mehrere Timer verwaltest.
Entsprechend dem BlinkWithoutDelay Muster werden alle Timer in loop() der Reihe nach abgefragt, ohne weiteren Verwaltungsaufwand. So funktionieren auch die Task-Makros.
In einem Betriebssystem werden alle Timer in einer Liste gehalten, und nur das nächstliegende Ereignis geprüft - Simula67 läßt grüßen, der Urahn aller OO Programme.
das millis + wurde eh schon erwähnt.
dabei fällt auch noch auf, du übergibst für den Intervall ein int das du dann auf ein long castest. Lass doch gleich ein uint32_t übergeben, dann gibts auch Blinkenzeiten > 32 Sekunden.
Generell scheint mir du verwendest oft int ... auch für pins. Arduino verwendet in den meisten Funktionen für Pins uint8_t.
die States die du in static definiert hast ... da würde sich wohl eine enum anbieten oder?
Vieleicht ein paar Kommentare in den Beispielen, zumindest was du mit den Variablen machen wirst?
Das Beispiel blinking_led ist klar.
Das Beispiel controlled_led hat states die du augenscheinlich nicht von außen nutzt. Fehlen da ein paar Taster oder Serielle Kommandos um on / off zu schalten?
wie das LED Objekt den scope von setup() überleben soll, muss ich mir nach einem Kaffee noch mal anschauen.
void setup() {
LED led(13);
Ein Diagram (oder auch Text) das das Zusammenspiel von Registry und StateMachine erklärt wäre nett gewesen - verteckt im Fließtext war das kaum zu finden.
Du hast Recht: Die Overflow Problematik war mir beim Implementieren auch bewusst - mein Anwendungsfall hat das aber sehr unrealistisch erscheinen lassen und dann ist es in Vergessenheit geraten. Ich habe einen Issue angelegt. Dein Link klingt vielversprechend, aber auf Anhieb habe ich die Lösung noch nicht verstanden.
Beim this-> kommt vermutlich mal wieder der alte Java Entickler durch - da gehört es einfach zum guten Ton. Ist aber ja nur Geschmackssache.
So viel static nutze ich aber doch gar nicht? Mir fällt nur die zentrale Registry ein - die muss static sein, damit alle StateMachines ohne manuelles Eingreifen automatisch die gleiche Instanz nutzen?
@DrDiettrich
Eine sehr nette Bibliothek hast du da. Hätte ich sie früher gekannt, wäre ich vermutlich damit angefangen
Besonders gefällt mir, dass die Abläufe sehr schön lesbar sind. Meine C++ Kenntnisse reichen leider nicht, um direkt zu verstehen, wie du das genau machst, aber der Code wirkt so sehr aufgeräumt.
Wenn ich das richtig sehe, sind die jeweiligen Handlungsstränge allerdings "nur" als Funktion verfügbar ist. Mir fehlt da ein wenig die Verknüpfung zum Zustand des jeweiligen Objektes - das muss immer separat in (globalen Variablen gespeichert werden, was mMn in größeren Umgebungen unübersichtlich wird. Mit meinem Ansatz ist der Zustand immer im Objekt gespeichert.
Ich habe mir erlaubt, dein Beispiel mit Ampel und Autos nach zu prorgammieren. Da sieht man dann auch, wie die parallelen "Timer" miteinander interagieren:
Ich prüfe in der Tat in jeder loop() Funktion alle "Timer". Hintergrund ist vor allem, dass die Reihenfolge, in der die Objekte aufgefordert werden, immer gleich bleiben soll. Sonst kommt man wieder in komische Zustände.
@noiasca
Danke für die Hinweise bzgl. der Typen - da bin ich etwas nachlässig gewesen. Bei der controlled_led ging es mir vor allem darum die Interaktion zwischen zwei StateMachines zu zeigen: Der "Controller" wartet, bis die LED fertig ist mit blinken und stößt dann ein neues Blink an. Das o.g. traffic.ino ist aber vermutlich das einleuchtendere Beispiel dafür.
Das LED Objekt im setup() ist in der Tat etwas unsauber: Durch Erzeugen einer StateMachine Instanz wird sie ja automatisch in der zentralen Registry vermerkt und wird von dort auch regelmäßig wieder angestoßen - schön ist das aber nicht, das habe ich gerade korrigiert. Danke für den Hinweis!
Bzgl. der enums - Ich vermute, hier stoße ich dann an C++ Wissensgrenzen: Die state Variable wird ja initial von der StateMachine Basisklasse bereitgestellt und der Wertebereich dann durch die implementierende Klasse definiert. Welchen Typ hätte denn state dann in StateMachine? OO
Und ja, die Registry ist nur sehr rudimentär erklärt, das hole ich nach. Ich nehme aber an, du bist letztlich auch so durchgestiegen?
Mir gefällt der Name waiting-StateMachine übrigens noch nicht wirklich gut - StateMachine ist im Arduino Kontext aber schon vergeben ^^
Aktuell tendiere ich zu "collaborating-StateMachine" - mal sehen, vielleicht finde ich noch etwas passenderes
Sie wurde inspiriert von @combie, der sie AFAIR von Knuth abgestaubt hat und inzwischen möglicherweise auch schon auf OO umgestellt hat. Mir kam es tatsächlich auf möglichst einfache und verständliche Implementierung an, dann kann am wenigsten schief gehen.
Ein Anfänger kann z.B. mehrere unterschiedliche Beispielprogramme als Tasks quasi-parallel laufen lassen und muß i.W. nur delay() in taskDelay() umbenennen. Mein Ampel-Beispiel ist eigentlich unpassend, da auf diese Art nur 1 Ampel o.ä. implementiert werden kann, also keine Ampelanlage für eine Kreuzung.
Bei Deinem Ansatz steige ich nicht so richtig durch. Wie kann in einem Zustand auf mehrere Ereignisse gewartet und entsprechend verzweigt werden? Einfaches Beispiel: Warten auf Ereignis A oder B oder Timeout?
Bekommst du hin.....
Der Trick ist keine Addition zu verwenden, sondern eine Subtraktion.
So wird der Überlauf durch einen Unterlauf kompensiert, das geht natürlich nur mit unsigned Zeitmerkern.
Ja, und Nein.
Grundlegende Betrachtungen stammen vom Herren Knuth, einer meiner Vorbilder/Idole, aber der Kern, der Quell der endgültigen Inspiration vom Herrn Adam Dunkels.
Dessen Protothreads habe ich gnadenlos verschlankt (Task Control Blocks abgeschafft)
Nun: Letztlich werden hier Zustandsautomaten implementiert - das Framework bietet ledigilch grundlegende Hilfe bei deren Zusammenarbeit, um nicht alle Sensoren / Aktoren in einem Automaten implementieren zu müssen.
Konkret heißt das: Wenn ich wie von dir angeführt sowohl einen Timer als auch eine sonstwie geartete Bedingung überwachen möchte, muss ich das manuell tun.
Als Beispiel: Im Traffic Example wird im Falle eines fahrenden Autos (Status DRIVING) bei jedem Durchlauf geprüft, ob die Ampel noch grün ist (etwas unschön gelöst, in dem die Ampel "available" ist, wenn sie grün zeigt):
if (this->state == DRIVING) {
// when driving: check if traffic light still green
if (!this->trafficLight->isAvailable()) {
// if not - BREAK
Serial.println("noticed a non-green traffic light!");
this->state = BRAKING;
}
}
Hier kann man natürlich die Logik erweitern und z.B. prüfen, ob man lange genug gefahren ist - der Timer dazu müsste an dieser Stelle dann selbst entwickelt werden.
Ein weiteres Beispiel: Beim Fahren sollte vielleicht auch immer immer geprüft werden, ob vor dem Auto ein Hindernis ist. Für das Auslesen der Sensoren könnte man einen eigenen Automaten implementieren, dessen Zustand an dieser Stelle nur noch abgefragt werden muss.
Die Idee dahinter: Statt einem riesengroßen Automaten implementiere ich mehrere kleine, die sich unabhängig Testen und Weiterentwickeln lassen. Alle diese StateMaschines
laufen dann "parallel"
können miteinander "interagieren" (z.B.: aufeinander warten).
Das reduziert die Komplexität in größeren Projekten.
Die "Trennung" der einzelnen State Machines ist schon ok. Ich vermute die Trennung in Registry und StateMachine ist das was mir weniger gefällt.
Mir gefällt zwar dass damit in setup/loop ein call ausreicht, aber die damit eingekaufte Limitierung auf "256" state machines wär's mir nicht wert.
Bei deinem Ampelbeispiel würde ich mir eher wünschen, dass das Auto den Ampelstatus abfragt ... und nicht auf ein "available". Im einfachsten Fall ein isGreen?
Die Registry dient ja nur dem Zweck, alle StateMachine Instanzen zu kennen und synchronisiert zu initialisieren / den nächsten Schritt gehen zu lassen. Die Limitierung auf 256 Instanzen rührt lediglich aus der Größe des dafür genutzten Arrays. Das zu vergrößern, erfordert nur die Änderung einer einzigen Zeile.
256 Zustandsaustomaten sind aus meiner Sicht schon eine ganze Menge. Um beim Beispiel zu bleiben: 100 Autos mit jeweils einem Kollisionssensor, 50 Ampeln und es bliebe immer noch Platz für ein paar Fußgänger
Ja, isGreen() gefällt mir auch deutlich besser, so würde ich es in einem echten Projekt auch umsetzen. Im Beispiel wollte ich gerne möglichst ohne weitere Funktionen auskommen und habe das isAvailable() missbraucht, das macht so nicht wirklich Sinn.
Ganz grundsätzlich wäre es cool, wenn ich einfach Bedingungen und dann zu setzenden Status angeben könnte. Also sowas wie
Falls [ this->trafficLight->isGreen() ] => DRIVE
Und die Basisklasse prüft diese Bedingung(en) und triggert die Instanz erst dann wieder, wenn sich etwas verändert hat.
Dazu müsste ich mich dann aber, wenn ich das richtig sehe, mit lambda Funktionen, Funktionspointern etc. auseinandersetzen und prüfen, was davon auf den Arduinos so möglich ist. Und ich vermute stark, dass ich dann nicht mehr ganz so prozessorunabhängig wäre. Aber es wäre sehr schöner Code
Aber eins habe ich schon gelernt, aus den "andersartigen Betrachtungen":
Ich kann in meinen Tasks auf die Runable::init() Methode verzichten.
Das wird in absehbarer Zeit umgestellt.
Die hat mir nämlich unangenehme Probleme bereitet, beim erzeugen von Tasks(Statemachines) zur Laufzeit.
Meinen Dank an dich, für die mir doch recht fremde Denke.
OT:
In meiner Stadt-Simualtion kann ich anbieten:
20 Autos (aus 4 Klassen)
16 Ampeln
3 Organisationen/Firmen
3 Buttons
8 Hausbrände (wobei diese keine Objekte sind sondern in der Stadt passieren, irgendwann wirds halt am Uno/Nano knapp...)
Habe mal eine kleine Überarbeitung vorgenommen.
Klar ist, dass sie dir nicht schmecken muss.....
Ist auch noch etwas von "optimal" entfernt.
Der Kern der Änderungen:
Die statischen Dinge sind zu anonymen enums geworden.
Fast alle Zeigerzugriffe entsorgt.
if-else Kaskaden entfernt
Der led Pin ist readonly geworden, wird sich ja zur Laufzeit nie ändern dürfen
Globale Instanzen entfernt
controlled_led.ino
#include "StateMachine.hpp"
class LED: public StateMachine
{
private:
enum /*State*/ {NullState=0,SWITCH_ON,ON,SWITCH_OFF,OFF};
/*
static constexpr int SWITCH_ON = 1;
static constexpr int ON = 2;
static constexpr int SWITCH_OFF = 3;
static constexpr int OFF = 4;
*/
const byte pin;
public:
LED(byte pin): pin{pin}
{
state = OFF;
}
void init() override
{
digitalWrite(pin, LOW);
pinMode(pin, OUTPUT);
}
void step() override
{
switch(state)
{
case ON: break;
case OFF: break;
case SWITCH_ON: state = ON;
digitalWrite(pin, HIGH);
wait(1000, SWITCH_OFF);
break;
case SWITCH_OFF: state = OFF;
digitalWrite(pin, LOW);
break;
default: Serial.println("Fatal LED");
while(1){};
}
}
bool isAvailable() override
{
return state == OFF;
}
void switchOn()
{
state = SWITCH_ON;
}
};
class LEDControl : StateMachine
{
private:
enum /* State */ {SWITCH_LED_ON=0,IDLE};
/*
static constexpr int SWITCH_LED_ON = 0;
static constexpr int IDLE = 1;
*/
LED &led;
public:
LEDControl(LED &led):led{led}{}
void step() override
{
switch(state)
{
case SWITCH_LED_ON: led.switchOn();
waitFor(&led, IDLE);
break;
case IDLE: wait(2000, SWITCH_LED_ON);
break;
default: Serial.println("Fatal LEDControl");
while(1){};
}
}
};
//LED led(13);
//LEDControl control(led);
void setup()
{
Serial.begin(9600);
new LEDControl(*new LED(13));
StateMachine::registry->init();
}
void loop()
{
StateMachine::registry->nextStep();
}
Alles nur kosmetische Änderungen!
Könnte man noch mehr tun? Sicherlich!
Weiß nicht.....
Irgendeine echte Not gibts dafür nicht.
Wie gesagt: Kosmetik.
Weniger Zeilen zu schreiben.
Kein Bezeichner für Dinge, die keinen Bezeichner brauchen. Also auch keine Chance für Irrtümer oder Tippfehler.
Immer wenn ich static sehe, bekomme ich das Gefühl gleich in eine Falle zu tappen.
Ich finde es irritierend zu sehen, dass irgendwo ein status definiert wurde, ohne zu definieren, wieviele/welche es davon gibt. Das geschieht an an anderer Stelle.
Konkreter wäre:
enum class State:byte {SWITCH_ON,ON,SWITCH_OFF,OFF}state;
public:
LED(byte pin): pin{pin}, state{State::OFF}
Auch wieder etwas mehr Schreibarbeit, aber dafür mehr Klarheit.
Geht aber nicht, da state eine doppelte Bedeutung hat, welche innerhalb der Klasse LED nicht ersichtlich ist.
Aber was solls, sind alles nicht meine Entscheidungen.