Add documentation
authorBenjamin Braatz <bb@bbraatz.eu>
Tue, 2 Mar 2021 21:29:52 +0000 (22:29 +0100)
committerBenjamin Braatz <bb@bbraatz.eu>
Tue, 2 Mar 2021 21:29:52 +0000 (22:29 +0100)
README.md
doc/index.md [new file with mode: 0644]

index 003e3d297f92cfb62871354c5875f1c653e89832..4f99abf5d2a3dabf39f2410de22e8a92c04e5b44 100644 (file)
--- 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 (file)
index 0000000..908bb58
--- /dev/null
@@ -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 <Pfad zum venv>
+$ source <Pfad zum venv>/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 <Pfad zum Code-Repository>
+```
+
+## 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 <Pfad zum Code-Repository>/controlpi
+(venv)$ pydocstyle <Pfad zum Code-Repository>/controlpi
+(venv)$ mypy <Pfad zum Code-Repository>/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 <Pfad zum Code-Repository>[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 <Pfad zur Code-Datei>
+```
+(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 <Pfad zur Code-Datei>
+```
+
+## 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 <Pfad zum Code-Repository>/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
+```