BW3-Core/module/moduleBase.py
KoenigMjr 0b9387af08 [feat/multicast]: add multi-instance multicast module with active trigger 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.
2026-03-05 13:52:48 +01:00

144 lines
4.6 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
r"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: moduleBase.py
@date: 01.03.2019
@author: Bastian Schroll
@description: Module main class to inherit
"""
import logging
import time
from abc import ABC
from boswatch import wildcard
logging.debug("- %s loaded", __name__)
class ModuleBase(ABC):
r"""!Main module class"""
_modulesActive = []
def __init__(self, moduleName, config):
r"""!init preload some needed locals and then call onLoad() directly"""
self._moduleName = moduleName
self.config = config
self._modulesActive.append(self)
# for time counting
self._cumTime = 0
self._moduleTime = 0
# for statistics
self._runCount = 0
self._moduleErrorCount = 0
logging.debug("[%s] onLoad()", moduleName)
self.onLoad()
def _cleanup(self):
r"""!Cleanup routine calls onUnload() directly"""
logging.debug("[%s] onUnload()", self._moduleName)
self._modulesActive.remove(self)
self.onUnload()
def _run(self, bwPacket):
r"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance
@return bwPacket or False"""
# --- FIX: Multicast list support for Module ---
if isinstance(bwPacket, list):
result_packets = []
for single_packet in bwPacket:
# Recursive call for single packet
processed = self._run(single_packet)
# new logic:
if processed is False:
# filter called 'False' -> packet discarded
continue
elif processed is None:
# module returned None -> keep packet unchanged
result_packets.append(single_packet)
elif isinstance(processed, list):
# module returned new list -> extend
result_packets.extend(processed)
else:
# module returned modified packet -> add
result_packets.append(processed)
# if list is not empty, return it. else False (filter all).
return result_packets if result_packets else False
# -----------------------------------------------
self._runCount += 1
logging.debug("[%s] run #%d", self._moduleName, self._runCount)
tmpTime = time.time()
try:
logging.debug("[%s] doWork()", self._moduleName)
bwPacket = self.doWork(bwPacket)
except:
self._moduleErrorCount += 1
logging.exception("[%s] alarm error", self._moduleName)
self._moduleTime = time.time() - tmpTime
self._cumTime += self._moduleTime
logging.debug("[%s] took %0.3f seconds", self._moduleName, self._moduleTime)
return bwPacket
def _getStatistics(self):
r"""!Returns statistical information's from last module run
@return Statistics as pyton dict"""
stats = {"type": "module",
"runCount": self._runCount,
"cumTime": self._cumTime,
"moduleTime": self._moduleTime,
"moduleErrorCount": self._moduleErrorCount}
return stats
def onLoad(self):
r"""!Called by import of the module
can be inherited"""
pass
def doWork(self, bwPacket):
r"""!Called module run
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("no functionality in module %s", self._moduleName)
def onUnload(self):
r"""!Called on shutdown of boswatch
can be inherited"""
pass
@staticmethod
def registerWildcard(newWildcard, bwPacketField):
r"""!Register a new wildcard
@param newWildcard: wildcard where parser searching for
@param bwPacketField: field from bwPacket where holds replacement data"""
if not newWildcard.startswith("{") or not newWildcard.endswith("}"):
logging.error("wildcard not registered - false format: %s", newWildcard)
return
if bwPacketField == "":
logging.error("wildcard not registered - bwPacket field is empty")
return
wildcard.registerWildcard(newWildcard, bwPacketField)