[feat/multicast] refactor: move packet filtering from module to downstream

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.
This commit is contained in:
KoenigMjr 2026-03-28 15:41:54 +01:00
parent 1ebcbf23e9
commit 08d09b4f50
2 changed files with 74 additions and 73 deletions

View file

@ -31,14 +31,14 @@ Das Modul unterstützt:
- Mehrere Startmarker (Delimiter) - Mehrere Startmarker (Delimiter)
- Mehrere Text-RICs - Mehrere Text-RICs
- Netzident-RIC zur Paketfilterung - Netzident-RIC zur Paketmarkierung
- Automatische Bereinigung alter Tone-RICs (Fehlerfall: Auto-Clear) - Automatische Bereinigung alter Tone-RICs (Fehlerfall: Auto-Clear)
- Active Trigger System zur verlustfreien Paketauslieferung - Active Trigger System zur verlustfreien Paketauslieferung
- Wildcards für spätere Weiterverarbeitung - Wildcards für spätere Weiterverarbeitung
- Frequenz-basierte Trennung - Frequenz-basierte Trennung
- Multi-Instanz-Betrieb mit geteiltem Zustand - Multi-Instanz-Betrieb mit geteiltem Zustand
Hinweis: Der Delimiter-RIC (0123456) wird automatisch gefiltert und nicht ausgegeben. Hinweis: Der Delimiter-RIC (0123456) wird mit multicastRole: delimiter markiert und durchgereicht. Downstream-Filter (z.B. filter.regexFilter) können ihn bei Bedarf ausfiltern.
**Wichtig:** Die Text-RIC (Message-RIC) wird **nicht als separates Paket ausgegeben**. Sie dient nur als Nachrichtenträger, der seinen Text an alle gesammelten Tone-RICs verteilt. Falls keine Tone-RICs vorhanden sind, wird die Text-RIC als `multicastMode: single` ausgegeben. **Wichtig:** Die Text-RIC (Message-RIC) wird **nicht als separates Paket ausgegeben**. Sie dient nur als Nachrichtenträger, der seinen Text an alle gesammelten Tone-RICs verteilt. Falls keine Tone-RICs vorhanden sind, wird die Text-RIC als `multicastMode: single` ausgegeben.
@ -53,9 +53,9 @@ Hinweis: Der Delimiter-RIC (0123456) wird automatisch gefiltert und nicht ausgeg
|Feld|Beschreibung|Default| |Feld|Beschreibung|Default|
|----|------------|-------| |----|------------|-------|
|autoClearTimeout|Auto-Clear Timeout in Sekunden - Nicht zugestellte Empfänger werden nach dieser Zeit als incomplete ausgegeben|10| |autoClearTimeout|Auto-Clear Timeout in Sekunden - Nicht zugestellte Empfänger werden nach dieser Zeit als incomplete ausgegeben|10|
|delimiterRics|Komma-getrennte Liste von Startmarkern, die einen Multicast-Block beginnen (leert sofort vorherige Empfänger und werden automatisch gefiltert wenn konfiguriert)|leer| |delimiterRics|Komma-getrennte Liste von Startmarkern, die einen Multicast-Block beginnen (leert sofort vorherige Empfänger und werden mit multicastRole: delimiter markiert)|leer|
|textRics|Komma-getrennte Liste von RICs, die den Alarmtext tragen|leer| |textRics|Komma-getrennte Liste von RICs, die den Alarmtext tragen|leer|
|netIdentRics|Komma-getrennte Liste von Netzwerk-Identifikations-RICs (werden automatisch gefiltert wenn konfiguriert)|leer| |netIdentRics|Komma-getrennte Liste von Netzwerk-Identifikations-RICs (werden mit multicastRole: netident markiert)|leer|
|triggerRic|RIC für das Wakeup-Trigger-Paket (optional, bei leer: dynamisch = erste Tone-RIC)|leer| |triggerRic|RIC für das Wakeup-Trigger-Paket (optional, bei leer: dynamisch = erste Tone-RIC)|leer|
|triggerHost|IP-Adresse für Loopback-Trigger|127.0.0.1| |triggerHost|IP-Adresse für Loopback-Trigger|127.0.0.1|
|triggerPort|Port für Loopback-Trigger|8080| |triggerPort|Port für Loopback-Trigger|8080|
@ -110,7 +110,7 @@ Verwendet eine feste RIC (9999999) für das interne Wakeup-Trigger-Paket.
textRics: '0299001,0310001' textRics: '0299001,0310001'
netIdentRics: '0000001' netIdentRics: '0000001'
``` ```
Filtert Netzident-Pakete (RIC 0000001) automatisch aus der Weiterverarbeitung. Markiert Netzident-Pakete (RIC 0000001) mit multicastRole: netident. Downstream-Filter können sie gezielt ausfiltern (z.B. RegEx-Filter).
## Integration in Router-Konfiguration ## Integration in Router-Konfiguration
@ -241,10 +241,10 @@ router:
- `multicastRole` (string): Beschreibt die Rolle dieses Pakets innerhalb des Multicast-Ablaufs, besitzt einen der Werte: - `multicastRole` (string): Beschreibt die Rolle dieses Pakets innerhalb des Multicast-Ablaufs, besitzt einen der Werte:
- `delimiter`: Startmarker-Paket (wird automatisch gefiltert wenn delimiterRics konfiguriert) - `delimiter`: Startmarker-Paket
- `recipient`: tatsächlicher Empfänger - `recipient`: tatsächlicher Empfänger
- `single`: Einzelner, "normaler" Alarm (Tone-RIC = Text-RIC) - `single`: Einzelner, "normaler" Alarm (Tone-RIC = Text-RIC)
- `netident`: Netzwerk-Identifikations-Paket (wird automatisch gefiltert wenn netIdentRics konfiguriert) - `netident`: Netzwerk-Identifikations-Paket
- `multicastSourceRic` (string): RIC des ursprünglichen Message-RICs - `multicastSourceRic` (string): RIC des ursprünglichen Message-RICs
- `multicastRecipientCount` (string): Anzahl der Empfänger insgesamt - `multicastRecipientCount` (string): Anzahl der Empfänger insgesamt
@ -262,7 +262,7 @@ router:
- `message`: Bei incomplete-Modus leer, sonst Text von Text-RIC - `message`: Bei incomplete-Modus leer, sonst Text von Text-RIC
### Rückgabewerte: ### Rückgabewerte:
- **False**: Paket wurde blockiert (z.B. Delimiter/Netident-Filterung), Router stoppt Verarbeitung - **False**: Paket wurde intern konsumiert (z.B. Tone-RIC wurde in den Buffer aufgenommen), Router stoppt Verarbeitung für dieses Paket
- **Liste von Paketen**: Multicast-Verteilung, Router verarbeitet jedes Paket einzeln - **Liste von Paketen**: Multicast-Verteilung, Router verarbeitet jedes Paket einzeln
- **None**: Normaler Alarm, Router fährt mit unveränderten Paket fort - **None**: Normaler Alarm, Router fährt mit unveränderten Paket fort
@ -378,11 +378,34 @@ Ausgegebene Multicast-Pakete:
→ RIC: 0345678 SubRIC: 3 Message: "B3 WOHNHAUS" (behält SubRIC 3!) → RIC: 0345678 SubRIC: 3 Message: "B3 WOHNHAUS" (behält SubRIC 3!)
``` ```
### Automatische Filterung ### Paketmarkierung statt interner Filterung
- **Delimiter-Pakete**: Werden automatisch gefiltert (nicht weitergegeben), wenn `delimiterRics` konfiguriert ist Das Modul filtert keine inhaltlich relevanten Pakete.
- **Netzident-Pakete**: Werden automatisch gefiltert (nicht weitergegeben), wenn `netIdentRics` konfiguriert ist Alle Pakete mit Alarminhalt werden mit `multicastRole` markiert und
- **Filterung nach multicastRole**: Ermöglicht saubere nachgelagerte Verarbeitung ohne manuelle Filter weitergereicht. Die Filterung nach Bedarf erfolgt nachgelagert,
z.B. mit `filter.regexFilter`.
Eine Ausnahme bilden **Tone-RICs** (leere Nachrichten): Diese werden
intern im Buffer gesammelt und bei einem complete-Alarm in die
Listenfelder aggregiert. Sie erscheinen nie als eigenständige Pakete
im Router.
Pakete und ihre Rollen:
- **Delimiter-Pakete**: Erhalten `multicastRole: delimiter`
- **Netzident-Pakete**: Erhalten `multicastRole: netident`
- **Empfänger-Pakete**: Erhalten `multicastRole: recipient`
- **Einzelalarme**: Erhalten `multicastRole: single`
Beispiel-Filter um Delimiter und Netident auszublenden:
```yaml
- type: module
res: filter.regexFilter
config:
- name: "Nur echte Alarme"
checks:
- field: multicastRole
regex: ^(recipient|single)$
```
### Multi-Instanz-Betrieb ### Multi-Instanz-Betrieb

