From: Benjamin Braatz Date: Tue, 2 Mar 2021 21:29:52 +0000 (+0100) Subject: Add documentation X-Git-Tag: v0.3.0~80 X-Git-Url: http://git.graph-it.com/?a=commitdiff_plain;h=3113c271777954da2cd16ad81a98f50a257139df;p=graphit%2Fcontrolpi.git Add documentation --- diff --git a/README.md b/README.md index 003e3d2..4f99abf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ # Control-Pi-Infrastruktur +ControlPi-Systeme sind Raspberry Pis, die Steuerungs-Aufgaben erledigen. +Sie sind nach dem Prinzip der übergeordneten Steuerung in einem Baum +organisiert, wobei jeweils höhere ControlPis die jeweils niedrigeren +organisieren, ohne dass benachbarte Systeme oder weiter oben oder unten in +der Hierarchie befindliche voneinander wissen müssen. + +Dieses Paket enthält die grundlegende Infrastruktur für ein +ControlPi-System, die Plugins verwaltet und ihnen ermöglicht über einen Bus +Nachrichten auszutauschen. + +Die Dokumentation kann unter [doc/index.md](doc/index.md) bzw. +[https://docs.graph-it.com/graphit/controlpi](https://docs.graph-it.com/graphit/controlpi) +gefunden werden. diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..908bb58 --- /dev/null +++ b/doc/index.md @@ -0,0 +1,380 @@ +# Control-Pi-Infrastruktur +ControlPi-Systeme sind Raspberry Pis, die Steuerungs-Aufgaben erledigen. +Sie sind nach dem Prinzip der übergeordneten Steuerung in einem Baum +organisiert, wobei jeweils höhere ControlPis die jeweils niedrigeren +organisieren, ohne dass benachbarte Systeme oder weiter oben oder unten in +der Hierarchie befindliche voneinander wissen müssen. + +Dieses Paket enthält die grundlegende Infrastruktur für ein +ControlPi-System, die Plugins verwaltet und ihnen ermöglicht über einen Bus +Nachrichten auszutauschen. + +## Installation zum Entwickeln +Es wird mindestens Python 3.7 benötigt. Für Ubuntu-Versionen, die selbst +noch keine aktuelle Python-Version zur Verfügung stellen, kann eine aus dem +PPA [deadsnakes](https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa) +installiert werden: +```sh +$ sudo add-apt-repository ppa:deadsnakes/ppa +$ sudo apt update +$ sudo apt install python3.9 python3.9-venv +``` + +Das Repository mit dem Code dieses Pakets kann vom öffentlichen git-Server +bezogen werden: +```sh +$ git clone git://git.graph-it.com/graphit/controlpi.git +``` +(Falls Zugang zu diesem Server per SSH besteht und Änderungen gepusht +werden sollen, sollte stattdessen die SSH-URL benutzt werden.) + +Damit Python-Module in diesem Paket gefunden und eventuelle Abhängigkeiten +installiert werden, dies aber von anderen Teilen des Systems, auf dem +entwickelt wird, isoliert gehalten wird, sollte ein virtuelles Environment +verwendet werden. Wo dieses Verzeichnis liegt, ist dabei relativ egal. +Übliche Orte sind innerhalb des Code-Repository-Verzeichnisses (hierfür +wird `venv/` von git ignoriert), parallel zum Code-Repository oder an einer +zentralen Stelle des Home-Verzeichnisses gesammelt. + +Sobald das virtuelle Environment eingerichtet und aktiviert ist, beziehen +sich Befehle wie `python` und `pip` auf die in diesem Environment +konfigurierten und müssen nicht mehr mit genauer Version aufgerufen werden: +```sh +$ python3.9 -m venv +$ source /bin/activate +(venv)$ pip install --upgrade pip setuptools +``` + +Damit Änderungen am Code sofort wirksam werden und getestet werden können, +sollte das Paket, an dem gerade entwickelt wird, im virtuellen Environment +editierbar installiert werden: +```sh +(venv)$ pip install --editable +``` + +## Code-Stil, Typ-Checks und Tests +Die Formatierung des Codes und das Vorhandensein und der Stil der +Kommentare werden durch `pycodestyle` und `pydocstyle` überprüft. Die an +Funktionen und Variablen annotierten Typ-Informationen werden durch `mypy` +gecheckt. Alle drei Tools können rekursiv ein gesamtes Python-Paket +überprüfen: +```sh +(venv)$ pycodestyle /controlpi +(venv)$ pydocstyle /controlpi +(venv)$ mypy /controlpi +``` + +Sie sind als Extra-Requirements in der Datei `setup.py` definiert, sodass +sie mit einem einzigen `pip`-Aufruf installiert werden können: +```sh +(venv)$ pip install --editable [dev] +``` + +Der Code wird durch in die Dokumentation eingebettete „doctests“ getestet. +Diese können für jede Code-Datei einzeln mit dem in der +Python-Standard-Distribution enthaltenen Modul `doctest` ausgeführt werden: +```sh +(venv)$ python -m doctest +``` +(Für die Datei `__main__.py`, die das kleine Hauptprogramm zum Ausführen +des Systems enthält, funtioniert dies leider nicht.) + +Komplette Informationen über alle durchgeführten Tests und die Abdeckung +des Codes mit Tests erhält man mit der zusätzlichen Option `-v`: +```sh +(venv)$ python -m doctest -v +``` + +## Beispiel-Konfiguration +Eine Beispiel-Konfiguration, die nur die mitgelieferten Werkzeug-Plugins +verwendet, ist im Repository enthalten. Das System kann mit dieser +Konfiguration gestartet werden: +```sh +(venv)$ python -m controlpi /conf.json +``` +(Das System läuft in normalerweise in einer Endlos-Schleife und kann mit +Ctrl-C oder durch anderes Beenden seines Prozesses von außen beendet +werden.) + +Die Beispiel-Konfiguration sieht folgendermaßen aus: +```json +{ + "State": { + "plugin": "State" + }, +``` +Ein `State`-Plugin speichert intern einen Booleschen Wert (`True` oder +`False`) und stellt Kommandos `get state` und `set state` zur Verfügung, um +diesen Wert abzufragen oder zu ändern und reagiert darauf durch das Senden +von `state`-Ereignissen. + +```json + "WaitCheck": { + "plugin": "Wait", + "seconds": 1.0 + }, + "TriggerStateCheck": { + "plugin": "Alias", + "from": { "sender": "WaitCheck", "event": "finished" }, + "to": { "target": "State", "command": "get state" } + }, + "TriggerWaitCheck": { + "plugin": "Alias", + "from": { "sender": "WaitCheck", "event": "finished" }, + "to": { "target": "WaitCheck", "command": "wait" } + }, +``` +Ein `Wait`-Plugin stellt ein Kommando `wait` zur Verfügung und wartet nach +Erhalt dieses Kommandos die in der Konfiguration angegebene Zahl an +Sekunden, bevor es ein `finished`-Ereignis sendet. Die beiden +`Alias`-Plugin-Instanzen übersetzen dieses Ereignis zum Einen in eine +Abfrage an das `State`-Plugin nach seinem momentanen Zustand, zum Anderen +in einen neuen Start des `Wait`-Plugins selbst. Es wird also im Endeffekt +jede Sekunde einmal der Zustand des `State`-Plugins abgefragt. + +Statt `Alias`-Instanzen zu verwenden, um Ereignisse direkt in Kommandos +anderer (oder desselben) Plugins zu übersetzen, werden im Paket +[controlpi-statemachine](https://docs.graph-it.com/graphit/controlpi-statemachine) +Zustands-Maschinen zur Verfügung gestellt, mit denen sich komplexere +Verschaltungen eleganter konfigurieren lassen. + +```json + "WaitOn": { + "plugin": "Wait", + "seconds": 1.5 + }, + "TriggerStateOnOff": { + "plugin": "Alias", + "from": { "sender": "WaitOn", "event": "finished" }, + "to": { "target": "State", "command": "set state", "state": false } + }, + "TriggerWaitOnOff": { + "plugin": "Alias", + "from": { "sender": "WaitOn", "event": "finished" }, + "to": { "target": "WaitOff", "command": "wait" } + }, + "WaitOff": { + "plugin": "Wait", + "seconds": 1.5 + }, + "TriggerStateOffOn": { + "plugin": "Alias", + "from": { "sender": "WaitOff", "event": "finished" }, + "to": { "target": "State", "command": "set state", "state": true } + }, + "TriggerWaitOffOn": { + "plugin": "Alias", + "from": { "sender": "WaitOff", "event": "finished" }, + "to": { "target": "WaitOn", "command": "wait" } + }, +``` +Mit `WaitOn` und `WaitOff` werden zwei `Wait`-Instanzen so konfiguriert, +dass sie sich gegenseitig aufrufen, wobei die eine den Zustand der +`State`-Instanz auf `False`, die andere dann wieder auf `True` setzt. + +```json + "Test Procedure": { + "plugin": "Init", + "messages": [ + { "event": "started" }, + { "target": "WaitOff", "command": "wait" }, + { "target": "WaitCheck", "command": "wait" }, + { "event": "stopped" } + ] + }, +``` +`Init`-Instanzen senden einfach am Beginn der Ausführung des +ControlPi-Systems eine konfigurierte Liste von Nachrichten. Hier werden die +beiden `Wait`-Instanzen für das Setzen und das Auslesen des +`State`-Zustandes (die sich danach endlos selbst auslösen) das erste Mal +angestoßen. + +```json + "Debug Logger": { + "plugin": "Log", + "filter": [ + {} + ] + }, + "State Change Logger": { + "plugin": "Log", + "filter": [ + { "sender": "State", "changed": true } + ] + } +} +``` +`Log`-Instanzen geben einfach durch einen konfigurierten Filter bestimmte +Nachrichten auf der Konsole (oder, falls das System als systemd-Unit läuft, +im System-Journal) aus. Hier wird eine Instanz `Debug Logger`, die alle +Nachrichten ausgibt, und eine Instanz `State Change Logger`, die nur +Nachrichten der `State`-Instanz und diese auch nur, wenn eine Änderung +stattgefunden hat, ausgibt, konfiguriert. + +Die Ausgabe eines Laufs dieses Beispiel-Systems sieht folgendermaßen aus: +``` +$ python -m controlpi conf.json +State 'State' configured. +Wait 'WaitCheck' configured. +Alias 'TriggerStateCheck' configured. +Alias 'TriggerWaitCheck' configured. +Wait 'WaitOn' configured. +Alias 'TriggerStateOnOff' configured. +Alias 'TriggerWaitOnOff' configured. +Wait 'WaitOff' configured. +Alias 'TriggerStateOffOn' configured. +Alias 'TriggerWaitOffOn' configured. +Init 'Test Procedure' configured. +Log 'Debug Logger' configured. +Log 'State Change Logger' configured. +``` +Zunächst melden alle konfigurierten Plugin-Instanzen, dass sie ihre +jeweilige Konfiguration eingelesen und verarbeitet haben. + +``` +State 'State' running. +Wait 'WaitCheck' running. +Alias 'TriggerStateCheck' running. +Alias 'TriggerWaitCheck' running. +Wait 'WaitOn' running. +Alias 'TriggerStateOnOff' running. +Alias 'TriggerWaitOnOff' running. +Wait 'WaitOff' running. +Alias 'TriggerStateOffOn' running. +Alias 'TriggerWaitOffOn' running. +Init 'Test Procedure' running. +Log 'Debug Logger' running. +Log 'State Change Logger' running. +``` +Dann melden wiederum alle, dass ihre Haupt-Routinen jetzt laufen. Bis auf +die `Init`-Instanz, die die konfigurierten Nachrichten in dieser +Hauptroutine abschickt, haben alle bisher gezeigten Plugins leere +Haupt-Routinen, da ihr Verhalten nur als Reaktion auf empfangene +Nachrichten stattfindet. + +``` +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'State'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'WaitCheck'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'TriggerStateCheck'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'TriggerWaitCheck'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'WaitOn'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'TriggerStateOnOff'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'TriggerWaitOnOff'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'WaitOff'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'TriggerStateOffOn'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'TriggerWaitOffOn'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'Test Procedure'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'Debug Logger'} +Debug Logger: {'sender': '', 'bus event': 'registered', 'client': 'State Change Logger'} +``` +Die Instanz `Debug Logger` zeigt uns alle im System verschickten +Nachrichten an. Zunächst meldet der Nachrichten-Bus selbst alle +Registrierungen der konfigurierten Plugins als Klienten des Busses. In +unserem Fall gibt es für jedes Plugin genau einen Klienten. Für komplexere +Plugins kann es aber durchaus mehrere (oder keine) Klienten für ein Plugin +geben, beispielsweise für jede gerade offfene Verbindung zu einer +Netzwerkschnittstelle. + +``` +Debug Logger: {'sender': 'Test Procedure', 'target': 'Test Procedure', 'command': 'execute'} +Debug Logger: {'sender': 'Test Procedure', 'event': 'started'} +Debug Logger: {'sender': 'Test Procedure', 'target': 'WaitOff', 'command': 'wait'} +Debug Logger: {'sender': 'Test Procedure', 'target': 'WaitCheck', 'command': 'wait'} +Debug Logger: {'sender': 'Test Procedure', 'event': 'stopped'} +``` +Die `Init`-Instanz `Test Procedure` hat alle ihre Nachrichten gesendet und +sie wurden von `Debug Logger` empfangen. + +``` +Debug Logger: {'sender': 'WaitCheck', 'event': 'finished'} +Debug Logger: {'sender': 'TriggerStateCheck', 'target': 'State', 'command': 'get state'} +Debug Logger: {'sender': 'TriggerWaitCheck', 'target': 'WaitCheck', 'command': 'wait'} +Debug Logger: {'sender': 'State', 'state': False, 'changed': False} +``` +Nach einer Sekunde ist die Wartezeit der `Wait`-Instanz `WaitCheck` das +erste Mal abgelaufen und daraufhin werden auch die beiden +`Alias`-Nachrichten geschickt, woraufhin `State' mit seinem momentanen +Zustand (aber auch mit der Information, dass dieser sich nicht geändert +hat) antwortet. + +``` +Debug Logger: {'sender': 'WaitOff', 'event': 'finished'} +Debug Logger: {'sender': 'TriggerStateOffOn', 'target': 'State', 'command': 'set state', 'state': True} +Debug Logger: {'sender': 'TriggerWaitOffOn', 'target': 'WaitOn', 'command': 'wait'} +Debug Logger: {'sender': 'State', 'state': True, 'changed': True} +State Change Logger: {'sender': 'State', 'state': True, 'changed': True} +``` +Eine weitere halbe Sekunde später sorgt der Ablauf der Instanz `WaitOff` +dafür, dass `State` auf `True` gesetzt wird. Hier reagiert jetzt auch der +`State Change Logger`. + +``` +Debug Logger: {'sender': 'WaitCheck', 'event': 'finished'} +Debug Logger: {'sender': 'TriggerStateCheck', 'target': 'State', 'command': 'get state'} +Debug Logger: {'sender': 'TriggerWaitCheck', 'target': 'WaitCheck', 'command': 'wait'} +Debug Logger: {'sender': 'State', 'state': True, 'changed': False} +``` +Wiederum eine halbe Sekunde später wird durch Ablauf von `WaitCheck` wieder +eine Abfrage des Zustands ausgelöst. + +``` +Debug Logger: {'sender': 'WaitOn', 'event': 'finished'} +Debug Logger: {'sender': 'TriggerStateOnOff', 'target': 'State', 'command': 'set state', 'state': False} +Debug Logger: {'sender': 'TriggerWaitOnOff', 'target': 'WaitOff', 'command': 'wait'} +Debug Logger: {'sender': 'State', 'state': False, 'changed': True} +State Change Logger: {'sender': 'State', 'state': False, 'changed': True} +``` +Nachdem `WaitOn` das erste Mal abgelaufen ist, wird `State` wieder auf +`False` gesetzt. + +Dies wiederholt sich jetzt solange weiter, bis das Programm abgebrochen +wird. + +## Installation auf Raspberry Pi +Auch auf dem Raspberry Pi wollen wir den Code in ein virtuelles Environment +installieren, wobei die weitere Vorgehensweise davon ausgeht, dass dieses +direkt unter `/home/pi` liegt, also insgesamt den Pfad +`/home/pi/controlpi-venv` hat: +```sh +$ sudo apt install python3-venv git +$ python3 -m venv controlpi-venv +$ source controlpi-venv/bin/activate +(venv)$ pip install --upgrade pip setuptools +(venv)$ pip install git+git://git.graph-it.com/graphit/controlpi.git +``` + +Die Datei `conf.json` und die Datei `controlpi.service` aus dem +Haupt-Verzeichnis des Repositories (oder jede anders, speziell für den +Einsatzzweck konfigurierte `conf.json`) sollten ebenfalls nach `/home/pi` +übertragen werden. + +Die `controlpi.service` ist vorbereitet, das ControlPi-System als +systemd-Service direkt beim Hochfahren zu starten und am Laufen zu halten. +Sie sieht folgendermaßen aus: +```ini +[Unit] +Description=ControlPi Service +Wants=network-online.target +After=network-online.target + +[Service] +WorkingDirectory=/home/pi +Environment=PYTHONUNBUFFERED=1 +ExecStart=/home/pi/controlpi-venv/bin/python -m controlpi conf.json +Restart=Always + +[Install] +WantedBy=multi-user.target +``` + +Sie wird durch folgende Kommandos aktiviert und gestartet: +```sh +$ sudo cp /home/pi/controlpi.service /etc/systemd/system/ +$ sudo systemctl enable controlpi.service +$ sudo systemctl start controlpi.service +``` + +Die Ausgaben können dann im Journal nachvollzogen werden: +```sh +$ journalctl -u controlpi.service +```