Theo Döllmann, BFDler, 18. Juli 2022
Im KI-Makerspace haben wir schon länger 2 Probleme:
Für letzteres hatte Gregor schon einmal versucht meine Begeisterung zu wecken und etwas dafür zu basteln, aber ich hatte damals kaum Zeit und das Projekt hatte mich auch etwas abgeschreckt: Personen zu zählen ist gar nicht so einfach. Möchte man es zuverlässig machen, brauch man eigentlich eine Kamera oder wenigstens eine kleinen LIDAR (Wie hier beschrieben).
Jedenfalls saß ich dann an irgendeinem Donnerstagvormittag daheim und dachte plötzlich: Warum nicht einfach einen Arduino an das Klingelsystem des Hauses anschließen. Dann könnte der Arduino als Lichtschranke fungieren und jedes Mal klingeln, wenn jemand vorbeiläuft.
Ich fand die Idee einfach den Klingeldraht kurzzuschließen so witzig, dass ich direkt wusste, was die diesen Abend bei meiner Spätschicht machen würde. Zu diesem Zeitpunkt hatte ich noch gar nicht so richtig vor, damit auch noch Personen zu zählen.
Ich bin also am Abend im KI-Makerspace und fange direkt an, die Klinge auseinander zuschrauben. Das war gar nicht so einfach, da ich in meinem Leben noch keine Lichtschalter (der bei uns als Klingelschalter fungiert) auseinandergenommen hatte. Aber auch das hat nach einer Weile geklappt. Dahinter habe ich eine Vielzahl an dünnen Kabeln gefunden. Die hat man die beiden Kabel, die zuvor mit dem Schalter verbunden waren, herausgenommen und kurzgeschlossen, hat es geklingelt. Alles so, wie ich es mir vorgestellt hatte. Mit ein paar Wago-Klemmen habe ich mir die zwei Drähte aus dem Klingelkasten herausgelegt. So konnte ich die Klingel wieder einbauen. Ich wollte ja nichts zerstören und die Klingel hat glücklicherweise auf einfach weiter getan. Und zwar egal, ob man auf den Klingelknopf gedrückt hat, oder ob man die beiden Drähte miteinander verbunden hat.
Jetzt ging es darum, diese Klingel automatisch auszulösen. Dafür habe ich mir einen Arduino Mega geschnappt. Wir hatten diese mal für einen Kurs angeschafft und daher nicht nur die Arduinos selber da, sondern auch noch einige Sensoren passend dazu. Weil ich mich damit schon einigermaßen auskannte, habe ich mir direkt den HC-SR04 Ultraschall Entfernungsmesser geschnappt. Dazu ein Relay, um die Klingel zu betätigen. Ein Mosfet wäre schöner gewesen, aber mit Mosfets hatte ich bisher eher schlechte Erfahrungen gemacht (ich habe für meinen 3D-Drucker schon einige durchgebrannt). Und bei einem Relay kann man wirklich nicht viel kaputtmachen. Ein Programm, um das Relay auszulösen, war schnell geschrieben. Alles angesteckt und ausprobiert: Hat einwandfrei geklappt, so wie man sich das wünschen würde. Nur eine Sache hat noch gefehlt. Wenn das jetzt im Dauerbetrieb laufen sollte, bräuchte es sicher noch einen Filter, um nicht bei jeder falschen Messung die Klingel auszulösen.
Mit Filtern kenne ich mich leider gar nicht aus. Also habe ich etwas im Internet gesucht, ein paar Videos zu dem Thema geschaut und war immer noch nicht viel schlauer. Die meisten Filter funktionieren nur, wenn die Messungen mit einer festen Frequenz gemacht werden. Das ist aber bei mir nicht der Fall. Wenn der gemessene Abstand vom Sensor kleiner ist, das war der Schall kürzer unterwegs und die Messung damit schneller. Nach weiterem Suchen bin ich über einen Filter gestoßen, bei dem ich immer noch nicht weiß, wie er heißt. War aber so einfach zu implementieren, dass ich nicht nein sagen konnte
double filter(double value, double prev_value, double trust_factor=0.3) {
return value * trust_factor + prev_value * (1 - trust_factor);
}
Nachdem das alles ganz gut geklappt hatte, dachte ich mir dann: Wenn ich jetzt den Arduino durch einen ESP ersetze, könnte ich einfach bei jedem auslösen der Klingel eine Nachricht an irgendeine Datenbank schicken. Damit hätte man zwar keinen besonders guten Personenzähler (kann nicht unterscheiden, ob jemand hereinkommt oder herausgeht), aber immerhin besser als nichts. Das hat das ganze Projekt aber auch gleich deutlich komplizierter gemacht: Jetzt brauche ich nicht nur den Mikrocontroller, sondern auch noch eine Datenbank und da man sich die Daten auch anschauen können soll ein Frontend. Am besten in unserer bestehenden Typo3 Website.
Nach einigem hin und her denken habe ich mich dann für folgenden Systemaufbau entschieden:
Was genau die einzelnen Komponenten jetzt am Ende tun, erläutere ich im folgenden im Detail
Den Arduino habe ich jetzt durch einen ESP8266 (hatten wir gerade herumfliegen) ersetzt. Wie gehabt misst dieser den Abstand in einer Ultraschall-Lichtschranke. Sobald dieser unter ca. einem Meter ist, löst der ESP die Klingel aus und sendet jetzt zusätzlich eine HTTPS PUT Request an die Python API. PUT ist im Vergleich zu GET oder POST eher unbekannt. Bedeutet aber einfach "put", zu Deutsch: "lege ab". Also ich gebe dir diese Daten hier, und die Antwort darauf interessiert mich nicht.
Dieser Teil war beim ESP trotzdem der schwerste. Nicht weil ein PUT schwer ist, sondern weil
Das Problem habe ich als Python-Programmierer erst gar nicht verstanden, weil es in Python einfach immer funktioniert. In Python sähe der Teil ungefähr so aus:
import requests
url = 'https://home.ki-maker.space/personcounter_api/inc'
requests.put(url, data={'api_key': 'my_secret_hehehe'})
Aber die Internetbibliotheken für den ESP unterstützen standardmäßig gar kein HTTPS. Und der Workaround ist, dass man das SSL Zertifikat der gewünschten Domain auf dem ESP speichert, aber das Zertifikat der API wird mit Let's Encrypt generiert und ändert sich ab und zu. Also auch keine Lösung. Am Ende musste ich so blöd es ist die Zertifikat-Validierung auf dem ESP einfach aus schallten.
Ich habe dem ESP noch einen Knopf gegönnt, mit dem man z.B. für Events die Klingelfunktion abschalten kann. Dann habe ich das ganze auf einer Platine verlötet und ein Gehäuse in Onshape designt und ausgedruckt.
Den gesamten Code für den ESP habe ich auf GitLab geladen: https://gitlab.com/kimakerspace/personcounter-esp/-/blob/main/personcounter_esp.ino
Beim Backend war mir sofort klar, dass ich Python und Flask verwenden würde. Mit beidem kenne ich mich bestens aus. Eigentlich verwendet man für die Produktion kein SQLite, sondern etwas Richtiges wie Postgresql, aber für ein so kleines Projekt fand ich eine richtige Datenbank trotzdem übertrieben.
Mithilfe von SQLAlchemy war schnell eine Tabelle in der Datenbank angelegt
class Counts(db.Model):
id = db.Column(db.Integer, primary_key=True)
datetime = db.Column(db.DateTime)
def __init__(self):
self.datetime = datetime.now()
Die API Route, die der ESP regelmäßig aufrufen soll, war genauso einfach
@api.route('/inc', methods=['PUT'])
def increment_count():
if current_app.config['API_KEY'] != request.headers.get('X-API-KEY'):
return make_response('Unauthorized', 401)
else:
count = Counts()
db.session.add(count)
db.session.commit()
return 'OK'
Damit nicht jeder einfach unseren Counter hochzählen kann, muss erst noch ein API Key überprüft werden. Aber wenn dieser stimmt, kann einfach ein neues Count Objekt der Datenbank hinzugefügt werden. Wie oben zu sehen ist, speichert die __init__
Methode der Klasse automatisch das aktuelle Datum und die aktuelle Uhrzeit.
Das einzige neue in der API war für mich der Code um die Daten aus der Datenbank zu holen, zu aggregieren und an das Frontend zu senden. Normalerweise hätte ich das mit Pandas gemacht, aber ich war vor einer Weile mal über eine andere Bibliothek namens Polars gestolpert. Diese ist in Rust geschrieben und deutlich, deutlich schneller als Pandas. Drum wollte ich die Bibliothek schon länger mal ausprobieren. Statt dem mir bekannten Pandas groupby
musste ich Polars grouby_dynamic
Funktion verwenden. Aber der Rest kam mir trotzt neuem Framework sehr bekannt und einfach vor. Vermutlich werde ich in Zukunft öfters mal Polars verwenden. Kann ja nicht schaden.
@api.route('/count', methods=['GET'])
@ttl_lru_cache(seconds_to_live=60*60)
def get_count():
counts = pl.read_sql('SELECT * FROM counts', current_app.config['CON_X_DATABASE_URI'])
counts = counts.groupby_dynamic("datetime", every="1d").agg(pl.col("id").count())
counts = counts.rename({'id': 'count'})
three_weeks_ago = datetime.now() - timedelta(days=7*3)
counts = counts[counts.datetime > three_weeks_ago]
return jsonify(counts.to_dict(as_series=False))
Die eine interessante Sache an dieser Funktion ist noch Zeile 2:
@ttl_lru_cache(seconds_to_live=60*60)
Das ist ein zeitabhängiger Cache decorator (Details), den ich mal für mein Zugverspätungsprojekt entwickelt habe. Dieser cached in diesem Fall das Ergebnis der Funktion get_count
für eine Stunde. Das mache ich, weil die Berechnungen von get_count
eher rechenintensiv ist. Normalerweise sollte das gar kein Problem sein, aber wenn man irgendein Script Kiddie versucht die API zu DDOSen, dann gibt die Internetleitung hoffentlich vor dem Webserver auf.
Das sollte eigentlich die ganze Geschichte zur API gewesen sein. Der Code liegt auch auf GitLab (https://gitlab.com/kimakerspace/personcounter-api/) und wird dort automatisch in einen Docker-Container verwandelt und dann auf unserem kubernetes Cluster deployed. Aber so einfach sollte es nicht sein. Der Container wollte in dem Cluster einfach nicht starten. Nach 2h Recherche stellte sich heraus, dass der Server so alt ist, dass ihm eine CPU Instruktion zum Ausführen eines WSGI Webserver fehlt. Also habe ich von gunicorn auf den Flask Development Server gewechselt (was man nie machen sollte, in die Produktion gehört kein Development Server), aber das hat nichts geholfen. Letztendlich musste ich das ganze Cluster auf einen etwas moderneren Rechner umziehen. So ist es halt manchmal. Hat mich auf jeden Fall gut einen Tag an Zeit gekostet. Aber jetzt läuft es immerhin.
Bisher habe ich immer eigenständige Webapp mit dem Framework VueJs gebaut, die ich dann auf irgendeinem Webserver deployen konnte. Das hätte ich hier auch fast gemacht, weil ich einfach gar keinen Bock hatte, etwas mit jQuery oder so in Typo3 zu integrieren. Ich hatte dann aber doch eine Idee: Das Framework Svelte verwendet keine Runtime, sondern wird zu nativem Javascript compiliert. Dadurch kann ich eine winzige Webapp bauen, die nur ein Diagramm anzeigt und diese App dann einfach compilieren und die einzige daraus entstandene HTML Datei in Typo3 einfügen. Und so habe ich es auch gemacht. Außerdem wollte ich Svelte lernen, aber das war nur ein Nebengrund .
Das Frontend besteht also wirklich nur aus einer kleinen Komponente, die das /count
von der API abruft, und das mithilfe von ChartJs in ein Diagramm zeichnet. So einfach kann es gehen. Das Ergebnis kann sich, denke ich, sehen lassen.
Natürlich gibt es auch für das Frontend den Code auf GitLab: https://gitlab.com/kimakerspace/personcounter-frontend