View file

@ -10,7 +10,7 @@ r"""!
by Bastian Schroll by Bastian Schroll
@file: multicast.py @file: multicast.py
@date: 26.01.2025 @date: 28.03.2026
@author: Claus Schichl @author: Claus Schichl
@description: multicast module @description: multicast module
""" """
@ -70,7 +70,8 @@ class BoswatchModule(ModuleBase):
@param None @param None
@return None""" @return None"""
self._my_frequencies = set() self._my_frequencies = set()
self.name = "Multicast" self.instance_id = hex(id(self))[-4:]
self.name = f"MCAST_{self.instance_id}"
self._auto_clear_timeout = int(self.config.get("autoClearTimeout", default=10)) self._auto_clear_timeout = int(self.config.get("autoClearTimeout", default=10))
self._hard_timeout = self._auto_clear_timeout * 3 self._hard_timeout = self._auto_clear_timeout * 3
@ -94,9 +95,6 @@ class BoswatchModule(ModuleBase):
self._trigger_host = self.config.get("triggerHost", default=self._TRIGGER_HOST) self._trigger_host = self.config.get("triggerHost", default=self._TRIGGER_HOST)
self._trigger_port = int(self.config.get("triggerPort", default=self._TRIGGER_PORT)) self._trigger_port = int(self.config.get("triggerPort", default=self._TRIGGER_PORT))
self._block_delimiter = bool(self._delimiter_rics)
self._block_netident = bool(self._netident_rics)
logging.info("[%s] Multicast module loaded", self.name) logging.info("[%s] Multicast module loaded", self.name)
with BoswatchModule._lock: with BoswatchModule._lock:
@ -118,28 +116,44 @@ class BoswatchModule(ModuleBase):
def doWork(self, bwPacket): def doWork(self, bwPacket):
r"""!Process an incoming packet and handle multicast logic. r"""!Process an incoming packet and handle multicast logic.
@param bwPacket: A BOSWatch packet instance Enriches packets with multicast metadata (mode, role, source).
@return bwPacket, a list of packets, or False if blocked""" Does NOT filter - all packets pass through, downstream modules handle filtering.
@param bwPacket: A BOSWatch packet instance or list of packets
@return bwPacket, a list of packets, or None if no processing"""
if isinstance(bwPacket, list):
result_packets = []
for single_packet in bwPacket:
processed = self.doWork(single_packet)
if processed is not None and processed is not False:
if isinstance(processed, list):
result_packets.extend(processed)
else:
result_packets.append(processed)
return result_packets if result_packets else None
packet_dict = self._get_packet_data(bwPacket) packet_dict = self._get_packet_data(bwPacket)
msg = packet_dict.get("message") msg = packet_dict.get("message")
ric = packet_dict.get("ric") ric = packet_dict.get("ric")
freq = packet_dict.get("frequency", "default") freq = packet_dict.get("frequency", "default")
mode = packet_dict.get("mode") mode = packet_dict.get("mode")
# Handle wakeup triggers
if msg == BoswatchModule._MAGIC_WAKEUP_MSG: if msg == BoswatchModule._MAGIC_WAKEUP_MSG:
if self._trigger_ric and ric != self._trigger_ric: if self._trigger_ric and ric != self._trigger_ric:
pass return None
else: logging.debug("[%s] Wakeup trigger received (RIC=%s)", self.name, ric)
logging.debug("[%s] Wakeup trigger received (RIC=%s)", self.name, ric) queued = self._get_queued_packets()
queued = self._get_queued_packets() return queued if queued else None
return queued if queued else False
# Only process POCSAG
if mode != "pocsag": if mode != "pocsag":
queued = self._get_queued_packets() queued = self._get_queued_packets()
return queued if queued else None return queued if queued else None
self._my_frequencies.add(freq) self._my_frequencies.add(freq)
# Determine if this is a text-RIC
is_text_ric = False is_text_ric = False
if self._text_rics: if self._text_rics:
is_text_ric = ric in self._text_rics and msg and msg.strip() is_text_ric = ric in self._text_rics and msg and msg.strip()
@ -155,21 +169,23 @@ class BoswatchModule(ModuleBase):
queued_packets = self._get_queued_packets() queued_packets = self._get_queued_packets()
incomplete_packets = None if is_text_ric else self._check_instance_auto_clear(freq) incomplete_packets = None if is_text_ric else self._check_instance_auto_clear(freq)
# === CONTROL PACKETS (netident, delimiter) ===
# Mark and pass through - no filtering!
if self._netident_rics and ric in self._netident_rics: if self._netident_rics and ric in self._netident_rics:
self._set_mcast_metadata(bwPacket, "control", "netident", ric) self._set_mcast_metadata(bwPacket, "control", "netident", ric)
result = self._combine_results(incomplete_packets, queued_packets, [bwPacket]) return self._combine_results(incomplete_packets, queued_packets, [bwPacket])
return self._filter_output(result)
if self._delimiter_rics and ric in self._delimiter_rics: if self._delimiter_rics and ric in self._delimiter_rics:
delimiter_incomplete = self._handle_delimiter(freq, ric, bwPacket) delimiter_incomplete = self._handle_delimiter(freq, ric, bwPacket)
result = self._combine_results(delimiter_incomplete, incomplete_packets, queued_packets) return self._combine_results(delimiter_incomplete, incomplete_packets, queued_packets)
return self._filter_output(result)
# === TONE-RICs (no message) ===
if not msg or not msg.strip(): if not msg or not msg.strip():
self._add_tone_ric_packet(freq, packet_dict) self._add_tone_ric_packet(freq, packet_dict)
result = self._combine_results(incomplete_packets, queued_packets, False) return self._combine_results(incomplete_packets, queued_packets, False)
return self._filter_output(result)
# === TEXT-RICs (with message) ===
if is_text_ric and msg: if is_text_ric and msg:
logging.info("[%s] Text-RIC received: RIC=%s", self.name, ric) logging.info("[%s] Text-RIC received: RIC=%s", self.name, ric)
alarm_packets = self._distribute_complete(freq, packet_dict) alarm_packets = self._distribute_complete(freq, packet_dict)
@ -180,18 +196,16 @@ class BoswatchModule(ModuleBase):
if not alarm_packets: if not alarm_packets:
logging.warning("[%s] No tone-RICs for text-RIC=%s", self.name, ric) logging.warning("[%s] No tone-RICs for text-RIC=%s", self.name, ric)
normal = self._enrich_normal_alarm(bwPacket, packet_dict) normal = self._enrich_normal_alarm(bwPacket, packet_dict)
result = self._combine_results(normal, incomplete_packets, queued_packets) return self._combine_results(normal, incomplete_packets, queued_packets)
else: else:
result = self._combine_results(alarm_packets, incomplete_packets, queued_packets) return self._combine_results(alarm_packets, incomplete_packets, queued_packets)
return self._filter_output(result)
# === SINGLE ALARM (message but no text-RICs configured) ===
if msg: if msg:
normal = self._enrich_normal_alarm(bwPacket, packet_dict) normal = self._enrich_normal_alarm(bwPacket, packet_dict)
result = self._combine_results(normal, incomplete_packets, queued_packets) return self._combine_results(normal, incomplete_packets, queued_packets)
return self._filter_output(result)
result = self._combine_results(incomplete_packets, queued_packets) return self._combine_results(incomplete_packets, queued_packets)
return self._filter_output(result)
# ============================================================ # ============================================================
# PACKET PROCESSING HELPERS (called by doWork) # PACKET PROCESSING HELPERS (called by doWork)
@ -220,31 +234,6 @@ class BoswatchModule(ModuleBase):
logging.warning("[%s] Error: %s", self.name, e) logging.warning("[%s] Error: %s", self.name, e)
return {} return {}
def _filter_output(self, result):
r"""!Apply multicastRole filtering before output.
@param result: Single packet, list of packets, None or False
@return Final packet(s) or False if blocked"""
if result is None or result is False:
return result
def get_role(packet):
"""Helper to extract multicastRole from Packet object"""
packet_dict = self._get_packet_data(packet)
return packet_dict.get("multicastRole")
if isinstance(result, list):
filtered = [p for p in result if self._should_output_packet(get_role(p))]
if not filtered:
logging.debug("All packets filtered out by multicastRole")
return False
return filtered if len(filtered) > 1 else filtered[0]
else:
if self._should_output_packet(get_role(result)):
return result
logging.debug("Packet filtered out: multicastRole=%s", get_role(result))
return False
def _combine_results(self, *results): def _combine_results(self, *results):
r"""!Combine multiple result sources into a single list or status. r"""!Combine multiple result sources into a single list or status.
@ -266,17 +255,6 @@ class BoswatchModule(ModuleBase):
return combined return combined
return False if has_false else None return False if has_false else None
def _should_output_packet(self, multicast_role):
r"""!Check if packet should be output based on role.
@param multicast_role: The role string to check
@return bool: True if allowed"""
if self._block_delimiter and multicast_role == "delimiter":
return False
if self._block_netident and multicast_role == "netident":
return False
return True
# ============================================================ # ============================================================
# TONE-RIC BUFFER MANAGEMENT # TONE-RIC BUFFER MANAGEMENT
# ============================================================ # ============================================================