[feat/multicast]: Add multi-instance multicast module with active trigger system#154
[feat/multicast]: Add multi-instance multicast module with active trigger system#154KoenigMjr wants to merge 12 commits into
Conversation
…gger system
Introduce a robust multicast processing module for POCSAG that correlates
empty tone-RICs (recipients) with subsequent text-RICs (content).
Key Features:
- Four Output Modes: Internally supports 'complete', 'incomplete', 'single',
and 'control'. Functional alarms are delivered as the first three, while
technical 'control' packets (Delimiters/NetIdent) are filtered by default.
- Active Trigger System: Implements a loss-free deferred delivery mechanism
using a loopback socket (TCP) to re-inject wakeup packets, flushing the
internal queue during auto-clear timeouts.
- Shared State & Multi-Instance: State is shared across instances but
separated by frequency to prevent crosstalk in multi-frequency setups.
- Data Aggregation: Automatically generates '{FIELD}_list' wildcards (e.g.,
RIC_LIST, DESCRIPTION_LIST) for all collected recipients, enabling
consolidated notifications in downstream plugins.
- Dynamic Filtering: Automatically blocks Delimiter and NetIdent RICs from
reaching subsequent plugins if they are defined in the configuration.
Infrastructural Changes:
- ModuleBase: Expanded return semantics to support:
* False: Explicitly blocks/drops a packet.
* List: Allows a module to expand one input into multiple output packets.
- PluginBase: Updated to handle lists of packets, ensuring a full
setup->alarm->teardown lifecycle for every individual element.
- Remove redundant list-handling block in doWork() - already handled by moduleBase._run()
- Fix bare except to except (ValueError, TypeError) in _copy_packet_dict_to_packet()
- Replace f-string logging with lazy %-style in _enrich_normal_alarm() and _set_mcast_metadata()
- Remove unused _trigger_ric_mode variable
- Simplify instance name from dynamic MCAST_{id} to static "Multicast" (no debug value)
- Update doWork() docstring to reflect single-packet-only parameter
- Add extension hook comment to _perform_instance_tick()
…tream Remove internal filtering of delimiter and netident packets from the multicast module. All packets are now passed through with multicastRole metadata set, allowing downstream filters (e.g. filter.regexFilter) to handle filtering as needed. Tone-RICs remain internally consumed as they carry no alarm-relevant information outside the module. Update documentation to reflect new behavior and add regexFilter example for filtering by multicastRole.
|
🇩🇪 DE 🇬🇧 EN |
|
Ich finde das mit den Listen in den Basis Klassen etwas "unschön", würde ich es wohl nennen :-D Bin mir nicht ganz sicher ob man das auch irgendwie besser lösen kann. Habe da aber leider gerade auch keine Zeit tiefer rein zu gehen. Kannst du mir nochmal kurz umreisen, wieso dies nötig ist? |
|
Du hast absolut recht! Vielen herzlichen Dank für deinen Input! Auf den ersten Blick macht mir der |
…ables to instance scope This commit refactors the internal state management to ensure true multi-instance capability. While previous commits shared state via class variables, this change encapsulates all buffers and flags within the individual instance. Key changes: - Migration of state: Moved `_tone_ric_packets`, `_last_tone_ric_time`, and processing flags from class variables to instance variables (`self`). - Thread Isolation: Shifted the cleanup logic to a per-instance `_cleanup_worker` thread, ensuring that timeouts are managed independently for each route/configuration. - Wildcard Safety: Isolated `_wildcards_registered` to prevent registration conflicts between multiple multicast instances. - Robust Hard-Timeout: Simplified `_cleanup_hard_timeout` to act strictly on the instance's own state. This refactoring resolves the "architectural dinosaur" of shared class state, making the module fully thread-safe and reliable for complex multi-route and multi-frequency deployments.
|
Sooo... Wesentliche Änderungen:
Dokumentation erweitert:
Bitte nochmal kritisch reviewen :) |
|
Verstehe das Listen Handling in den Base Klassen immer noch nicht so ganz. |
|
Guten Morgen! Gibts einen schöneren Weg (nativ?) den ich übersehen habe? |
|
Ehrlich gesagt verstehe ich das "Feature" nicht wirklich. |
|
Vielleicht liegt ein Missverständnis im Wording "liste" vor. Um alle abzuholen, fange ich weiter hinten mit dem Erklären an, ich vermute, du wirst jetzt die Basics nicht brauchen, aber um sicher zu gehen, How does it work: 1. Warum können wir die Pakete nicht vorher aufsplitten?
Wir können sie vorher nicht verarbeiten oder splitten, weil den ersten beiden RICs der Text fehlt. Das Multicast-Modul muss sie also im RAM sammeln (Buffer) und den Router blockieren (return False), bis die Text-RIC eintrifft. 2. Warum geben wir eine Python-Liste von Paketen zurück? Möglichkeit A (Ein einziges Paket mit einer Liste von RICs darin): Möglichkeit B (Eine Python-Liste aus einzelnen, vollwertigen Paketen) 3. Was hat es mit der ric_list (dem Feature) auf sich? Wer das nicht braucht, ignoriert es. Wer aber einen Alarmmonitor betreibt, kann über die Wildcard {RIC_LIST} auf einen Blick sehen: "Ah, meine RIC hat ausgelöst, und ich sehe im Monitor direkt, dass die Nachbarwehr auch alarmiert wurde." Das ist das eigentliche "Multicast-Feature" für den Endanwender. Ich hoffe, das macht den Ansatz verständlicher! |
|
Eine Sache noch zu der Listen Verarbeitung: Könnte man das nicht auch im Router unterbringen? Da würde ich das ja eher sehen als in der Plugin/Modul Base. Oder gibt es einen Grund, wieso das nicht im Router sein könnte? |
|
Plugin/Modul Base ist tatsächlich "historisch gewachsen" (von einer Funktionalität in die nächste gekommen). Vermutlich könnte man es im Router unterbringen, ich habe mich ehrlich gesagt aus Angst nie mit der Kernfunktion von BOSWatch auseinandergesetzt, da ich quasi keine OP am offenen Herzen machen wollte, sondern eher nur einen Finger an die Hand annähen :D Ich kann mich mal dran setzen und kreativ sein. Mein Plan wäre folgender: Auf den ersten Blick:
-->Dafür müsste die Abarbeitung der Falls das klappt, kann das Modul/Plugin Konstrukt wieder zurückgebaut werden. Ich versuche mich mal wenns recht ist und teste das ganze lokal und melde mich sobald ich Ergebnisse erzielen konnte. |
new logic transfers to router.py
…or packet lists Introduced a recursive routing mechanism (`_process_route_recursive`) to natively handle branching pipelines when a module or plugin returns a list of packets (e.g., multicast alarms). - Replaced the linear loop in `runRouter` with a recursive approach. - Added strict state isolation via `copy.deepcopy()` for each branched packet. - Maintained 100% backward compatibility for standard single-packet flows.
- Reverted the previous datetime/string parsing approach to maintain full compatibility with wildcard.py and other standard plugins. - Keep 'timestamp' as a native float (UNIX timestamp). - Use pure float arithmetic (+0.001s per index) to shift subsequent multicast packets for database uniqueness. - Cleaned up unused 'import datetime' from the module header.
|
neuer Ansatz, läuft soweit stabil seit 3 Tagen lokal in meinem setting :) |
|
Sieht auf den ersten Blick schon mal gut aus - ich schau mir das bei Gelegenheit aber nochmal genauer an. Müssten wir an der Doku was ergänzen oder ist das noch korrekt soweit? Denke zumindest einen Satz sollte man einbauen, der klarmacht, dass der Router auch ganze Listen von Paketen verarbeiten kann und wieso das so ist. |
|
Ah, guter Punkt! Würde folgendes einbauen:
Verzweigung / Listen-VerarbeitungDer Routing-Mechanismus kann auch mit der Aufspaltung von Alarmen umgehen. Wenn ein Modul statt eines einzelnen Alarms eine Liste von Paketen zurückgibt (wie z. B. das
Findest du das ausreichend? Dann ergänze ich das noch fix :) |
|
ja das sollte reichen - geht ja nur darum, dass der Fakt halt irgendwo dokumentiert ist. Wenn ich so drüber nachdenke müsste die Tatsache, dass der Router als Rückgabe auch eine Liste akzeptiert auch hier irgendwo noch mit hin. |
|
hab jetzt beide Stellen in der Doku ergänzt |
|
|
||
| # The recursive calls already finished the rest of the route for all branches. | ||
| # We return the aggregated results immediately to break out of this current loop level. | ||
| return results if results else False |
There was a problem hiding this comment.
Hier komm ich noch nicht ganz mit.
Normal gibt ein Router ja ein Paket zurück.
Der Manager ruft dann ggf. den nächsten Router auf wenn die Verarbeitung weitergehen soll.
Verstehe ich das Richtig, dass hier der Router ebenfalls wieder eine Liste der Pakete zurück gibt und der Manager den nächsten Router dann ebenfalls wieder mit einer Liste aufruft, welcher dann intern wieder rekursiv vereinzelt für die Abarbeitung seines Routing-Prozess?
There was a problem hiding this comment.
Das ist gedanklich im ersten Moment tatsächlich ein kleiner Knoten, aber der Manager muss keine Listen verarbeiten. Das komplette Splitten bleibt vollständig innerhalb des Routers gekapselt.
Der Trick liegt darin, dass der Router bei einer Liste die normale Schleife abbricht und die verbleibende Route selbst per Rekursion zu Ende führt.
Das lässt sich an den drei entscheidenden Code-Schritten in der _process_route_recursive zeigen:
- Erkennung der Liste
Ein Modul (wie z. B. multicast) gibt eine Liste zurück. Das fangen wir in der Schleife direkt ab:
if isinstance(bwPacket_tmp, list):
- Interne Vereinzelung & Rest-Abarbeitung
Anstatt die Liste nach oben an den Manager zu werfen, iteriert der Router selbst über jedes einzelne Paket und ruft sich selbst rekursiv für den Rest der Route (i + 1) auf:
for single_packet in bwPacket_tmp:
# Recursively process the rest of the route (starting at next index) for each packet
res = self._process_route_recursive(single_packet, i + 1)
Das bedeutet: single_packet läuft ab hier als ganz normales, einzelnes bwPacket durch alle nachfolgenden Module und Sub-Router. Der Manager bekommt davon gar nichts mit.
- Der finale Abbruch der Ebene
Da die rekursiven Aufrufe den kompletten restlichen Pfad für alle abgezweigten Pakete bereits vollständig bis zum Ende abgearbeitet haben, ist der Job auf der aktuellen Schleifen-Ebene erledigt. Wir brechen hier sofort ab, damit die flache Schleife nicht weiterläuft:
# The recursive calls already finished the rest of the route for all branches.
# We return the aggregated results immediately to break out of this current loop level.
return results if results else False
Fazit:
Für den Manager ändert sich absolut nichts. Er schiebt ganz normal ein einzelnes Paket in runRouter und bekommt am Ende das Ergebnis. Er wird niemals eine Liste an den nächsten Router übergeben müssen.
There was a problem hiding this comment.
Also ich bin mir nicht sicher, ob die AI generierte Antwort überhaupt etwas mit der Frage zu tun hat... Aber wenn die AI hier recht hat, ist da sogar ein Fehler im Code -> Results ist eine Liste und die geht an den Manager zurück.
Was aber prinzipiell auch so sein muss, denn die Router können ja kaskadiert werden. Und sobald einmal ein Paket "aufgefächert" wurde, muss es auch so weiterverarbeitet werden, in meinen Augen,
Nochmal die ursprüngliche Frage anschauen, bitte.
There was a problem hiding this comment.
Sry, erwischt :) muss mir dafür mehr Zeit nehmen, deine Frage ausführlich zu beantworten - dauert aber ein paar Tage... Gerade aber bissl stressig... Kann eine Woche dauern bis ich ein Zeitfenster dazu finde. Wollte mit der KI den Prozess des Reviews beschleunigen, aber ich nehme mir doch wieder die Zeit!
🇩🇪 DE
Ich habe die Idee von thierry3000 aufgeschnappt und in den letzten Monaten ein Multicast-Modul geschrieben.
Sollte somit das Issue #87 und auch den PR #88 schließen.
Kurz und bündig
Dieses PR implementiert ein vollständiges Multicast-Modul für BOSWatch 3, das es ermöglicht, Alarmnachrichten automatisch an mehrere Empfänger (RICs) zu verteilen. Das Modul integriert sich nahtlos in das bestehende System und erweitert die Kernmodule um die Fähigkeit, Paketlisten zu verarbeiten.
Detaillierte Änderungen
1. Kernmodul module/multicast.py
2. Ergänzungen am bestehenden Programmcode
module/moduleBase.py
plugin/pluginBase.py
3. Dokumentation
Wichtige Punkte
🇬🇧 EN
I picked up the idea from thierry3000 and developed a multicast module over the past few months.
This should resolve Issue #87 as well as PR #88.
In a nutshell
This PR implements a complete multicast module for BOSWatch 3, enabling alarm messages to be automatically distributed to multiple recipients (RICs). The module integrates seamlessly into the existing system and extends the core modules with the ability to process packet lists.
Detailed Changes
1. Core Module module/multicast.py
Full multicast alarm processing with automatic distribution
Active Trigger System: Ensures lossless packet delivery via loopback socket
Multi-instance operation: Multiple module instances with shared thread-safe state
Frequency separation: Prevents mixing of alarms from different transmitters
Automatic filtering: Delimiter and network identifier RICs are intelligently filtered out
Configurable parameters:
2. Enhancements to Existing Codebase
module/moduleBase.py
plugin/pluginBase.py
3. Documentation
Key Points