Ein Server schickt eine Nachricht ins MeshCore-Netz. Ein
mosquitto_pubauf ein MQTT-Topic genügt – oder ein Textfeld im Browser. Dahinter hängt derselbe Node, der vielleicht schon als Observer mithört.
Der Observer hört zu und meldet, was er hört. meshinfra ist die Gegenrichtung: Meldungen aus der eigenen IT – Monitoring, Alarme, ein neuer Beitrag auf der Vereinsseite – gehen als Funknachricht ins Netz. Beides läuft am selben Node gleichzeitig, ohne dass am Observer-Aufbau etwas geändert wird.
Wer schon einen Observer betreibt, ist zwei Container davon entfernt.

Ein Observer-Node, der ganz normal im Kanal auftaucht. Die letzte Meldung trägt den Zusatz (sent by meshinfra) – daran erkennt man, dass sie nicht in dem Moment jemand getippt hat.
Die Schnittstelle ist MQTT. Alles, was auf ein Topic schreiben kann, kann ins Netz senden – ohne eine Zeile MeshCore-Code.
| Weg | Wie | Wofür |
|---|---|---|
| Sendefenster | Browser, http://<Stack-Host>:8080 |
von Hand tippen, ohne Terminal |
| Shell / Cron | mosquitto_pub |
Skripte, Backup-Meldungen, Tagesreport |
| Monitoring | Webhook oder MQTT-Ausgang | Zabbix, Uptime Kuma, Grafana Alerting, Prometheus Alertmanager |
| Home Assistant | MQTT-Integration, mqtt.publish |
Automationen, Sensoren, Türklingel |
| Node-RED | MQTT-out-Node | alles zusammenklicken, ohne Code |
| Eigener Code | jede MQTT-Bibliothek | Python, Go, Node, PHP – ein Publish genügt |
| CMS / Blog | Hook beim Veröffentlichen | neuer Beitrag → Ankündigung mit Link ins Netz |
| AI-Agent | Publish auf meshinfra/tx/chan |
siehe unten |
Ein Agent braucht keine Sonderanbindung – er ruft dieselbe Zeile auf wie jedes Skript. Praktisch heißt das: Ein Agent, der ohnehin ein Deployment überwacht, einen Text schreibt oder Logs auswertet, kann das Ergebnis als Funknachricht rausschicken.
Zwei Dinge sind dabei nicht verhandelbar:
Der Agent bekommt kein eigenes Rate-Limit und keinen eigenen Kanal für sich. Er liefert über dasselbe
infra-Konto ein, geht durch dasselbe Gate und ist an dasselbe Stundenkontingent gebunden wie alles andere. Das ist Absicht: Ein Agent, der in einer Schleife hängt, kostet dann Kontingent – nicht das Band.Was ein Agent schreibt, geht als Funknachricht an Menschen. 140 Zeichen, kein Zurückholen, kein Löschen. Auf den eigenen Automatik-Kanal, nicht auf den Gemeinschaftskanal.
Weil dasselbe Werkzeug beides kann, landet auch Empfangenes auf MQTT – Kanalnachrichten, Direktnachrichten, Verbindungsereignisse, Batteriestand, Adverts, Traceroutes, Telemetrie, Kontakte.
mosquitto_sub -h <Stack-Host> -u infra -P "$MQTT_PASS" -t 'meshinfra/#' -v
Damit lässt sich die andere Richtung bauen: eine Nachricht im Netz löst etwas in der Infrastruktur aus. Ein Schlüsselwort schaltet eine Automation, eine Direktnachricht fragt einen Status ab, ein Agent antwortet.
Das ist ein anderes Kaliber als Senden. Wer aus einer Funknachricht heraus etwas auslöst, baut eine unauthentifizierte Fernsteuerung – Kanalnachrichten kann jeder senden, der den Kanal hat, und bei Hash-Kanälen ist das jeder, der den Namen errät. Wenn überhaupt, dann über Direktnachrichten von bekannten Kontakten und mit einer Whitelist auf deiner Seite.
Über dieselbe Verbindung gehen mehr als Nachrichten – Gerätestatus abfragen, Batteriestand, Advert senden, Traceroute, Telemetrie anfordern, Namen setzen, An- und Abmeldung an einem Repeater. Nützlich, um den eigenen Node aus dem Monitoring heraus zu beobachten, statt ihn in der App nachzuschauen.
Die vollständige Liste der Kommandos und ihrer Payloads steht im Repo.
send_advertkostet Sendezeit und wird vom ganzen Netz gehört. Nicht in eine Schleife hängen.
Alert / Monitoring / Beitrag
│ mosquitto_pub -t meshinfra/tx/chan -m "..."
▼
┌───────────────────────────────────────────────────────────┐
│ Stack-Host │
│ │
│ mosquitto ── ACL trennt Einlieferung von Ausführung │
│ │ │
│ │ meshinfra/tx/# │
│ ▼ │
│ ratelimit ── max N/Stunde, Rest verworfen + geloggt │
│ │ │
│ │ meshinfra/command/send_chan_msg │
│ ▼ │
│ meshcore-mqtt ── 15 s Sendeabstand (eingebaut) │
└──────┬────────────────────────────────────────────────────┘
│ TCP 5000 (Companion)
▼
Heltec V3 ──────────────► 868 MHz Mesh
▲
│ zweite, unabhängige TCP-Verbindung
Observer-Host: meshcore-packet-capture (unverändert)
Das ist der Punkt, an dem das Ganze steht oder fällt – und er ist erprobt:
Ein Companion-Node nimmt zwei gleichzeitige TCP-Clients auf Port 5000. Observer-Bridge und meshinfra laufen nebeneinander, auf getrennten Hosts, mit getrennten Brokern und getrennten Topic-Prefixen.
Eine dritte Verbindung ist nicht mehr unauffällig: der laufende Bridge-Container quittiert das gelegentlich mit
health check failed, attempting recoveryund fängt sich binnen einer Sekunde wieder. Abfragen also sparsam, nicht im Dauerbetrieb.
Am Observer-Stack wird nichts geändert. Kein Reflash, keine neue Firmware, keine andere Konfiguration am Node – außer einem zusätzlichen Kanal, siehe unten.
Bricht der Observer nach dem Start von meshinfra ab und liefert keine Pakete mehr, dann trägt der Node die zweite Verbindung nicht. Das ist ein Grund, den Aufbau neu zu bewerten, und keiner, es mit Reconnect-Schleifen zu übertünchen.
Die Rolle „Observer" beschreibt den Stack, nicht das Gerät. Der Node ist und bleibt ein Companion – und ein Companion kann senden. Was ihn zum Observer macht, ist die Bridge daneben, die Gehörtes an den Analyzer meldet.
Zwei Dinge bleiben davon unberührt:
Was meshinfra sendet, geht über Funk ins Netz und wird dort ganz normal von allen gehört – auch vom eigenen Observer. Das ist kein Sonderweg an der Luftschnittstelle vorbei.
http://<Stack-Host>:8080 — Kanal auswählen, Text einfügen, senden.
Zeigt den Zeichenzähler, das Restkontingent der Stunde, den Verbindungszustand des Nodes und die zuletzt verworfene Nachricht.
Das Sendefenster ist kein Sonderweg: Es liefert auf dasselbe Topic ein wie ein Kommando von Hand und geht durch dasselbe Rate-Limit. Die Broker-Zugangsdaten bleiben im Container, der Browser bekommt sie nie zu sehen.
Zu lange Nachrichten werden abgelehnt statt gekürzt. Beim Ankündigen eines Beitrags fiele sonst genau der Link am Ende weg.
Kein Login. Wer im LAN ist, kann senden. Soll nur der Stack-Host selbst zugreifen:
WEBUI_BIND=127.0.0.1.
# Kanalnachricht, Standardkanal aus .env
mosquitto_pub -h <Stack-Host> -u infra -P "$MQTT_PASS" \
-t meshinfra/tx/chan -m "Backup fehlgeschlagen"
# Kanal explizit
mosquitto_pub -h <Stack-Host> -u infra -P "$MQTT_PASS" \
-t meshinfra/tx/chan -m '{"channel":1,"message":"Backup fehlgeschlagen"}'
# Direktnachricht — Empfänger muss in der Kontaktliste des Nodes stehen
mosquitto_pub -h <Stack-Host> -u infra -P "$MQTT_PASS" \
-t meshinfra/tx/direct -m '{"destination":"<Kontakt>","message":"Test"}'
Bei tx/direct ist destination Pflicht, Klartext ohne JSON wird verworfen.
Darauf sollte man sich einstellen, bevor man Alarme daran hängt:
Kein Bestätigungs-Topic. Ob ein Kommando durchging, steht nur im Containerlog. Das Status-Topic trägt den Verbindungszustand zum Node, nicht das Schicksal einzelner Nachrichten.
docker compose logs -f meshcore-mqtt | grep -E "successful|failed"
Wer Zustellung sicher wissen muss, baut das oben drauf – oder nimmt für Kritisches einen zweiten Weg.
| Was | Wirkung | |
|---|---|---|
| eingebaut | 15 s Sendeabstand | Abstand zwischen zwei Sendungen |
| eigener Layer | RATE_LIMIT_PER_HOUR |
Menge pro rollender Stunde |
Der Abstand allein deckelt die Menge nicht – ein Alarmsturm läuft dort ungebremst durch, nur langsamer. Deshalb der zweite Layer. Was darüber hinausgeht, wird verworfen statt gepuffert und gemeldet.
Das ist keine Bequemlichkeit, sondern die Bedingung, unter der wir das Band nutzen: 10 % Sendezeitanteil pro Gerät und Stunde → Rechtliches · Best Practises
Die 15 Sekunden nicht runterdrehen. Und set dutycycle 10 gilt am Node genauso wie überall sonst.
Drei getrennte Broker-Konten statt einem. Die ACL gibt dem einliefernden Konto Schreibrecht nur auf die tx/-Topics; auf die Kommando-Topics darf allein das Gate schreiben. Ein Publish direkt auf ein Kommando-Topic wird vom Broker stillschweigend verworfen – mosquitto_pub meldet trotzdem Erfolg, angekommen ist nichts.
Wer nur das Rate-Limit als Konfigurationswert baut, umgeht es beim ersten Debugging selbst. Als Rechtemodell nicht.
Die Trennlinie läuft nicht zwischen Mensch und Maschine, sondern zwischen wiederkehrend und einmal relevant:
| Was | Wohin |
|---|---|
| Alarme, Monitoring, alles Wiederkehrende | eigener Kanal |
| Neuigkeiten, die alle hören sollen – ein neuer Beitrag, eine Ankündigung | Gemeinschaftskanal, z. B. #kf |
Ein Alarmsturm auf dem Gemeinschaftskanal ist genau die Sorte Verkehr, gegen die das ganze Sendezeit-Kapitel geschrieben ist. Eine Nachricht, die alle angeht, gehört dagegen dorthin, wo alle mitlesen – auch wenn sie technisch über meshinfra rausgeht.
Kanalbelegung am Node, Beispiel:
| Slot | Name | Wofür |
|---|---|---|
| 0 | Public |
Gemeinschaftskanal – kein Dauerverkehr |
| 2 | eigener Kanal | Alarme und Meldungen aus der Infrastruktur |
Was nicht in dem Moment ein Mensch getippt hat, wird als solches gekennzeichnet. Ein Zusatz wie
(sent by meshinfra)am Ende der Nachricht genügt.
Das kostet ein paar Zeichen und erspart die Frage, ob am anderen Ende gerade jemand sitzt. Wer auf eine Maschinennachricht antwortet, soll wissen, dass er das tut.
Gilt für alles, was ein Skript, ein Monitoring oder ein Agent absetzt – unabhängig vom Kanal. Eine Ankündigung, die jemand von Hand ins Sendefenster tippt, braucht die Kennzeichnung nicht.
Bei 140 Zeichen Nutzlast zählt der Zusatz mit. Deshalb kurz halten und den Link nicht ans Ende einer Nachricht setzen, die knapp wird – zu lange Nachrichten werden abgelehnt, nicht gekürzt.
Hash-Kanäle sind nicht privat. Bei Namen mit
#ist das Secret die ersten 16 Byte vonsha256(name), inklusive des#. Wer den Namen kennt oder errät, liest mit. Nichts darüber senden, was nicht mitgelesen werden darf → KanäleDie Kehrseite ist bequem: Ein zweites Gerät braucht keinen Schlüsselaustausch, derselbe Name genügt.
Kanäle lassen sich über MQTT nicht abfragen oder setzen – die Kanalauswahl im Sendefenster wird von Hand gepflegt, das Setzen läuft direkt über die MeshCore-Bibliothek.
| Symptom | Ursache | Abhilfe |
|---|---|---|
mosquitto_pub meldet Erfolg, nichts passiert |
ACL greift – falsches Konto oder falsches Topic | mit dem Einliefer-Konto auf tx/, nicht auf command/ |
| Gar nichts kommt an, keine Fehlermeldung | Topic-Prefix in .env geändert, ACL nicht |
Mosquitto interpoliert keine Variablen – Prefix in der ACL nachziehen |
| Kommando kurz nach Neustart verpufft | 5 s Startup-Grace-Period | kurz warten |
| Nach jedem Neustart geht dieselbe Nachricht raus | irgendwo retain=true auf einem Kommando-Topic |
Retain muss aus bleiben; retained Kommando mit leerer Payload löschen |
| Passwort sieht richtig aus, wird nicht akzeptiert | Quoting-Falle in .env |
keine Anführungszeichen, kein #, kein $ im Wert – Passwörter rein alphanumerisch |
| Observer liefert nach dem Start nichts mehr | Node verträgt die zweite Verbindung nicht | Aufbau neu bewerten, nicht mit Reconnect-Schleifen übertünchen |
Die Quoting-Falle hat in diesem Projekt zweimal zugeschlagen – einmal am Node beim WLAN-Passwort, einmal beim Broker. Nach dem Start prüfen, dass der Container den vollständigen String sieht.
.env und die Passwortdatei gehören nicht ins Git.ARCHITECTURE.md erklärt Sende- und Empfangsweg, Rechtemodell und Airtime-BudgetStart · Schnellstart · Funkeinstellungen · Kanäle · Geräte · Rollen · Repeaterliste · Repeater Setup Guide · Observer · meshinfra · Karte · FAQ · Glossar · Mitmachen · Alle Themen