Als Controller kommt der Raspberry
Pi Pico W zum Einsatz (wie er offiziell heißt; ich sage gerne Pico 1 W, denn
inzwischen gibt es auch den Pico 2 W mit mehr von allem). Um genau zu sein ist es sogar der
Pico WH den ich verwende, das H steht für Header und bezeichnet die Variante, die bereits
auf einer Stiftleiste montiert ist. Mein Gedanke war, dass jeder das energy-display
auf Lochraster
nachbauen können sollte, ohne ein spezifisches PCB zu benötigen. Die Idee habe ich allerdings schon
wieder zu bereuen begonnen, weil "nur mal schnell" auf Lochraster aufbauen am Ende doch immer eine ganze Weile
dauert, da man ständig aufpassen muss, keine Verdrahtungsfehler zu machen.
Programmiert habe ich in MicroPython. Das ist eine Python-Variante für Mikrocontroller-Umgebungen, die auf Python 3.4 basiert, aber auch zahlreiche Features späterer Python-Versionen beinhaltet (zum Beispiel f-strings). Als Entwicklungsumgebung verwende ich Visual Studio Code mit der Extension MicroPico. Weiterhin kommt gelegentlich das Kommandozeilen-Werkzeug mpremote zum Einsatz, um einzelne Abläufe zu automatisieren (Dateien runterladen, Packages installieren, etc.).
Ein wesentlicher Erfolgsfaktor von Python besteht in den zahlreichen Packages für so ziemlich jeden Zweck.
Dieses Konzept gibt es auch bei MicroPython, wobei dort mit
mip ("mip installs packages")
gearbeitet wird. Die Pakete stammen entweder aus der
micropython-lib, oder aus anderen GitHub- oder
GitLab-Repositories, die direkt per URL angegeben werden können. In Version 2.0 benutze ich z.B. diese
zusätzlichen Packages: umqtt.simple
, pimoroni/phew
, ntptime
,
pathlib
, time
, logging
. Darüber hinaus sind zahlreiche Pakete
bereits fest eingebaut (z.B. math
, os
, json
, asyncio
).
Als Software-Architekt neigt man irgendwie dazu, Plattformen zu bauen. Ich wollte diesmal "nur" eine Anwendung bauen, aber irgendwie waren doch immer wieder Aspekte dabei, die dort raus wollten. Nach mehreren Umbenennungen und Refactorings kam ich bei dieser Hauptstruktur heraus (aufs Wesentliche gekürzt):
Alles was applikationsspezifisch ist liegt unter app/
und wird in eine Klasse
EnergyDisplay
aggregiert, die von runtime.Application
abgeleitet ist. Mit dem System
kommuniziert es über das Interface runtime.Runtime
, das einzelne Runtime-Komponenten
exponiert, wie z.B. einen MqttProvider
oder eine EndpointRegistry
(wobei dies auch nur
schlanke Interfaces der tatsächlichen Komponenten sind).
Unter runtime/
folgen ca. 10 weitere Packages mit Komponenten, die spezifische Aufgaben
erfüllen, wie z.B. clock
, das die Systemzeit via NTP aktuell hält und registrierte
Clients über Anpassungen informiert. Die Runtime selbst wird durch die Klasse AsyncioRuntime
gebildet, die wiederum alle Runtime-Komponenten aggregiert und u.a. die asyncio
-Hauptschleife
beinhaltet.
In config.json
steht die Konfiguration aller Komponenten, ebenfalls unterteilt in die beiden
Hauptzweige app
und runtime
, und darunter thematisch weiter untergliedert. Um genau zu
sein gibt es im Dateisystem anfangs nur eine defaults.json
, die dann verwendet wird, wenn es keine
config.json
gibt; das folgt einem Copy-On-Write-Ansatz, bei dem config.json
automatisch dadurch entsteht, dass von den Defaults abweichende Werte hinterlegt werden. (Allerdings auf
Datei-Ebene, es wird nicht zusammengeführt.)
main.py
hält schlußendlich alles zusammen, indem es die Instanzen anlegt und die
Applikation mit der konkreten Runtime bekannt macht. Normalerweise wäre das auch der Ort, an dem die ganzen
dreckigen Details sichtbar werden, allerdings stecken diese weitgehend in der Konfigurationsdatei, die hier
ebenfalls geöffnet und als ConfigView
-Instanz in die Komponenten rein gereicht wird.
Eine der Hauptaufgaben eines Software-Entwicklers besteht darin, die Abhängigkeiten einzelner Komponenten, Module, Arbeitsschritte, etc. zu identifizieren und richtig aufzulösen. Die Applikation will sich per MQTT auf ein Topic registrieren, der MQTT-Proxy muss zuvor aber mit dem MQTT-Broker verbunden sein, um die Subscribe-Nachricht verschicken zu können, und das alles funktioniert nur, wenn das WLAN verbunden ist. Dabei begegnen sich sowohl inhaltliche Abhängigkeiten (die Applikation muss den MQTT-Provider kennen, aber umgekehrt sollte der MQTT-Provider nicht von der Applikation abhängig sein) als auch zeitliche Abhängigkeiten (die bereits skizzierte Reihenfolgen-Thematik). Die inhaltlichen Abhängigkeiten bekommt man für gewöhnlich mit Interfaces in den Griff, die zeitlichen Abhängigkeiten mit Callbacks.
Ein Interface sollte möglichst schmal und inhaltlich zusammenhängend sein (in gehobeneren
Informatiker-Kreisen spricht man auch von
Kohäsion und
Interface Segregation, aber viel davon
ist gesunder Menschenverstand hochtrabend verpackt). Dazu habe ich jeder Komponente, die nach außen in
Erscheinung tritt, ein eigenes Interface verpasst, das nur das Notwendigste beinhaltet. Der bereits
erwähnte MqttProvider
hat beispielsweise die Methoden publish
,
subscribe
und unsubscribe
. Tatsächlich implementiert wird dieses Interface von
der Klasse MqttProxy
, die selbst mit einem "echten" Broker in Verbindung steht; ein Detail, das den
Benutzer des MqttProvider
-Interfaces gar nicht interessiert. Außerdem merkt sich der
MqttProxy
die Topic-Wünsche seiner Clients in einer eigenen Liste, die er abarbeitet, wenn die
Verbindung zum Broker später / erneut hergestellt wird.
Für die Veranschaulichung der Callbacks kommen mehrere Beispiele in Frage: die Verbindung zum WLAN wurde hergestellt, die Uhr wurde (erstmals) synchronisiert, die Konfiguration wurde geändert, oder auch: eine MQTT-Nachricht ist eingetroffen. Alle haben gemeinsam, dass ein "Vertrag" zwischen Anbieter und Nutzer geschlossen wird: Du rufst diese meine Funktion auf, wenn das und das geschieht. Damit lassen sich erstaunlich komplexe Abhängigkeiten abbilden, man muss nur etwas aufpassen, dass keine Rekursionen entstehen, und dass scheinbar harmlose Aufrufe lange dauern können, wenn eine ganze Abhängigkeitskette losgetreten wird. (Berühmtes Beispiel: Donald E. Knuth wollte seine Buchreihe The Art of Computer Programming schreiben und hat dafür zunächst das Textsatzsystems TeX entwickelt...)
Der Haupt-Treiber der Callback-Architektur war tatsächlich das Zulassen von Konfigurationsänderungen
zur Laufzeit. Nachdem ich einen API-Endpunkt zum Editieren der Konfiguration bereitgestellt hatte, erschien es
enorm unelegant, das Gerät neustarten zu müssen, damit z.B. eine geänderte zeitliche
Auflösung der Einspeisehistorie Wirkung zeigt. Viele Refactorings später kam ich bei einem Interface
ConfigView
heraus, das eine Ansicht auf einen bestimmten Teilbaum des JSON-Objekts aus
config.json
darstellt. Dabei kann jede Komponente eine solche Baum-Ansicht weiter
einschränken, z.B. kann Runtime den gesamten Zweig runtime
bearbeiten, aber um die Komponente
MqttProxy
zu konfigurieren einfach nur den Teilbaum runtime.mqtt
weiter reichen. Als
Benutzer eines ConfigView
-Objekts kann man sich für Änderungen registrieren und wird
fortan immer dann aufgerufen, wenn sich das JSON-Objekt geändert hat. (Allerdings bislang stumpf immer,
egal ob die Änderungen unterhalb dieses Teilbaums waren oder nicht.)
Die Komponenten selbst müssen sich ihre aktuellen Betriebswerte merken und bei einem Aufruf des
config_changed
-Callbacks prüfen, ob sie etwas zu tun haben. Wird z.B. der minimale Anzeigewert
der Batterieanzeige geändert (also wann die Batterie als leer gilt), dann muss die Anzeige mit dem
bestehenden, zuletzt empfangenen Wert und der neuen Randbedingung neu gezeichnet werden. Wird aber das Feld oder
Topic geändert, wäre es nur seriös den zuletzt empfangenen Wert komplett zu verwerfen und auf
einen neuen zu warten, bis wieder etwas dargestellt werden kann, weil es ja sonst gelogen wäre. Viel
einfacher hingegen ist der Fall bei der Display-Helligkeit: hier wird nur ein Faktor aktualisiert und im
gleichen Kontext passiert nichts weiter; die Änderung zeigt ihre Wirkung erst bei der nächsten,
zyklisch erfolgenden Display-Aktualisierung.
Auf den Konfigurations-Mechanismus bin ich ziemlich stolz. War ein zähes Biest, aber bislang gibt es keine Eigenschaft, für deren Änderung ein Neustart notwendig wäre (von den bislang nicht online editierbaren WLAN-Zugangsdaten abgesehen).
Das energy-display
ist mein erstes Projekt, bei dem ich einen Webserver integriert habe. Ich
verwende dazu das Package phew von Pimoroni, basierend auf einem
Tipp aus der Make. Zwar liefert der Webserver auch statische Seiten aus, aber seine Hauptaufgabe in meinem
Projekt besteht in der Bereitstellung von API-Endpunkten zur Interaktion mit dem Gerät. Um ganz genau zu
sein ist es derzeit nur der Endpunkt /config
der tatsächlich Änderungen verursachen kann,
die anderen Endpunkte sind eher informativer Natur:
Route | Methode | Wirkung |
---|---|---|
/config |
GET | Aktuelle Konfiguration darstellen |
PUT | Konfiguration durch neuen Inhalt ersetzen | |
DELETE | Default-Werte wiederherstellen | |
/syslog |
GET | System-Log darstellen |
/status |
GET | Status-Auskunft erteilen |
/version |
GET | Versions-Informationen darstellen |
Die Informationen werden immer als JSON-Objekte bereitgestellt bzw. entgegengenommen. Für eine etwas benutzerfreundlichere Darstellung habe ich ein paar Webseiten gebaut, die diese Daten per JavaScript abfragen (Fetch API) und darstellen. Ich bin kein Experte auf diesem Gebiet, aber es erschien mir einfach und ressourcenschonend (die meiste Arbeit entsteht beim Client). Der Nachteil ist die relativ enge Kopplung (die Status-Seite kennt z.B. den genauen Aufbau des JSON-Objekts; kommt ein neues Feld hinzu, muss die Seite ebenfalls angepasst werden), aber andererseits wäre diese bei der serverseitigen Erzeugung auch vorhanden, nur anderswo. Naja, und der DOM-Baum lässt sich schlechter untersuchen, wenn die Elemente dynamisch generiert werden; dafür sind "lokale" Themen wie die zu verwendende Zeitzone deutlich sauberer lösbar.
Bei der Latenz habe ich große Unterschiede gesehen. Wenn es schnell geht, dann kommt eine Antwort innerhalb von rund 40 ms, manchmal kann es aber auch bis zu 300 ms dauern (Abfrage eines API-Endpunkts; die komplette Darstellung einer Seite, die nach dem eigenen Laden noch den Inhalt nachlädt, kann auch mal 800 ms dauern). Im Durchschnitt liegt die Antwortzeit im Bereich von 100 bis 200 Millisekunden, wobei der Aufwand für die Beschaffung des Inhalts (Status-Provider abklappern, Syslog lesen) kaum ins Gewicht zu fallen scheint. Ich vermute die Latenz ist in der Ein- und Ausgabe zu suchen, und der Art wie hierbei das Scheduling der asyncio-Coroutinen erfolgt.
Logging ist in einem eingebetteten System immer so eine Sache: einerseits will man es, andererseits läuft
einem der begrenzte Speicher voll. Ich habe auf dem bewährten logging.Logger
-Ansatz von Python
aufgesetzt und einen RotatingFileHandler
gebaut, der die Log-Ausgaben in einem Set von
.ndjson
-Dateien sammelt. Dabei kann eine Maximalgröße konfiguriert werden, ab der eine
neue Logdatei begonnen wird, und eine Maximalzahl alter Logdateien, die aufgehoben werden sollen. Über den
API-Endpunkt /syslog
werden die Meldungen als ein zusammenhängendes Array aus JSON-Objekten
gestreamt (mit einem Generator; abgefahrene Sache), sodass diese Stückelung komplett verborgen bleibt.
Durch den logging.Logger
-Ansatz unterstützt das ganze auch die verschiedenen Level (Fehler,
Warnung, Info, etc.) und die darauf basierende Filterung (während der Entwicklung lasse ich z.B. Meldungen
der Stufe Debug mitprotokollieren, im produktiven Betrieb nur ab Info aufwärts). Dank zweier strategisch
klug platzierter Exception-Handler werden auch unvorhergesehene Fehlerszenarien "gefangen" und inklusive
Stacktrace gespeichert (sofern das System noch dazu in der Lage ist). Eine Webseite zeigt die JSON-Daten auch in
hübsch an und fürbt die Meldungen entsprechend ein (Debug-Meldungen grau, Fehler rot, etc.).
Parallel zum Logging existiert eine Status-Schnittstelle, die Auskunft über den Zustand der Runtime und der
Applikation gibt. Jede Komponente kann das Interface StatusProvider
implementieren, das die
Bereitstellung einer Eigenschaft status
fordert, die ein Dictionary zurückliefert. Ein solcher
Status Provider registriert sich bei einem Status Collector. Dieser wiederum kann über zwei Wege in
Erscheinung treten: per API-Endpunkt /status
kann der Status asynchron zu einem beliebigen
Zeitpunkt erfragt werden. Weiterhin kann der StatusPublisher
angewiesen werden, den Status zyklisch
abzufragen und als MQTT-Nachricht zu versenden. Letzteres eignet sich dann z.B. hervorragend, um via Telegraf /
InfluxDB / Grafana gesammelt und ausgewertet zu werden. Für die einfache Darstellung im Webbrowser gibt es
eine Webseite, die den API-Endpunkt befragt und die Antwort in aufbereiteter Form darstellt (optional mit
regelmäßiger Aktualisierung).
Eine Teilmenge der Status-Informationen sind die Geräte-Informationen, bereitgestellt durch den
DeviceMonitor
. Dieser gibt Auskunft über aktuelle Systemzeit, Uptime, Temperatur sowie
Auslastung von Arbeitsspeicher und Dateisystem. Der Device-Monitor war auch der erste Kunde der
ClockSyncer
-Komponente, die Datum und Uhrzeit per NTP abfragt und nachführt, denn mangels
Pufferbatterie startet das energy-display
immer am 1. Januar 2021 um 0 Uhr. Bis jetzt ist die
Uhrzeit zwar nur für die Logmeldungen wirklich relevant, wird aber auch zur Berechnung der Uptime genutzt,
indem die Uhrzeit des Systemstarts gemerkt wird; dieser wird dann rückwirkend und einmalig nach der ersten
Synchronisation angepasst, als Reaktion auf den clock_synced
-Callback, der das Delta mitbekommt, um
das die Uhr gestellt wurde. Es sind diese Kleinigkeiten, die nach und nach zu einem runden Gesamtbild
führen, aber auch den verfügbaren Speicher des Mikrocontrollers irgendwann zum Platzen bringen werden;
derzeit sind ca. 60% belegt, aber ich habe noch Pläne... (zur Not folgt der Umstieg auf den Pico 2 W, der
doppelt so viel Flash-Speicher hat).
So spannend eine Plattform an sich auch ist, eigentlich soll sie ja nur ein Hilfsmittel sein, damit die
Anwendung glänzen kann. Die Anwendung habe ich nicht in Packages untergliedert, sondern "nur" in einzelne
Module: averager.py
, bargraph.py
, battery.py
, brightness.py
,
curve.py
, display.py
, hwdef.py
, power.py
und
__init__.py
. Diese lassen sich in die Themengebiete Display-Ansteuerung, Darstellung und Logik
unterteilen.
Die Display-Ansteuerung (display.py
) erfolgt über neopixel.NeoPixel
, wobei ich
nicht direkt auf die Pixel schreibe, sondern einen internen Puffer mit den "echten" RGB-Werten unterhalte, die
in _flush
mit dem aktuellen Helligkeits-Faktor verrechnet werden. Dieser wird aus der vom
Fotowiderstand gemessenen Umgebungshelligkeit anhand einer Kennlinie (curve.py
) mittels linearer
Interpolation bestimmt. Der Grund dafür liegt in der Helligkeitswahrnehmung, die einem logarithmischen
Zusammenhang folgt (Stichwort
Weber-Fechner-Gesetz); das einfache
Multiplizieren mit einem direkt aus dem Helligkeitswert berechneten Faktor führt nicht zu einem brauchbaren
Ergebnis. Der Aufruf von _flush
erfolgt zyklisch mit der konfigurierten Aktualisierungsrate
(Default: 5 Sekunden), damit das Display nicht zu hektisch zappelt, wenn viele Aktualisierungen in kurzer Zeit
stattfinden.
Das Display kennt nur Pixel, und zwar auf Grund der Zusammenschaltung der beiden Module nur eine Kette aus 76
Pixeln, die sich "irgendwie" auf den Ring und auf die Matrix aufteilen. Dieses Wissen, also welcher logische
Index zu welchem physischen Pixel gehört, steckt in hwdef.py
in Form der Liste
CIRCLE_PIXELS
sowie der Liste aus Listen MATRIX_PIXELS
. Durch diese Indirektion ist
eine beliebige Zuordnung möglich, was insbesondere bei der Matrix sehr hilfreich ist, um die Zählweise
der Spalten ("alt" und "neu") und Zeilen ("viel" und "wenig") an den Anwendungsfall anzupassen, um dort die
Logik zu vereinfachen.
Die Darstellung steckt in bargraph.py
, wobei die Klasse BarGraph
in ihrem Konstruktor
eine Pixel-Liste erhält (praktischerweise genau in dem Format, das in hwdef.py
steckt...),
sowie eine Color Strategy, eine Callback-Funktion die aus einem Füllstandwert (z.B. 0.55) einen Farbwert
(z.B. Color.YELLOW
) ableitet. Diese BarGraph-Klasse wird sowohl für den Ring als auch
für die Matrix-Spalten genutzt (die Matrix ist dann eine Liste aus BarGraph-Instanzen). Ein BarGraph selbst
hat jeweils ein Minimum und ein Maximum sowie einen aktuellen Wert. Immer wenn dieser Wert (oder der
Wertebereich) aktualisiert wird, wird anhand von Minimum, Maximum sowie der Anzahl der zur Verfügung
stehenden Pixel ein Anzeigewert errechnet (z.B. 3.55 von 12), der dann mit Hilfe der Color Strategy in eine
Pixel-Folge wie grün, grün, grün, gelb umgesetzt wird.
Die eigentliche Applikations-Logik steckt in battery.py
, power.py
und
averager.py
. Letzteres umfasst die beiden Hilfsklassen SimpleAverager
(addiert Werte
auf und liefert das arithmethische Mittel) und BucketAverager
(bildet einen gleitenden Mittelwert,
wobei die Werte nach und nach verschiedene "Eimer" durchlaufen, bis sie am Ende raus fallen). Der
BatteryMonitor
registiert sich auf die MQTT-Botschaft für den aktuellen Batteriefüllstand
und bildet dessen nutzbaren Bereich auf den LED-Ring ab (wobei streng genommen die Hauptarbeit von
BarGraph
erledigt wird und BatteryMonitor
die Werte für soc_min
,
soc_max
und den jeweils aktuellen Füllstandwert nur weitergibt). Im PowerMonitor
steckt ungleich mehr Intelligenz: neben des MQTT-Handlings besitzt er einen SimpleAverager
um die
sekundenweise auftretenden Einzelwerte in größere Einheiten (je nach Konfiguration z.B. 30 Sekunden
oder 5 Minuten) zu aggregieren, die dann wiederum in einen BucketAverager
eingespeist werden, der
einen Bucket für jede Matrix-Spalte beinhaltet. Über die Parameter seconds_per_unit
,
units_per_column
, live_column_enabled
sowie display_threshold
lässt
sich das Verhalten individuell anpassen, letzteres beispielsweise unterdrückt die ständig auftretende
Über- und Unterkorrektur der Batteriesteuerung die dafür sorgt, dass immer ein paar Watt eingespeist
oder aus dem Netz bezogen werden, auch wenn der Wert eigentlich bei 0 liegen sollte.
Ich hatte mir bei diesem Projekt zur Angewohnheit gemacht, nach jeder Bastel-Session eine lauffähige
Version der Software auf mein jeweils aktuelles Testgerät zu spielen und mindestens bis zur nächsten
Session laufen zu lassen. Mit abnehmender Äderungsfrequenz entstanden immer ausgedehntere Langzeittests,
aber mitunter auch die Unsicherheit, welche Version genau jetzt getestet wird. Im Prinzip müsste es dem
letzten Stand im Git-Repository entsprechen, aber manchmal liegen doch ein paar Commits dazwischen, weil sich
(vermeintlich) nur Details geändert haben und ich dann einen Langzeittest nicht unterbrechen wollte (z.B.
um die Drift der Systemuhr genauer einschätzen zu können). Da nun mit der HTTP-API ein schöner
Weg existiert, Informationen abfragen zu können, ohne das System anhalten und inspizieren zu müssen,
lag es nahe auch einen Endpunkt /version
anzulegen. Der Endpunkt an sich war schnell angelegt, aber
um sicherzustellen, dass er immer die passende Information ausliefert, waren noch ein paar weitere Schritte
notwendig.
Zum einen muss immer die richtige Information erhoben werden, idealerweise direkt aus Git. Dazu habe ich ein
Skript geschrieben, das mit GitPython das letzte Release-Tag
aufsucht (z.B. v2.0
) und daraus die Major- und Minor-Version ermittelt (hier also 2 und 0).
Weiterhin wird betrachtet, wie viele Commits zwischen dem Release-Tag und dem aktuellen Commit liegen; diese
Zahl wird als dritte Stelle der Versionsnummer benutzt. Eine Version 2.0.24 gibt also an, dass 24 Commits seit
dem Release 2.0 vergangen sind. Weitere Informationen umfassen den Commit-Hash, das Datum, den aktiven Branch
sowie ob die Arbeitskopie "dirty" war (also Änderungen vorlagen, die nicht in besagtem Commit enthalten
sind).
Die nächste Hürde besteht darin, dass tatsächlich aufgespielte Software und angegebene
Software-Version garantiert konsistent sein müssen. Das habe ich dadurch erreicht, dass der Download auf
das Target durch ein Skript (pico-deploy
) gesteuert wird, das diese Versions-Information
aktualisiert. Jetzt muss man sich nur noch daran halten, nicht hinterrücks am Target rumzufummeln, und
schon passt's. Um dies zu begünstigen habe ich dem Skript noch ein paar mehr Funktionen spendiert, wie z.B.
das alte Syslog zu entfernen, die notwendigen Packages zu installieren, etc. sodass es am Ende zu einem "mach,
dass alles gut ist"-Button geworden ist, den man gerne nutzt. (Ein Button ist es tatsächlich noch nicht,
aber es lässt sich bestimmt auch in die IDE integrieren.)
Nachdem ich nun eh schon dabei war, alles zu automatisieren, habe ich auch noch ein Skript
make-release
gebaut, das ein Release-Paket erzeugt, indem es das aktuelle Git-Repository in eine
Arbeitskopie klont, auf ein bestimmtes Commit bringt, die Versions-Information aktualisiert, dann das
.git
-Verzeichnis wegwirft und alles in ein korrekt benanntes Tarball-Archiv verpackt. So erstmals
geschehen für Version 2.0.0, die natürlich noch ein paar Macken enthält, die erst mit
Version 2.1 gefixt sein werden (siehe Known Issues). Je dichter ich an das Ziel
"Version 2.0" kam, desto deutlicher wurde mir auch, wieso das scheinbar immer so ist: man muss einfach irgendwo
einen Schnitt machen und zu einem Feature oder komplexeren Bugfix sagen können "Du kommst erst in der
nächsten Version"; andernfalls wird man nie veröffentlichen und hat ein (hoffentlich) super-tolles,
fast-fertiges Produkt rumliegen, von dem nie jemand erfährt, weil es bei 99% stagniert (dem geschuldet,
dass sich das letzte Prozent während seiner Abarbeitung immer weiter ausdehnt).