Eine TP-Brücke ohne ETS: mehrere TP-Linien an einem Gerät, gekoppelt nach Regeln, die im Quelltext stehen. Kein Applikationsprogramm, kein Download, keine Inbetriebnahme - flashen, und es koppelt. Gedacht für Fälle, in denen eine Kopplung ohne ETS-Abhängigkeit gebraucht wird, typisch zum Debuggen.
Die Filterung ist damit übersetzt und nicht geladen: wer sie ändert, flasht. src/main.cpp ist die
Konfiguration, alles andere ist Mechanik.
pio run -e pico -t upload -t monitor
pio run -e esp32
Die tpuart-Library kommt über lib_deps aus ihrem Repo, der Stand steht in platformio.ini. Wer an ihr
selbst arbeitet, legt sie als Symlink nach lib/tpuart - siehe AGENTS.md.
Ein Interface ist der Weg zum Transceiver. Die Library bringt drei mit, dieses Projekt benutzt sie über
den gemeinsamen Basistyp TPUart::Interface::Abstract:
| Interface | Konstruktor | Anmerkung |
|---|---|---|
Interface::RP2040 |
(rx, tx, uart) |
Hardware-UART, Empfang per DMA. Der belastbare Weg. |
Interface::ArduinoSerial<SerialPIO> |
SerialPIO(tx, rx, fifo) |
PIO-UART auf beliebigen Pins. (tx, rx) — umgekehrt! |
Interface::ESP32 |
(rx, tx, uart) |
ESP-IDF-Treiber. |
TPUart::Interface::Abstract *interfaceTp1 = nullptr; // global nur der Zeiger
void setup()
{
interfaceTp1 = new TPUart::Interface::RP2040(17, 16, uart0);Als globales Objekt funktioniert Interface::RP2040 nicht: sein Konstruktor beansprucht einen
DMA-Kanal und beschreibt dessen Register, und das läuft vor main() — was der Arduino-Core danach an der
Hardware zurechtsetzt, sieht diese Konfiguration nicht mehr. Der Empfang bleibt dann tot, ohne jede
Fehlermeldung: der Zustand kommt nie über Uninitialized hinaus und rx bleibt bei 0 Byte.
SerialPIO und Interface::ESP32 dürfen statisch stehen, ihre Konstruktoren fassen keine Hardware an —
aber einheitlich spät anzulegen ist billiger als die Ausnahme zu merken.
Auf dem RP2040 sind die UART-Pins nicht frei wählbar: uart0 kann TX 0/12/16/28 und RX 1/13/17/29,
uart1 TX 4/8/20/24 und RX 5/9/21/25. SerialPIO kann jeden Pin; was dort am Bus aufgefallen ist, steht
in AGENTS.md.
Auf dem ESP32 prüft uart_set_pin() nicht, ob ein Pin frei ist. Liegt dort der eingebaute Flash oder
PSRAM, bootet das Board still im Kreis — ohne ein einziges Zeichen Ausgabe.
Eine Linie entsteht aus einem Interface, die Reihenfolge der Aufrufe bestimmt den Namen in der Ausgabe:
Line &tp1 = bridge.addInterface(*interfaceTp1); // heisst TP1
Line &tp2 = bridge.addInterface(*interfaceTp2); // heisst TP2Die Regeln einer Linie sagen, was HINTER ihr liegt — nicht, was sie durchlässt. Das ist der
Denkschritt, an dem alles hängt: tp1.permitUnicast("1.1") heißt „die Linie 1.1 ist über TP1
erreichbar", und daraus folgen Quittung und Weiterleitung.
Nur Koppler, also zwei Stellen — ein einzelnes Gerät ist keine gültige Angabe:
| Muster | Greift für | Eigene Adresse |
|---|---|---|
"1.1" |
1.1.0 … 1.1.255 |
1.1.0 (Linienkoppler) |
"1.0" oder "1.*" |
1.0.0 … 1.15.255 |
1.0.0 (Bereichskoppler) |
"*" |
jede physikalische Adresse | keine |
Eine Linie 0 gibt es deshalb nicht: 1.0 ist der Bereichskoppler. Die Koppleradresse der ersten
permit-Regel geht per setOwnAddress() an den Chip — dessen Auto-Quittung kennt genau eine Adresse,
alles Weitere entscheidet die Brücke selbst.
Je Stelle eine Zahl oder *:
tp1.permitBroadcast("12/*/12");
tp1.permitBroadcast("12/*/*");
tp1.permitBroadcast("*/*/10");
tp1.permitBroadcast("*"); // alleDreistufig, wie oben — die zweistufige Schreibweise (12/2000) nimmt der Parser nicht. Ein Muster mit
Tippfehler wird abgelehnt und gemeldet, nicht stillschweigend anders ausgelegt.
Platz ist für 8 Unicast- und 16 Broadcast-Regeln je Linie (MAX_UNICAST_RULES, MAX_BROADCAST_RULES);
eine Regel darüber wird abgelehnt und gemeldet.
tp1.denyBroadcast("12/*/*"); // erst die Ausnahme
tp1.permitBroadcast("*"); // dann alles UebrigeAndersherum geschrieben ist dasselbe deny toter Code — und das fällt nicht auf, weil beides
übersetzt und läuft. Passt keine Regel, ist die Linie für dieses Ziel nicht zuständig.
Der Kniff, und er folgt direkt aus „die Regeln sagen, was hinter der Linie liegt":
Kommt auf TP2 ein Telegramm herein, quittiert TP2 genau dann, wenn eine andere Linie das Ziel erlaubt — denn nur dann kann es weitergegeben werden.
Zwei Einschränkungen dazu:
- Unicast auf der Empfangslinie selbst wird nicht quittiert. Dort antwortet das Gerät.
- Gruppenadressen schon — die hören mehrere, und die Quittung ist auf TP1 ein verdrahtetes Oder.
Mit einer einzigen Linie wird nichts quittiert, es gibt kein Gegenüber. Der Hochlauf sagt das.
Gerechnet wird das beim Hochlauf für jede der 65536 Adressen; im Tick kostet die Entscheidung zwei Speicherzugriffe (Tabellen, ~768 Byte je Linie). Das ist nötig, weil die Quittung binnen 2,8 ms auf dem Bus sein muss und die Entscheidung auf dem RP2040 im Interrupt fällt.
tp1.setAcknowledgeAll(true);Umgeht die Regeln vollständig — auch Ziele auf der Linie selbst und solche, die keine andere Linie erlaubt. Das ist eine Lüge auf den Bus: der Absender hält sein Telegramm für angekommen, obwohl niemand es weitergibt. Zum Messen (Buslast, Timing, Wiederholungsverhalten) genau richtig, im Betrieb als Koppler falsch. Weitergeleitet wird davon nichts zusätzlich.
Dieselbe Regelauswertung wie bei der Quittung — sonst verspräche die Quittung eine Zustellung, die nicht stattfindet. Ein Telegramm geht in die Sendewarteschlange jeder anderen Linie, die es erlaubt und verbunden ist.
Nicht weitergegeben werden: kaputte Telegramme, eigene Echos, erkannte Wiederholungen, und alles zu einer Linie, die gerade nicht verbunden ist.
Der Hop Count wird heruntergezählt. Ohne das kreist ein Telegramm zwischen zwei Linien, bis ein
anderer Koppler es abwürgt — am Bus beobachtet als dasselbe Telegramm mit Hop 6, 4, 2, 0. Er steht im
Steuerfeld hinter den Adressen (Standard Byte 5, Extended Byte 1, Bits 6-4); 7 heißt „nicht zählen",
0 heißt verbraucht und wird verworfen. Weil das die Prüfsumme mitzieht und die Library sie beim
Einreihen prüft statt neu zu rechnen, geht eine Kopie mit eigener CRC auf die Reise.
Ein Zeichen je Befehl, kein Zeilenpuffer:
| Taste | Wirkung |
|---|---|
Enter |
Leerzeile als Marke im Mitlesen |
i |
Zustand und Regeln je Linie |
s |
die Zähler je Linie |
m |
Telegrammausgabe an/aus (Vorgabe: aus) |
h ? |
Kurzhilfe |
Zyklisch gibt die Brücke nichts aus. Kaputte Telegramme kommen unabhängig davon immer, in Rot.
m schaltet nur die Ausgabe — der Busmonitor-Modus des Chips wird nicht angefasst, der macht ihn
transparent und als Koppler unbrauchbar.
| Feld | Bedeutung |
|---|---|
forwarded in |
von anderen Linien übernommen |
rejected |
nicht angenommen: nicht verbunden oder Warteschlange voll |
hop exhausted |
hier hereingekommen mit Hop Count 0 |
no con |
gesendet, aber vom Chip nicht bestätigt |
peak tx |
Spitze der Sendewarteschlange — Rückstau |
max run / ack |
längster Tick, längste Quittungsentscheidung (µs) |
overflow … |
muss 0 bleiben |
setRepetitions(0, 0) heißt: jedes Telegramm geht genau einmal auf den Bus, statt nach der
Chip-Vorgabe bis zu dreimal. Das macht eine Mitschrift eindeutig — und reicht auch beim Koppeln, denn
Wiederholungen heilen keinen fehlenden Abnehmer. Wer auf eine Linie schiebt, auf der niemand zuhört,
bekommt keine Quittung, und dann eben dreimal keine. Die Frage ist die Filterung, nicht die
Wiederholungszahl.
setAcknowledgeAll(true) ist eine Präferenz, keine Betriebsart:
- aus — quittiert wird nur, was weitergegeben werden kann. Die Quittung sagt die Wahrheit, und ein
Telegramm ohne Abnehmer bleibt sichtbar unquittiert (
no conin der Statistik). - an — quittiert wird alles. Der Bus bleibt ruhig, weil fremde Sender nicht wiederholen; die Quittung behauptet dafür eine Zustellung, die von der Filterung nicht gedeckt ist.