Merge branch 'develop' into update_check

This commit is contained in:
Bastian Schroll 2020-02-19 14:21:29 +01:00
commit be73d3c381
60 changed files with 1013 additions and 345 deletions

View file

@ -25,3 +25,8 @@ jobs:
- name: Test with pytest
run: |
pytest -c 'test/pytest.ini'
- name: Save artifacts
uses: actions/upload-artifact@master
with:
name: test.log
path: log/test.log

1
.gitignore vendored
View file

@ -10,6 +10,7 @@
\venv/
# generated files
stats_*
log/
docu/docs/api/html
docu/site/

31
Dockerfile Normal file
View file

@ -0,0 +1,31 @@
FROM alpine:3.10 AS build-base
RUN apk add git make cmake g++ libusb-dev libpulse
FROM build-base AS rtl_fm
ARG RTL_SDR_VERSION=0.6.0
RUN git clone --depth 1 --branch ${RTL_SDR_VERSION} https://github.com/osmocom/rtl-sdr.git /opt/rtl_sdr
WORKDIR /opt/rtl_sdr/build
RUN cmake .. && make
FROM build-base AS multimon
ARG MULTIMON_VERSION=1.1.8
RUN git clone --depth 1 --branch ${MULTIMON_VERSION} https://github.com/EliasOenal/multimon-ng.git /opt/multimon
WORKDIR /opt/multimon/build
RUN cmake .. && make
FROM alpine:3.10 AS boswatch
ARG BW_VERSION=develop
RUN apk add git && \
git clone --depth 1 --branch ${BW_VERSION} https://github.com/BOSWatch/BW3-Core.git /opt/boswatch
FROM python:3.6-alpine AS runner
LABEL maintainer="bastian@schroll-software.de"
# for RTL for MM
RUN apk add libusb-dev libpulse && \
pip3 install pyyaml
COPY --from=boswatch /opt/boswatch/ /opt/boswatch/
COPY --from=multimon /opt/multimon/build/multimon-ng /opt/multimon/multimon-ng
COPY --from=rtl_fm /opt/rtl_sdr/build/src/ /opt/rtl_sdr/build/src/

View file

@ -33,6 +33,10 @@ class ConfigYAML:
else:
yield item
def __len__(self):
"""!returns the length of an config element"""
return len(self._config)
def __str__(self):
"""!Returns the string representation of the internal config dict"""
return str(self._config)

View file

@ -31,7 +31,6 @@ class Decoder:
@param data: data to decode
@return bwPacket instance"""
logging.debug("search decoder")
data = str(data)
if "FMS" in data:
return FmsDecoder.decode(data)

View file

@ -62,10 +62,9 @@ class FmsDecoder:
bwPacket.set("directionText", directionText)
bwPacket.set("tacticalInfo", tacticalInfo)
logging.debug(bwPacket)
return bwPacket
logging.warning("no valid data")
logging.warning("no valid FMS")
return None
logging.warning("CRC Error")
return None

View file

@ -56,10 +56,9 @@ class PocsagDecoder:
bwPacket.set("subricText", subricText)
bwPacket.set("message", message)
logging.debug(bwPacket)
return bwPacket
logging.warning("no valid data")
logging.warning("no valid POCSAG")
return None
@staticmethod

View file

@ -44,10 +44,9 @@ class ZveiDecoder:
bwPacket.set("mode", "zvei")
bwPacket.set("zvei", ZveiDecoder._solveDoubleTone(data[7:12]))
logging.debug(bwPacket)
return bwPacket
logging.warning("no valid data")
logging.warning("no valid ZVEI")
return None
@staticmethod

View file

@ -0,0 +1,2 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

View file

@ -0,0 +1,65 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: inoutSource.py
@date: 28.10.2018
@author: Bastian Schroll
@description: Base class for boswatch input sources
"""
import time
import logging
import threading
from abc import ABC, abstractmethod
logging.debug("- %s loaded", __name__)
class InputBase(ABC):
"""!Base class for handling inout sources"""
def __init__(self, inputQueue, inputConfig, decoderConfig):
"""!Build a new InputSource class
@param inputQueue: Python queue object to store input data
@param inputConfig: ConfigYaml object with the inoutSource config
@param decoderConfig: ConfigYaml object with the decoder config"""
self._inputThread = None
self._isRunning = False
self._inputQueue = inputQueue
self._inputConfig = inputConfig
self._decoderConfig = decoderConfig
def start(self):
"""!Start the input source thread"""
logging.debug("starting input thread")
self._isRunning = True
self._inputThread = threading.Thread(target=self._runThread, name="inputThread",
args=(self._inputQueue, self._inputConfig, self._decoderConfig))
self._inputThread.daemon = True
self._inputThread.start()
@abstractmethod
def _runThread(self, dataQueue, sdrConfig, decoderConfig):
"""!Thread routine of the input source has to be inherit"""
def shutdown(self):
"""!Stop the input source thread"""
if self._isRunning:
logging.debug("wait for stopping the input thread")
self._isRunning = False
self._inputThread.join()
logging.debug("input thread stopped")
def addToQueue(self, data):
"""!Adds alarm data to the queue for further processing during boswatch client"""
self._inputQueue.put_nowait((data, time.time()))
logging.debug("Add received data to queue")
print(data)

View file

@ -0,0 +1,77 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: sdrInput.py
@date: 28.10.2018
@author: Bastian Schroll
@description: Input source for sdr with rtl_fm
"""
import logging
from boswatch.utils import paths
from boswatch.processManager import ProcessManager
from boswatch.inputSource.inputBase import InputBase
logging.debug("- %s loaded", __name__)
class SdrInput(InputBase):
"""!Class for the sdr input source"""
def _runThread(self, dataQueue, sdrConfig, decoderConfig):
sdrProc = None
mmProc = None
try:
sdrProc = ProcessManager(str(sdrConfig.get("rtlPath", default="rtl_fm")))
sdrProc.addArgument("-d " + str(sdrConfig.get("device", default="0"))) # device id
sdrProc.addArgument("-f " + str(sdrConfig.get("frequency"))) # frequencies
sdrProc.addArgument("-p " + str(sdrConfig.get("error", default="0"))) # frequency error in ppm
sdrProc.addArgument("-l " + str(sdrConfig.get("squelch", default="1"))) # squelch
sdrProc.addArgument("-g " + str(sdrConfig.get("gain", default="100"))) # gain
sdrProc.addArgument("-M fm") # set mode to fm
sdrProc.addArgument("-E DC") # set DC filter
sdrProc.addArgument("-s 22050") # bit rate of audio stream
sdrProc.setStderr(open(paths.LOG_PATH + "rtl_fm.log", "a"))
sdrProc.start()
mmProc = ProcessManager(str(sdrConfig.get("mmPath", default="multimon-ng")), textMode=True)
if decoderConfig.get("fms", default=0):
mmProc.addArgument("-a FMSFSK")
if decoderConfig.get("zvei", default=0):
mmProc.addArgument("-a ZVEI1")
if decoderConfig.get("poc512", default=0):
mmProc.addArgument("-a POCSAG512")
if decoderConfig.get("poc1200", default=0):
mmProc.addArgument("-a POCSAG1200")
if decoderConfig.get("poc2400", default=0):
mmProc.addArgument("-a POCSAG2400")
mmProc.addArgument("-f alpha")
mmProc.addArgument("-t raw -")
mmProc.setStdin(sdrProc.stdout)
mmProc.setStderr(open(paths.LOG_PATH + "multimon-ng.log", "a"))
mmProc.start()
logging.info("start decoding")
while self._isRunning:
if not sdrProc.isRunning:
logging.warning("rtl_fm was down - try to restart")
sdrProc.start()
elif not mmProc.isRunning:
logging.warning("multimon was down - try to restart")
mmProc.start()
elif sdrProc.isRunning and mmProc.isRunning:
line = mmProc.readline()
if line:
self.addToQueue(line)
except:
logging.exception("error in sdr input routine")
finally:
mmProc.stop()
sdrProc.stop()

View file

@ -57,6 +57,7 @@ class TCPClient:
@return True or False"""
try:
if self.isConnected:
self._sock.shutdown(socket.SHUT_RDWR)
self._sock.close()
logging.debug("disconnected")
return True
@ -72,29 +73,35 @@ class TCPClient:
@param data: data to send to the server
@return True or False"""
try:
logging.debug("transmitting: %s", data)
header = str(len(data)).ljust(HEADERSIZE)
self._sock.sendall(bytes(header + data, "utf-8"))
logging.debug("transmitting:\n%s", data)
data = data.encode("utf-8")
header = str(len(data)).ljust(HEADERSIZE).encode("utf-8")
self._sock.sendall(header + data)
logging.debug("transmitted...")
return True
except socket.error as e:
logging.error(e)
return False
def receive(self):
def receive(self, timeout=1):
"""!Receive data from the server
@param timeout: to wait for incoming data in seconds
@return received data"""
try:
read, _, _ = select.select([self._sock], [], [], 1)
read, _, _ = select.select([self._sock], [], [], timeout)
if not read: # check if there is something to read
return False
header = self._sock.recv(HEADERSIZE)
header = self._sock.recv(HEADERSIZE).decode("utf-8")
if not len(header): # check if there data
return False
length = int(header.decode("utf-8").strip())
length = int(header.strip())
received = self._sock.recv(length).decode("utf-8")
logging.debug("received %d bytes: %s", length, received)
logging.debug("recv header: '%s'", header)
logging.debug("received %d bytes: %s", len(received), received)
return received
except socket.error as e:
logging.error(e)
@ -104,12 +111,17 @@ class TCPClient:
def isConnected(self):
"""!Property of client connected state"""
try:
aliveMsg = "<alive>"
header = str(len(aliveMsg)).ljust(HEADERSIZE)
self._sock.sendall(bytes(header + aliveMsg, "utf-8"))
return True
if self._sock:
_, write, _ = select.select([], [self._sock], [], 0.1)
if write:
data = "<keep-alive>".encode("utf-8")
header = str(len(data)).ljust(HEADERSIZE).encode("utf-8")
self._sock.sendall(header + data)
return True
return False
except socket.error as e:
if e.errno is 32: # broken pipe - no one will read from this pipe anymore
return False
logging.error(e)
return False
if e.errno != 32:
logging.exception(e)
return False
except ValueError:
return False

View file

@ -27,10 +27,10 @@ class NetCheck:
"""!Create a new NetCheck instance
@param hostname: host against connection check is running ("https://www.google.com/")
@param timout: timout for connection check in sec. (1)"""
@param timeout: timeout for connection check in sec. (1)"""
self._hostname = hostname
self._timeout = timeout
self._connectionState = False
self.connectionState = False
self.checkConn() # initiate a first check
def checkConn(self):
@ -40,14 +40,9 @@ class NetCheck:
try:
urlopen(self._hostname, timeout=self._timeout)
logging.debug("%s is reachable", self._hostname)
self._connectionState = True
self.connectionState = True
return True
except: # todo find right exception type
logging.warning("%s is not reachable", self._hostname)
self._connectionState = False
self.connectionState = False
return False
@property
def connectionState(self):
"""!Property for the last connection state from checkConn()"""
return self._connectionState

View file

@ -46,17 +46,18 @@ class _ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
if not read:
continue # nothing to read on the socket
header = self.request.recv(HEADERSIZE)
header = self.request.recv(HEADERSIZE).decode("utf-8")
if not len(header):
break # empty data -> socked closed
length = int(header.decode("utf-8").strip())
length = int(header.strip())
data = self.request.recv(length).decode("utf-8")
if data == "<alive>":
if data == "<keep-alive>":
continue
logging.debug("%s recv %d bytes: %s", req_name, length, data)
logging.debug("%s recv header: '%s'", req_name, header)
logging.debug("%s recv %d bytes:\n%s", req_name, len(data), data)
# add a new entry and the decoded data dict as an string in utf-8 and an timestamp
self.server.alarmQueue.put_nowait((self.client_address[0], data, time.time())) # queue is threadsafe
@ -64,15 +65,15 @@ class _ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
logging.debug("%s send: [ack]", req_name)
data = "[ack]"
header = str(len(data)).ljust(HEADERSIZE)
self.request.sendall(bytes(header + data, "utf-8"))
self.request.close()
data = "[ack]".encode("utf-8")
header = str(len(data)).ljust(HEADERSIZE).encode("utf-8")
self.request.sendall(header + data)
except socket.error as e:
logging.error(e)
return False
finally:
self.request.close()
del self.server.clientsConnected[threading.current_thread().name]
logging.info("Client disconnected: %s", self.client_address[0])
@ -115,7 +116,7 @@ class TCPServer:
@return True or False"""
if not self.isRunning:
try:
socketserver.TCPServer.allow_reuse_address = False # because we can start two instances on same port elsewhere
socketserver.TCPServer.allow_reuse_address = True # because we can start two instances on same port elsewhere
self._server = _ThreadedTCPServer(("", port), _ThreadedTCPRequestHandler)
self._server.timeout = self._timeout
self._server.alarmQueue = self._alarmQueue
@ -144,6 +145,7 @@ class TCPServer:
if self.isRunning:
self._server.shutdown()
self._server.isActive = False
self._server.server_close()
self._server_thread.join()
self._server_thread = None
self._server = None

View file

@ -43,7 +43,7 @@ class Packet:
@param fieldName: Name of the data to set
@param value: Value to set"""
self._packet[fieldName] = value
self._packet[fieldName] = str(value)
def get(self, fieldName):
"""!Returns the value from a single field.
@ -52,7 +52,7 @@ class Packet:
@param fieldName: Name of the field
@return Value or None"""
try:
return self._packet[fieldName]
return str(self._packet[fieldName])
except:
logging.warning("field not found: %s", fieldName)
return None

View file

@ -32,9 +32,6 @@ class ProcessManager:
self._processHandle = None
self._textMode = textMode
def __del__(self):
self.stop()
def addArgument(self, arg):
"""!add a new argument

View file

@ -15,24 +15,22 @@
@description: Class for a single BOSWatch packet router route point
"""
import logging
logging.debug("- %s loaded", __name__)
class Route:
"""!Class for single routing points"""
def __init__(self, name, callback):
def __init__(self, name, callback, statsCallback=None, cleanupCallback=None):
"""!Create a instance of an route point
@param name: name of the route point
@param callback: instance of the callback function
@param statsCallback: instance of the callback to get statistics (None)
@param cleanupCallback: instance of the callback to run a cleanup method (None)
"""
self._name = name
self._callback = callback
@property
def name(self):
"""!Property to get the route point name"""
return self._name
@property
def callback(self):
"""!Porperty to get the callback function instance"""
return self._callback
self.name = name
self.callback = callback
self.statistics = statsCallback
self.cleanup = cleanupCallback

View file

@ -16,6 +16,7 @@
"""
import logging
import copy
import time
logging.debug("- %s loaded", __name__)
@ -26,17 +27,25 @@ class Router:
"""!Create a new router
@param name: name of the router"""
self._name = name
self._routeList = []
logging.debug("[%s] new router", self._name)
self.name = name
self.routeList = []
# for time counting
self._cumTime = 0
self._routerTime = 0
# for statistics
self._runCount = 0
logging.debug("[%s] add new router", self.name)
def addRoute(self, route):
"""!Adds a route point to the router
@param route: instance of the Route class
"""
logging.debug("[%s] add route: %s", self._name, route.name)
self._routeList.append(route)
logging.debug("[%s] add route: %s", self.name, route.name)
self.routeList.append(route)
def runRouter(self, bwPacket):
"""!Run the router
@ -44,29 +53,37 @@ class Router:
@param bwPacket: instance of Packet class
@return a instance of Packet class
"""
logging.debug("[%s] started", self._name)
for routeObject in self._routeList:
logging.debug("[%s] -> run route: %s", self._name, routeObject)
self._runCount += 1
tmpTime = time.time()
logging.debug("[%s] started", self.name)
for routeObject in self.routeList:
logging.debug("[%s] -> run route: %s", self.name, routeObject.name)
bwPacket_tmp = routeObject.callback(copy.deepcopy(bwPacket)) # copy bwPacket to prevent edit the original
if bwPacket_tmp is None: # returning None doesnt change the bwPacket
continue
if bwPacket_tmp is False: # returning False stops the router immediately
logging.debug("[%s] stopped", self._name)
logging.debug("[%s] stopped", self.name)
break
bwPacket = bwPacket_tmp
logging.debug("[%s] <- bwPacket returned: %s", self._name, bwPacket)
logging.debug("[%s] finished", self._name)
logging.debug("[%s] bwPacket returned", self.name)
logging.debug("[%s] finished", self.name)
self._routerTime = time.time() - tmpTime
self._cumTime += self._routerTime
return bwPacket
@property
def name(self):
"""!Property to get the name of the router"""
return self._name
def _getStatistics(self):
"""!Returns statistical information's from last router run
@property
def routeList(self):
"""!Property to get a list of all route points of this router"""
return self._routeList
@return Statistics as pyton dict"""
stats = {"type": "router",
"runCount": self._runCount,
"cumTime": self._cumTime,
"moduleTime": self._routerTime}
return stats

View file

@ -18,6 +18,7 @@
# todo think about implement threading for routers and the plugin calls (THREAD SAFETY!!!)
import logging
import importlib
import time
from boswatch.configYaml import ConfigYAML
from boswatch.router.router import Router
from boswatch.router.route import Route
@ -30,16 +31,10 @@ class RouterManager:
def __init__(self):
"""!Create new router"""
self._routerDict = {}
def __del__(self):
"""!Destroy the internal routerDict
All routers and route point instances will be destroyed too
Also destroys all instances from modules or plugins"""
# destroy all routers (also destroys all instances of modules/plugins)
del self._routerDict
self._startTime = int(time.time())
# if there is an error, router list would be empty (see tmp variable)
def buildRouter(self, config):
def buildRouters(self, config):
"""!Initialize Routers from given config file
@param config: instance of ConfigYaml class
@ -64,33 +59,42 @@ class RouterManager:
for route in router.get("route"):
routeType = route.get("type")
routeName = route.get("name")
routeRes = route.get("res")
routeName = route.get("name", default=routeRes)
routeConfig = route.get("config", default=ConfigYAML()) # if no config - build a empty
if routeType is None or routeName is None:
if routeType is None or routeRes is None:
logging.error("type or name not found in route: %s", route)
return False
try:
if routeType == "plugin":
importedFile = importlib.import_module(routeType + "." + routeName)
importedFile = importlib.import_module(routeType + "." + routeRes)
loadedClass = importedFile.BoswatchPlugin(routeConfig)
routerDict_tmp[routerName].addRoute(Route(routeName, loadedClass._run))
routerDict_tmp[routerName].addRoute(Route(routeName,
loadedClass._run,
loadedClass._getStatistics,
loadedClass._cleanup))
elif routeType == "module":
importedFile = importlib.import_module(routeType + "." + routeName)
importedFile = importlib.import_module(routeType + "." + routeRes)
loadedClass = importedFile.BoswatchModule(routeConfig)
routerDict_tmp[routerName].addRoute(Route(routeName, loadedClass._run))
routerDict_tmp[routerName].addRoute(Route(routeName,
loadedClass._run,
loadedClass._getStatistics,
loadedClass._cleanup))
elif routeType == "router":
routerDict_tmp[routerName].addRoute(Route(routeName, routerDict_tmp[routeName].runRouter))
routerDict_tmp[routerName].addRoute(Route(routeName, routerDict_tmp[routeRes].runRouter))
else:
logging.error("unknown type '%s' in %s", routeType, route)
return False
except ModuleNotFoundError:
logging.error("%s not found: %s", route.get("type"), route.get("name"))
# except ModuleNotFoundError: # only since Py3.6
except ImportError:
logging.error("%s not found: %s", route.get("type"), route.get("res"))
return False
logging.debug("finished building routers")
@ -98,7 +102,7 @@ class RouterManager:
self._showRouterRoute()
return True
def runRouter(self, routerRunList, bwPacket):
def runRouters(self, routerRunList, bwPacket):
"""!Run given Routers
@param routerRunList: string or list of router names in string form
@ -112,6 +116,16 @@ class RouterManager:
else:
logging.warning("unknown router: %s", routerName)
self._saveStats() # write stats to stats file
def cleanup(self):
"""!Run cleanup routines for all loaded route points"""
for name, routerObject in self._routerDict.items():
logging.debug("Start cleanup for %s", name)
for routePoint in routerObject.routeList:
if routePoint.cleanup:
routePoint.cleanup()
def _showRouterRoute(self):
"""!Show the routes of all routers"""
for name, routerObject in self._routerDict.items():
@ -120,3 +134,27 @@ class RouterManager:
for routePoint in routerObject.routeList:
counter += 1
logging.debug(" %d. %s", counter, routePoint.name)
def _saveStats(self):
"""!Save current statistics to file"""
lines = []
for name, routerObject in self._routerDict.items():
lines.append("[" + name + "]")
lines.append(" - Route points: " + str(len(routerObject.routeList)))
lines.append(" - Runs: " + str(routerObject._getStatistics()['runCount']))
for routePoint in routerObject.routeList:
lines.append("[+] " + routePoint.name)
if routePoint.statistics:
if routePoint.statistics()['type'] == "module":
lines.append(" - Runs: " + str(routePoint.statistics()['runCount']))
lines.append(" - Run errors: " + str(routePoint.statistics()['moduleErrorCount']))
elif routePoint.statistics()['type'] == "plugin":
lines.append(" - Runs: " + str(routePoint.statistics()['runCount']))
lines.append(" - Setup errors: " + str(routePoint.statistics()['setupErrorCount']))
lines.append(" - Alarm errors: " + str(routePoint.statistics()['alarmErrorCount']))
lines.append(" - Teardown errors: " + str(routePoint.statistics()['teardownErrorCount']))
lines.append("")
with open("stats_" + str(self._startTime) + ".txt", "w") as stats:
for line in lines:
stats.write(line + "\n")

View file

@ -36,8 +36,8 @@ class RepeatedTimer:
self._args = args
self._kwargs = kwargs
self._start = 0
self._overdueCount = 0
self._lostEvents = 0
self.overdueCount = 0
self.lostEvents = 0
self._isRunning = False
self._event = Event()
self._thread = None
@ -88,8 +88,8 @@ class RepeatedTimer:
lostEvents = int(runTime / self._interval)
logging.warning("timer overdue! interval: %0.3f sec. - runtime: %0.3f sec. - "
"%d events lost - next call in: %0.3f sec.", self._interval, runTime, lostEvents, self.restTime)
self._lostEvents += lostEvents
self._overdueCount += 1
self.lostEvents += lostEvents
self.overdueCount += 1
logging.debug("repeatedTimer thread stopped: %s", self._thread.name)
self._thread = None # set to none after leave teh thread (running recognize)
@ -104,13 +104,3 @@ class RepeatedTimer:
def restTime(self):
"""!Property to get remaining time till next call"""
return self._interval - ((time.time() - self._start) % self._interval)
@property
def overdueCount(self):
"""!Property to get a count over all overdues"""
return self._overdueCount
@property
def lostEvents(self):
"""!Property to get a count over all lost events"""
return self._lostEvents

View file

@ -12,28 +12,43 @@
@file: wildcard.py
@date: 15.01.2018
@author: Bastian Schroll
@description: Little Helper to replace wildcards in stings
@description: Functions to replace wildcards in stings
"""
import logging
import time
# from boswatch.module import file
logging.debug("- %s loaded", __name__)
# todo check function and document + write an test
# todo maybe can be a module instead of a native boswatch piece
# idea: maybe this can be a class with a register_wildcard() method
# so the list with wildcards can be modified by other modules
# todo check function - write an test
_additionalWildcards = {}
def registerWildcard(wildcard, bwPacketField):
"""!Register a new additional wildcard
@param wildcard: New wildcard string with format: '{WILDCARD}'
@param bwPacketField: Field of the bwPacket which is used for wildcard replacement"""
if wildcard in _additionalWildcards:
logging.error("wildcard always registered: %s", wildcard)
return
logging.debug("register new wildcard %s for field: %s", wildcard, bwPacketField)
_additionalWildcards[wildcard] = bwPacketField
def replaceWildcards(message, bwPacket):
"""!Replace the wildcards in a given message
@param message: Message in which wildcards should be replaced
@param bwPacket: bwPacket instance with the replacement information
@return Input message with the replaced wildcards"""
_wildcards = {
# formatting wildcards
# todo check if br and par are needed - if not also change config
"{BR}": "\r\n",
"{LPAR}": "(",
"{RPAR}": ")",
"{TIME}": time.time(),
"{TIME}": time.strftime("%d.%m.%Y %H:%M:%S"),
# info wildcards
# server
@ -78,7 +93,10 @@ def replaceWildcards(message, bwPacket):
# message for MSG packet is done in poc
}
for wildcard in _wildcards:
message = message.replace(wildcard, _wildcards.get(wildcard))
for wildcard, field in _wildcards.items():
message = message.replace(wildcard, field)
for wildcard, field in _additionalWildcards.items():
message = message.replace(wildcard, bwPacket.getField(field))
return message

View file

@ -32,8 +32,6 @@ logging.debug("BOSWatch client has started ...")
logging.debug("Import python modules")
import argparse
logging.debug("- argparse")
import threading
logging.debug("- threading")
import queue
logging.debug("- queue")
import time
@ -43,16 +41,14 @@ logging.debug("Import BOSWatch modules")
from boswatch.configYaml import ConfigYAML
from boswatch.network.client import TCPClient
from boswatch.network.broadcast import BroadcastClient
from boswatch.processManager import ProcessManager
from boswatch.decoder.decoder import Decoder
from boswatch.utils import header
from boswatch.utils import misc
from boswatch.inputSource.sdrInput import SdrInput
header.logoToLog()
header.infoToLog()
logging.debug("parse args")
# With -h or --help you get the Args help
parser = argparse.ArgumentParser(prog="bw_client.py",
description="""BOSWatch is a Python Script to receive and
@ -60,7 +56,7 @@ parser = argparse.ArgumentParser(prog="bw_client.py",
epilog="""More options you can find in the extern client.ini
file in the folder /config""")
parser.add_argument("-c", "--config", help="Name to configuration File", required=True)
parser.add_argument("-t", "--test", help="Start Client with testdata-set")
parser.add_argument("-t", "--test", help="Start Client with testdata-set", action="store_true")
args = parser.parse_args()
bwConfig = ConfigYAML()
@ -69,6 +65,10 @@ if not bwConfig.loadConfigFile(paths.CONFIG_PATH + args.config):
exit(1)
# ========== CLIENT CODE ==========
bwClient = None
inputSource = None
inputQueue = queue.Queue()
try:
ip = bwConfig.get("server", "ip", default="127.0.0.1")
port = bwConfig.get("server", "port", default="8080")
@ -79,73 +79,33 @@ try:
ip = broadcastClient.serverIP
port = broadcastClient.serverPort
inputQueue = queue.Queue()
inputThreadRunning = True
if not args.test:
logging.debug("loading input source: %s", bwConfig.get("client", "inputSource"))
if bwConfig.get("client", "inputSource") == "sdr":
inputSource = SdrInput(inputQueue, bwConfig.get("inputSource", "sdr"), bwConfig.get("decoder"))
else:
logging.fatal("Invalid input source: %s", bwConfig.get("client", "inputSource"))
exit(1)
# ========== INPUT CODE ==========
def handleSDRInput(dataQueue, sdrConfig, decoderConfig): # todo exception handling inside
sdrProc = ProcessManager(str(sdrConfig.get("rtlPath", default="rtl_fm")))
sdrProc.addArgument("-d " + str(sdrConfig.get("device", default="0"))) # device id
sdrProc.addArgument("-f " + sdrConfig.get("frequency")) # frequencies
sdrProc.addArgument("-p " + str(sdrConfig.get("error", default="0"))) # frequency error in ppm
sdrProc.addArgument("-l " + str(sdrConfig.get("squelch", default="1"))) # squelch
sdrProc.addArgument("-g " + str(sdrConfig.get("gain", default="100"))) # gain
sdrProc.addArgument("-M fm") # set mode to fm
sdrProc.addArgument("-E DC") # set DC filter
sdrProc.addArgument("-s 22050") # bit rate of audio stream
sdrProc.start()
sdrProc.skipLinesUntil("Output at")
mmProc = ProcessManager(str(sdrConfig.get("mmPath", default="multimon-ng")), textMode=True)
if decoderConfig.get("fms", default=0):
mmProc.addArgument("-a FMSFSK")
if decoderConfig.get("zvei", default=0):
mmProc.addArgument("-a ZVEI1")
if decoderConfig.get("poc512", default=0):
mmProc.addArgument("-a POCSAG512")
if decoderConfig.get("poc1200", default=0):
mmProc.addArgument("-a POCSAG1200")
if decoderConfig.get("poc2400", default=0):
mmProc.addArgument("-a POCSAG2400")
mmProc.addArgument("-f alpha")
mmProc.addArgument("-t raw -")
mmProc.setStdin(sdrProc.stdout)
mmProc.start()
mmProc.skipLinesUntil("Available demodulators:")
logging.info("start decoding")
while inputThreadRunning:
if not sdrProc.isRunning:
logging.warning("rtl_fm was down - try to restart")
sdrProc.start()
sdrProc.skipLinesUntil("Output at") # last line form rtl_fm before data
elif not mmProc.isRunning:
logging.warning("multimon was down - try to restart")
mmProc.start()
mmProc.skipLinesUntil("Available demodulators:") # last line from mm before data
elif sdrProc.isRunning and mmProc.isRunning:
line = mmProc.readline()
if line:
dataQueue.put_nowait((line, time.time()))
logging.debug("Add data to queue")
print(line)
logging.debug("stopping thread")
mmProc.stop()
sdrProc.stop()
# ========== INPUT CODE ==========
mmThread = threading.Thread(target=handleSDRInput, name="mmReader", args=(inputQueue, bwConfig.get("inputSource", "sdr"), bwConfig.get("decoder")))
mmThread.daemon = True
mmThread.start()
inputSource.start()
else:
logging.warning("STARTING TESTMODE!")
logging.debug("reading testdata from file")
testFile = open("test/testdata.list", mode="r", encoding="utf-8")
for testData in testFile:
if (len(testData.rstrip(' \t\n\r')) > 1) and ("#" not in testData[0]):
logging.info("Testdata: %s", testData.rstrip(' \t\n\r'))
inputQueue.put_nowait((testData.rstrip(' \t\n\r'), time.time()))
logging.debug("finished reading testdata")
bwClient = TCPClient()
bwClient.connect(ip, port)
while 1:
if not bwClient.isConnected:
logging.warning("connection to server lost - sleep %d seconds", bwConfig.get("client", "reconnectDelay", default="3"))
time.sleep(bwConfig.get("client", "reconnectDelay", default="3"))
reconnectDelay = bwConfig.get("client", "reconnectDelay", default="3")
logging.warning("connection to server lost - sleep %d seconds", reconnectDelay)
time.sleep(reconnectDelay)
bwClient.connect(ip, port)
elif not inputQueue.empty():
@ -164,13 +124,16 @@ try:
for sendCnt in range(bwConfig.get("client", "sendTries", default="3")):
bwClient.transmit(str(bwPacket))
if bwClient.receive() == "[ack-]":
if bwClient.receive() == "[ack]":
logging.debug("ack ok")
break
logging.warning("cannot send packet - sleep %d seconds", bwConfig.get("client", "sendDelay", default="3"))
time.sleep(bwConfig.get("client", "sendDelay", default="3"))
sendDelay = bwConfig.get("client", "sendDelay", default="3")
logging.warning("cannot send packet - sleep %d seconds", sendDelay)
time.sleep(sendDelay)
else:
if args.test:
break
time.sleep(0.1) # reduce cpu load (wait 100ms)
# in worst case a packet have to wait 100ms until it will be processed
@ -183,7 +146,8 @@ except: # pragma: no cover
logging.exception("BOSWatch interrupted by an error")
finally:
logging.debug("Starting shutdown routine")
bwClient.disconnect()
inputThreadRunning = False
mmThread.join()
if inputSource:
inputSource.shutdown()
if bwClient:
bwClient.disconnect()
logging.debug("BOSWatch client has stopped ...")

View file

@ -46,11 +46,9 @@ from boswatch.network.broadcast import BroadcastServer
from boswatch.router.routerManager import RouterManager
from boswatch.utils import misc
header.logoToLog()
header.infoToLog()
logging.debug("parse args")
# With -h or --help you get the Args help
parser = argparse.ArgumentParser(prog="bw_server.py",
description="""BOSWatch is a Python Script to receive and
@ -67,10 +65,13 @@ if not bwConfig.loadConfigFile(paths.CONFIG_PATH + args.config):
exit(1)
# ############################# begin server system
try:
bwRoutMan = None
bwServer = None
bcServer = None
try:
bwRoutMan = RouterManager()
if not bwRoutMan.buildRouter(bwConfig):
if not bwRoutMan.buildRouters(bwConfig):
logging.fatal("Error while building routers")
exit(1)
@ -97,7 +98,9 @@ try:
bwPacket.set("clientIP", data[0])
misc.addServerDataToPacket(bwPacket, bwConfig)
bwRoutMan.runRouter(bwConfig.get("alarmRouter"), bwPacket)
logging.debug("[ --- ALARM --- ]")
bwRoutMan.runRouters(bwConfig.get("alarmRouter"), bwPacket)
logging.debug("[ --- END ALARM --- ]")
incomingQueue.task_done()
@ -109,7 +112,10 @@ except: # pragma: no cover
logging.exception("BOSWatch interrupted by an error")
finally:
logging.debug("Starting shutdown routine")
del bwRoutMan
bwServer.stop()
bcServer.stop()
if bwRoutMan:
bwRoutMan.cleanup()
if bwServer:
bwServer.stop()
if bcServer:
bcServer.stop()
logging.debug("BOSWatch server has stopped ...")

View file

@ -19,8 +19,12 @@ router:
- name: Router 1
route:
- type: module
name: filter.modeFilter
res: filter.modeFilter
name: Filter Fms/Zvei
config:
allowed:
- fms
- zvei
- type: plugin
name: test plugin
res: template_plugin

View file

@ -110,7 +110,8 @@ Jeder Router kann eine beliebige Anzahl einzelner Routenpunkte enthalten. Diese
|Feld|Beschreibung|Default|
|----|------------|-------|
|type|Art des Routenpunktes (module, plugin, router)||
|name|Zu ladende Resource (Siehe entsprechende Kapitel)||
|res|Zu ladende Resource (Siehe entsprechende Kapitel)||
|name|Optionaler Name des Routenpunktes|gleich wie Resource|
|config|Konfigurationseinstellungen des Routenpunktes (Siehe entsprechende Kapitel)||
**Beispiel:**
@ -119,7 +120,8 @@ router:
- name: Router 1
route:
- type: module
name: filter.modeFilter
res: filter.modeFilter
name: Filter Fms/Zvei
config:
allowed:
- fms

View file

@ -33,7 +33,8 @@ Die Plugin Basisklasse bietet einige Methoden, welche vom Plugin überschrieben
Jedes Modul oder Plugin wird in einem Router folgendermaßen deklariert:
```yaml
- type: module # oder 'plugin'
name: template_module # Name der Python Datei (ohne .py)
res: template_module # Name der Python Datei (ohne .py)
name: Mein Modul # optionaler Name
config: # config-Sektion
option1: value 1
option2:
@ -43,7 +44,7 @@ Jedes Modul oder Plugin wird in einem Router folgendermaßen deklariert:
- list 1
- list 2
```
Eine entsprechende Dokumentation der Parameter ist in der Dokumentation der [Konfiguration](../config.md) zu hinterlegen.
Eine entsprechende Dokumentation der Parameter **muss** in der Dokumentation des jeweiligen Moduls oder Plugins hinterleget werden.
### Konfiguration verwenden
Wird der Instanz eine Konfiguration übergeben wird diese in `self.config` abgelegt und kann wie folgt abgerufen werden:
@ -57,7 +58,7 @@ Wird der Instanz eine Konfiguration übergeben wird diese in `self.config` abgel
`self.config.get("option2", "underOption1")`
> liefert `value 21`
- Es kann ein Default Wert angegeben werden
- Es kann ein Default Wert angegeben werden (falls entsprechender Eintrag fehlt)
`self.config.get("notSet", default="defValue")`
> liefert `defValue`
@ -75,30 +76,58 @@ Aus dieser kann mittels `bwPacket.get(FIELDNAME)` das entsprechende Feld ausgele
Mittels `bwPacket.set(FIELDNAME, VALUE)` kann ein Wert hinzugefügt oder modifiziert werden.
Eine Auflistung der bereitgestellten Informationen findet sich im entsprechenden [BOSWatch Paket](packet.md) Dokumentation.
Bitte beachten:
**Bitte beachten:**
- Selbst vom Modul hinzugefügte Felder **müssen** in der Modul Dokumentation unter `Paket Modifikation` aufgeführt werden.
- Sollte ein Modul oder Plugin Felder benutzen, welche in einem anderen Modul erstellt werden, **muss** dies im Punkt `Abhänigkeiten` des jeweiligen Moduls oder Plugins zu dokumentieren.
- Sollte ein Modul oder Plugin Felder benutzen, welche in einem anderen Modul erstellt werden, **muss** dies im Punkt `Abhänigkeiten` des jeweiligen Moduls oder Plugins dokumentiert werden.
### Zu beachten bei Module
### Rückgabewert bei Modulen
Module können Pakete beliebig verändern. Diese Änderungen werden im Router entsprechend weitergeleitet.
Mögliche Rückgabewerte eines Moduls:
- `return bwPacket` gibt das modifizierte bwPacket an den Router zurück
- `return None` Router fährt mit dem unveränderten bwPacket fort (Input = Output)
- `return False` Router stopt sofort die Ausführung (zB. in Filtern verwendet)
- `return bwPacket` Gibt das modifizierte bwPacket an den Router zurück (Paket Modifikation)
- `return None` Der Router fährt mit dem unveränderten bwPacket fort (Input = Output)
- `return False` Der Router stopt sofort die Ausführung (zB. in Filtern verwendet)
### Zu beachten bei Plugins
### Rückgabewert bei Plugins
Plugins geben keine Pakete mehr zurück. Sie fungieren ausschließlich als Endpunkt.
Die Plugin Basisklasse liefert intern immer ein `None` an den Router zurück,
was zur weiteren Ausführung des Routers mit dem original Paket führt. Daher macht es in Plugins keinen Sinn ein Paket zu modifizieren.
---
## Nutzung der Wildcards
Es gibt einige vordefinierte Wildcards welche in der [BOSWatch Paket](packet.md) Dokumentation zu finden sind.
Außerdem sind die folgenden allgemeinen Wildcards definiert:
- `{BR}` - Zeilenumbruch `\r\n`
- `{LPAR}` - öffnende Klammer `(`
- `{RPAR}` - schließende Klammer `)`
- `{TIME}` - Aktueller Zeitstempel im Format `%d.%m.%Y %H:%M:%S`
### Wildcards registrieren [Module]
Module können zusätzliche Wildcards registrieren welche anschließend in den Plugins ebenfalls geparst werden können.
Dies kann über die interne Methode `self.registerWildcard(newWildcard, bwPacketField)` gemacht werden.
- `newWildcard` muss im folgenden Format angegeben werden: `{WILDCARD}`
- `bwPacketField` ist der Name des Feldes im bwPacket - gestezt per `bwPacket.set(FIELDNAME, VALUE)`
**Bitte beachten:**
- Selbst vom Modul registrierte Wildcards **müssen** in der Modul Dokumentation unter `Zusätzliche Wildcards` aufgeführt werden.
### Wildcards parsen [Plugins]
Das parsen der Wildcars funktioniert komfortabel über die interne Methode `msg = self.parseWildcards(msg)`.
- `msg` enstrpicht dabei dem String in welchem die Wildcards ersetzt werden sollen
Die Platzhalter der Wildcards findet man in der [BOSWatch Paket](packet.md) Dokumentation.
Sollten Module zusätzliche Wildcards registrieren, findet man Informationen dazu in der jeweiligen Modul Dokumentation
---
## Richtiges Logging
tbd ...
---
## Wildcards parsen (Plugin only)
Das parsen der Wildcars funktioniert komfortabel über die interne Methode `self.parseWildcards(MSG)`.
Die Platzhalter der Wildcards findet man in der [BOSWatch Paket](packet.md) Dokumentation.

View file

@ -1,4 +1,4 @@
# <center>BOSWatch Packet Format</center>
# <center>BOSWatch Paket Format</center>
Ein BOSWatch Datenpaket wird in einem Python Dict abgebildet. In der nachfolgenden Tabelle sind die genutzten Felder abgebildet.
@ -48,13 +48,4 @@ Ein BOSWatch Datenpaket wird in einem Python Dict abgebildet. In der nachfolgend
|status|X||||`{STAT}`||
|direction|X||||`{DIR}`||
|dirextionText|X||||`{DIRT}`|(Fhz->Lst, Lst->Fhz)|
|vehicle|X||||`{VEC}`||
|vehicle|X||||`{VEC}`||
|tacticalInfo|X||||`{TACI}`|(I, II, III, IV)|
---
## Weitere Wildcards
- `{BR}` - Zeilenumbruch `\r\n`
- `{LPAR}` - öffnende Klammer `(`
- `{RPAR}` - schließende Klammer `)`
- `{TIME}` - Aktueller zeitstempel

View file

@ -0,0 +1 @@
<mxfile host="www.draw.io" modified="2019-10-26T07:55:48.917Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/77.0.3865.90 Chrome/77.0.3865.90 Safari/537.36" etag="82d1pq0jwhTcNaaLB_Ck" version="12.1.7" type="device" pages="1"><diagram id="9aEBdlF2oZdVepulqs3T" name="Page-1">7Vjbcts2EP0aPdrDi0gpj5FsJ9Nxpm7VadMnD0iuSNQgwYJLU+rXFzeSoijJlyhOMq09o9EeAIvdPdhDQhN/mW8+CFJmn3gCbOI5yWbiX008bx5M5acCtgbwZ74BUkETA7k9sKL/gAUdi9Y0gWowETlnSMshGPOigBgHGBGCN8Npa86Gu5YkhRGwigkbo3/QBDObljfr8Y9A06zd2Q3fmZGctJNtJlVGEt7sQP71xF8KztF8yzdLYKp2bV3Mupsjo11gAgp8zoKmWa0XC//q4Sf8rYohvZn/vrqY29hw2yYMiczfmlxgxlNeEHbdowvB6yIB5dWRVj/nlvNSgq4E/wLErSWT1MgllGHO7ChsKH5Wyy/DmTX/1GZgrauNda6N7Y5xB4LmgCBarECx/bxr7HhSZu9KW62vcfFsPSteixhOVKw9hESkgCfmeWaeKufOBpaaD8BlFmIrJwhgBOnj8LgRe2rTbl5PrPxiuX0Bz9bvI2G13WlE/CMIpPLgv2c0LSSEis1Fi96SCNgdryhSrkYjjshzOYHtDcSyooqeBbF+OmDnBPAaGS1g2TWt4mRNGVtyxoWOx1/PY4hjiVco+APsjERSVQK1IhUkodL9FRXSjdm/UAeyW9X2rKeQjJQq13yTKqm6LAAbLh6qS+WF3yNvdJhHj4YqBWxOktmqnm15q3mhNZteQILAQNmudjjOV2I//CZd/uat6T2zNYPvqjW9A60ZMlQNwWUFvDBFXRWDRfuAmbTDbfh3zduBi0qz815OcKflph9svfwKMagMZdUyASRpvcpMDu0u4VEAkXgaqUpSPCtI/3SQpEjUuwTE8jVDx6w+E4JEd5dsLi8kuZKtIqrKYdwmhGFY9PUxCWT363yinrndFvS8aetkT8Y/LvQXZJTX8q0q58VFkZ7KaXQy9pRlqBtNRhFWJdF920jZHWrESNod/Td+GNws1f951NkNn5RnN3DeUp9dd1TGNxDo1wvt9NwCapfecarFzBIV7hHlunsEGKW3q/Y46MJ4PS3Tb6nMnwgtvm9ZXoFWYyPCssUfAPXtrMMqELIrVZXXHRbxqiEYZ52M6/HKXJjijCrxsVLu0KKs0aj7C2Xwxxep6fRpkZq9qUjN/hsvkcEzXyLProFfRE5wVKo6YfilBjl2UDH2eG0vSvFWXtISfSl6okUiw/Nt1AFSDlLN/s/mqnesl7q73LHb3xl6yZ8Gg16ajXspPNBK86/VSe8OkPX/Zdxexhu5nEFV3efy4ZCf5wCE+2IajMXUOXQCghefAGn2P+mZF5H+d1H/+l8=</diagram></mxfile>

BIN
docu/docs/img/client.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1 @@
<mxfile host="www.draw.io" modified="2019-10-28T07:09:19.767Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36" etag="YtlXSxxCTdQhtX4skT94" version="12.1.7" type="device" pages="1"><diagram id="LuQiMuTTxK4eFKppVUgf" name="Page-1">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</diagram></mxfile>

BIN
docu/docs/img/router.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -0,0 +1 @@
<mxfile host="www.draw.io" modified="2019-10-26T14:21:37.009Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/77.0.3865.90 Chrome/77.0.3865.90 Safari/537.36" etag="lnG6lN8TfbR21d0bu-ug" version="12.1.7" type="device" pages="1"><diagram id="9aEBdlF2oZdVepulqs3T" name="Page-1">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</diagram></mxfile>

BIN
docu/docs/img/server.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -3,10 +3,6 @@
<center>
![BOSWatch](img/bw3.png "BOSWatch 3 Logo")
Falls du uns unterstützen möchtest würden wir uns über eine Spende freuen.
Server, Hosting, Domain sowie Kaffee kosten leider Geld ;-)
[![](https://www.paypalobjects.com/de_DE/DE/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CLK9VBN2MSLZY&source=url)
</center>
**Es wird darauf hingewiesen, dass für die Teilnahme am BOS-Funk nur nach den Technischen Richtlinien der BOS zugelassene Funkanlagen verwendet werden dürfen.**
@ -15,3 +11,12 @@ Server, Hosting, Domain sowie Kaffee kosten leider Geld ;-)
---
**The intercept of the German BOS radio is strictly prohibited and will be prosecuted. The use is only permitted for authorized personnel.**
---
<center>
Falls du uns unterstützen möchtest würden wir uns über eine Spende freuen.
Server, Hosting, Domain sowie Kaffee kosten leider Geld ;-)
[![](https://www.paypalobjects.com/de_DE/DE/i/btn/btn_donate_LG.gif)](https://www.paypal.me/BSchroll)
</center>

View file

@ -1,6 +1,6 @@
# <center>Broadcast Service</center>
Durch den Broadcast Service haben CLients die Möglichkeit, automatisch den Server zu finden und sich mit diesem zu verbinden. Dazu stellt der Server die benötigten Verbinungsinformationen per Broadcast Service bereit.
Durch den Broadcast Service haben Clients die Möglichkeit, automatisch den Server zu finden und sich mit diesem zu verbinden. Dazu stellt der Server die benötigten Verbinungsinformationen per Broadcast Service bereit.
**Hinweis:** *Server und Client müssen sich im selben Subnetz befinden.*
@ -8,7 +8,7 @@ Durch den Broadcast Service haben CLients die Möglichkeit, automatisch den Serv
## Aufbau
Der Broadcast Service besteht aus 2 Teilen - einem Server und einem Clienten.
Nachfolgend soll der Ablauf einer Verbunding des Clienen zum Server mittels des Broadcast Services erklärt werden.
Nachfolgend soll der Ablauf einer Verbindung des Clienten zum Server mittels des Broadcast Services erklärt werden.
<center>![](../img/broadcast.png)</center>
@ -16,16 +16,16 @@ Nachfolgend soll der Ablauf einer Verbunding des Clienen zum Server mittels des
## Ablauf
### Schritt 1 - Broadcast Server starten
Im ersten Schritt wird auf dem Server ein zusätzlicher Broadcast Server in einem seperaten Thread gestartet. Dieser lauscht auf einem festgelegten Port auf UDP Broadcast Pakete. Nun kann eine beliebige Anzahl von Clienten mittels des Broadcast Services die Verbinundgdaten des Server abfragen.
Im ersten Schritt wird auf dem Server ein zusätzlicher Broadcast Server in einem seperaten Thread gestartet. Dieser lauscht auf einem festgelegten Port auf UDP Broadcast Pakete. Nun kann eine beliebige Anzahl von Clienten mittels des Broadcast Services die Verbindungsgdaten des Servers abfragen.
### Schritt 2 - Broadcast durch Clienten
Die Client Applikation startet nun zur Abfrage der Verbindungsdaten einen BC Clienten und sendet dort auf dem festgelegten Port ein Paket per UDP Boradcast. Der Inhalt des Paketes ist das Magic-Word `<BW3-Request>` und wird von allen im selben Subnetz vohandenen Gegenstellen empfangen. Nun wird auf eine Antwort des Broadcast Server mit den Verbindungsdaten gewartet.
Die Client Applikation startet nun zur Abfrage der Verbindungsdaten einen BC Clienten und sendet dort auf dem festgelegten Port ein Paket per UDP Boradcast. Der Inhalt des Paketes ist das Magic-Word `<BW3-Request>` und wird von allen im selben Subnetz befindlichen Gegenstellen empfangen. Nun wartet der Client auf eine Antwort des Broadcast Server mit den Verbindungsdaten.
### Schritt 3 - Verbindungsdaten senden
Wird nun ein Broadcast Paket empfangen, prüft der BC Server die Daten auf das Magic-Word `<BW3-Request>`. Wird dieses erkannt, liest der Server die Absender-IP-Addresse aus dem Paket aus und sendet eine Antwort direkt an diesen Clienten. Dieses Antwortpaket sieht folgendermaßen aus: `<BW3-Result>;8080` wobei die `8080` hier den normalen TCP Kommunikationsport des Server darstellt.
Wird nun ein Broadcast Paket empfangen, prüft der BC Server die Daten auf das Magic-Word `<BW3-Request>`. Wird dieses erkannt, liest der Server die Absender-IP-Addresse aus dem Paket aus und sendet eine Antwort direkt an diesen Clienten. Dieses Antwortpaket sieht folgendermaßen aus: `<BW3-Result>;8080` wobei die `8080` hier den normalen TCP Kommunikationsport des Servers darstellt.
### Schritt 4 - Verbindungsdaten empfangen
Nachdem der Client das direkt an ihn gerichtete Paket mit den Verbindungsdaten vom Server empfangen hat, prüft er auf das Magic-Word `<BW3-Result>`. Ist dieses enthalten wird der Port für die TCP Verbundindung aus dem Paket extrahiert. Außerdem wird die IP-Addresse des Absenders aus dem Paket gelesen.
Anschließend stehen dem Clienten die Verbindungsdaten des Servers zur Verfügung und er kann sich per TCP über den angegebenen Port mit dem BOSWatch Server verbindden um seine Alarmierungs-Pakete zu versenden.
Nachdem der Client das direkt an ihn gerichtete Paket mit den Verbindungsdaten vom Server empfangen hat, prüft er auf das Magic-Word `<BW3-Result>`. Ist dieses enthalten wird der Port für die TCP Verbindung aus dem Paket extrahiert. Außerdem wird die IP-Addresse des Absenders aus dem Paket gelesen.
Anschließend stehen dem Clienten die Verbindungsdaten des Servers zur Verfügung und er kann sich per TCP auf den angegebenen Port mit dem BOSWatch Server verbinden um seine Alarmierungs-Pakete abzusetzen.
Da der Broadcast Server in einem eigenen Thread, unabhängig vom Hauptprogram läuft, können ganz einfach weitere Clienten per Broadcast Service die Verbindungsdaten des Servers abrufen.

View file

@ -0,0 +1,93 @@
# <center>Routing Mechanismus</center>
BOSWatch 3 hat einen Routing Mechanismus integriert. Mit diesem ist es auf einfache Weise möglich, den Verlauf von Alarmpaketen zu steuern.
---
## Ablauf
Nachfolgender Ablauf soll am Beispiel eines Alarms mit einem Pocsag Paket erklärt werden.
<center>![](../img/router.png)</center>
- BOSWatch startet alle Router, welche in der config als `alarmRouter` konfiguriert worden sind (in diesem Fall nur `Router1`)
- Der Router `Router1` beginnt seine Ausführung und arbeitet die einzelnen Routenpunkte sequentiell ab
- Das Modul `descriptor` wird aufgerufen und fügt ggf. Beschreibungen zum Paket hinzu
- Das Modul `doubleFilter` wird aufgerufen und blockiert doppelte Alarme
(hier würde die Ausführung dieses Routers und damit des kompletten Alarmprozesses stoppen wenn der Alarm als doppelter erkannt würde)
- Der Router `Router2` wir nun aufgerufen (bis zur Rückkehr aus `Router2` ist der Router `Router1` angehalten)
- Der Router `Router2` beginnt seine Ausführung und arbeitet die einzelnen Routenpunkte sequentiell ab
- Das Modul `modeFilter` wird aufgerufen und stoppt den Router da es sich nicht um ein FMS Paket handelt
- Es wird zur Ausführung von `Router1` zurückgekehrt
- Der Router `Router3` beginnt seine Ausführung und arbeitet die einzelnen Routenpunkte sequentiell ab
- Das Modul `modeFilter` wird aufgerufen und leitet das Paket weiter da es sich um ein Pocsag Paket handelt
- Das Plugin `Telegram` wird aufgerufen
- Das Plugin `MySQL` wird augerufen
- Es wird zur Ausführung von `Router1` zurückgekehrt
- Der Router `Router1` setzt seine Ausführung fort
- Das Modul `modeFilter` wird aufgerufen und stoppt den Router da es sich nicht um ein ZVEI Paket handelt
Jetzt sind alle Routenpunkte abgearbeitet und die Alarmierung damit abgeschlossen.
---
## Konfiguration
Nachfolgend ist die Router Konfiguration des BW3-Servers für das obige Beispiel zu finden:
```yaml
alarmRouter:
- Router1
router:
- name: Router1
route:
- type: module
res: descriptor
config:
[...]
- type: module
res: filter.doubleFilter
config:
[...]
- type: router
res: Router2
- type: router
res: Router3
- type: module
res: filter.modeFilter
config:
allowed:
- zvei
- type: plugin
res: sms
config:
[...]
- name: Router2
route:
- type: module
res: filter.modeFilter
config:
allowed:
- fms
- type: plugin
res: mysql
config:
[...]
- name: Router3
route:
- type: module
res: filter.modeFilter
config:
allowed:
- pocsag
- type: plugin
res: telegram
config:
[...]
- type: plugin
res: mysql
config:
[...]
```

View file

@ -17,6 +17,8 @@ nachträglich an den Server übermittelt werden.
Dabei überwacht der Client selbstständig die benötigten Programme zum Empfang der Daten und startet diese bei einem Fehler ggf. neu.
<center>![](../img/client.png)</center>
---
## BOSWatch Server
@ -26,4 +28,6 @@ Verarbeitung der Daten.
Auch hier werden die empfangenen Daten in From von bwPacket's in einer Queue abelegt um zu gewährleisten, das auch während einer länger
dauernden Plugin Ausführung alle Pakete korrekt empfangen werden können und es zu keinen Verlusten kommt.
Die Verarbeitung der Pakete geschieht anschließend in sogenannten Routern, welche aufgrund ihres Umfangs jedoch in einem eigenen Kapitel
erklärt werden. Diese steuern die Verteilung der Daten an die einzelnen Plugins.
erklärt werden. Diese steuern die Verteilung der Daten an die einzelnen Plugins.
<center>![](../img/server.png)</center>

View file

@ -0,0 +1,66 @@
# <center>Descriptor</center>
---
## Beschreibung
Mit diesem Modul können einem Alarmpaket beliebige Beschreibungen in Abhänigkeit der enthaltenen Informationen hinzugefügt werden.
## Resource
`descriptor`
## Konfiguration
Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md)
|Feld|Beschreibung|Default|
|----|------------|-------|
|scanField|Feld des BW Pakets welches geprüft werden soll||
|descrField|Name des Feldes im BW Paket in welchem die Beschreibung gespeichert werden soll||
|wildcard|Optional: Es kann für das angelegte `descrField` automatisch ein Wildcard registriert werden|None|
|descriptions|Liste der Beschreibungen||
#### `descriptions:`
|Feld|Beschreibung|Default|
|----|------------|-------|
|for|Inhalt im `scanField` auf welchem geprüft werden soll||
|add|Beschreibungstext welcher im `descrField` hinterlegt werden soll||
**Beispiel:**
```yaml
- type: module
res: descriptor
config:
- scanField: zvei
descrField: description
wildcard: "{DESCR}"
descriptions:
- for: 12345
add: FF DescriptorTest
- for: 45678
add: FF TestDescription
- scanField: status
descrField: fmsStatDescr
wildcard: "{STATUSTEXT}"
descriptions:
- for: 1
add: Frei (Funk)
- for: 2
add: Frei (Wache)
- ...
```
---
## Abhängigkeiten
- keine
---
## Paket Modifikationen
- Wenn im Paket das Feld `scanField` vorhanden ist, wird das Feld `descrField` dem Paket hinzugefügt
- Wenn keine Beschreibung vorhanden ist, wird im Feld `descrField` der Inhalt des Feldes `scanField` hinterlegt
---
## Zusätzliche Wildcards
- Von der Konfiguration abhängig

View file

@ -2,7 +2,7 @@
---
## Beschreibung
Mit diesem Modul ist es Möglich, die Pakete auf bestimmte Modes (FMS, POCSAG, ZVEI) zu Filtern. Je nach Konfiguration werden Pakete eines bestimmten Modes im aktuellen Router weitergeleitet oder verworfen.
Mit diesem Modul ist es möglich, die Pakete auf bestimmte Modes (FMS, POCSAG, ZVEI) zu Filtern. Je nach Konfiguration werden Pakete eines bestimmten Modes im aktuellen Router weitergeleitet oder verworfen.
## Resource
`filter.modeFilter`
@ -16,7 +16,7 @@ Mit diesem Modul ist es Möglich, die Pakete auf bestimmte Modes (FMS, POCSAG, Z
**Beispiel:**
```yaml
- type: module
name: filter.modeFilter
res: filter.modeFilter
config:
allowed:
- fms
@ -32,3 +32,8 @@ Mit diesem Modul ist es Möglich, die Pakete auf bestimmte Modes (FMS, POCSAG, Z
## Paket Modifikationen
- keine
---
## Zusätzliche Wildcards
- keine

View file

@ -0,0 +1,65 @@
# <center>Regex Filter</center>
---
## Beschreibung
Mit diesem Modul ist es möglich, komplexe Filter basierend auf Regulären Ausdrücken (Regex) anzulegen.
Für einen Filter können beliebig viele Checks angelegt werden, welche Felder eines BOSWatch Pakets mittels Regex prüfen.
Folgendes gilt:
- Die Filter werden nacheinander abgearbeitet
- Innerhalb des Filters werden die Checks nacheinander abgearbeitet
- Sobald ein einzelner Check fehlschlägt ist der ganze Filter fehlgeschlagen
- Sobald ein Filter mit all seinen Checks besteht, wird mit der Ausführung des Routers fortgefahren
- Sollten alle Filter fehlschlagen wird die Ausführung des Routers beendet
Vereinfacht kann man sagen, dass einzelnen Router ODER-verknüpft und die jeweiligen Checks UND-verknüpft sind.
## Resource
`filter.regexFilter`
## Konfiguration
|Feld|Beschreibung|Default|
|----|------------|-------|
|name|Beliebiger Name des Filters||
|checks|Liste der einzelnen Checks innerhalb des Filters||
#### `checks:`
|Feld|Beschreibung|Default|
|----|------------|-------|
|field|Name des Feldes innerhalb des BOSWatch Pakets welches untersucht werden soll||
|regex|Regulärer Ausdruck (Bei Sonderzeichen " " verwenden)||
**Beispiel:**
```yaml
- type: module
res: filter.regexFilter
config:
- name: "Zvei filter"
checks:
- field: zvei
regex: "65[0-9]{3}" # all zvei with starting 65
- name: "FMS Stat 3"
checks:
- field: mode
regex: "fms" # check if mode is fms
- field: status
regex: "3" # check if status is 3
```
---
## Abhängigkeiten
- keine
---
## Paket Modifikationen
- keine
---
## Zusätzliche Wildcards
- keine

View file

@ -15,10 +15,12 @@ nav:
- Server/Cient Prinzip: information/serverclient.md
- Broadcast Service: information/broadcast.md
# - Modul/Plugin Konzept: tbd.md
# - Routing Mechanismus: tbd.md
- Routing Mechanismus: information/router.md
- Changelog: changelog.md
- Module:
- Mode Filter: modul/mode_filter.md
- Regex Filter: modul/regex_filter.md
- Descriptor: modul/descriptor.md
- Plugins: tbd.md
- Entwickler:
- Eigenes Modul/Plugin schreiben: develop/ModulPlugin.md

58
module/descriptor.py Normal file
View file

@ -0,0 +1,58 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: descriptor.py
@date: 27.10.2019
@author: Bastian Schroll
@description: Module to add descriptions to bwPackets
"""
import logging
from module.moduleBase import ModuleBase
# ###################### #
# Custom plugin includes #
# ###################### #
logging.debug("- %s loaded", __name__)
class BoswatchModule(ModuleBase):
"""!Adds descriptions to bwPackets"""
def __init__(self, config):
"""!Do not change anything here!"""
super().__init__(__name__, config) # you can access the config class on 'self.config'
def onLoad(self):
"""!Called by import of the plugin"""
for descriptor in self.config:
if descriptor.get("wildcard", default=None):
self.registerWildcard(descriptor.get("wildcard"), descriptor.get("descrField"))
def doWork(self, bwPacket):
"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance"""
for descriptor in self.config:
for description in descriptor.get("descriptions"):
if not bwPacket.get(descriptor.get("scanField")):
break # scanField is not available in this packet
bwPacket.set(descriptor.get("descrField"), description.get("for"))
if str(description.get("for")) == bwPacket.get(descriptor.get("scanField")):
logging.debug("Description '%s' added in packet field '%s'",
description.get("add"), descriptor.get("descrField"))
bwPacket.set(descriptor.get("descrField"), description.get("add"))
break # this descriptor has found a description - run next descriptor
return bwPacket
def onUnload(self):
"""!Called by destruction of the plugin"""
pass

View file

@ -15,7 +15,7 @@
@description: Filter module for the packet type
"""
import logging
from module.module import Module
from module.moduleBase import ModuleBase
# ###################### #
# Custom plugin includes #
@ -25,7 +25,7 @@ from module.module import Module
logging.debug("- %s loaded", __name__)
class BoswatchModule(Module):
class BoswatchModule(ModuleBase):
"""!Filter of specific bwPacket mode"""
def __init__(self, config):
"""!Do not change anything here!"""
@ -38,8 +38,7 @@ class BoswatchModule(Module):
def doWork(self, bwPacket):
"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance
@return bwPacket or False"""
@param bwPacket: A BOSWatch packet instance"""
for mode in self.config.get("allowed", default=[]):
if bwPacket.get("mode") == mode:

View file

@ -0,0 +1,65 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""!
____ ____ ______ __ __ __ _____
/ __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ /
/ __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ <
/ /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ /
/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/
German BOS Information Script
by Bastian Schroll
@file: regexFilter.py
@date: 26.10.2019
@author: Bastian Schroll
@description: Regex filter module
"""
import logging
from module.moduleBase import ModuleBase
# ###################### #
# Custom plugin includes #
import re
# ###################### #
logging.debug("- %s loaded", __name__)
class BoswatchModule(ModuleBase):
"""!Regex based filter mechanism"""
def __init__(self, config):
"""!Do not change anything here!"""
super().__init__(__name__, config) # you can access the config class on 'self.config'
def onLoad(self):
"""!Called by import of the plugin"""
pass
def doWork(self, bwPacket):
"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance"""
for regexFilter in self.config:
checkFailed = False
logging.debug("try filter '%s' with %d check(s)", regexFilter.get("name"), len(regexFilter.get("checks")))
for check in regexFilter.get("checks"):
fieldData = bwPacket.get(check.get("field"))
if not fieldData or not re.search(check.get("regex"), fieldData):
logging.debug("[-] field '%s' with regex '%s'", check.get("field"), check.get("regex"))
checkFailed = True
break # if one check failed we break this filter
else:
logging.debug("[+] field '%s' with regex '%s'", check.get("field"), check.get("regex"))
if not checkFailed:
logging.debug("[PASSED] filter '%s'", regexFilter.get("name"))
return None # None -> Router will go on with this packet
logging.debug("[FAILED] filter '%s'", regexFilter.get("name"))
return False # False -> Router will stop further processing
def onUnload(self):
"""!Called by destruction of the plugin"""
pass

View file

@ -9,32 +9,34 @@
German BOS Information Script
by Bastian Schroll
@file: module.py
@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 Module:
class ModuleBase(ABC):
"""!Main module class"""
_modulesActive = 0
_modulesActive = []
def __init__(self, moduleName, config):
"""!init preload some needed locals and then call onLoad() directly"""
self._moduleName = moduleName
self.config = config
self._modulesActive += 1
self._modulesActive.append(self)
# for time counting
self._cumTime = 0
self._moduleTime = 0
self._tmpTime = 0
# for statistics
self._runCount = 0
@ -43,28 +45,28 @@ class Module:
logging.debug("[%s] onLoad()", moduleName)
self.onLoad()
def __del__(self):
"""!Destructor calls onUnload() directly"""
def _cleanup(self):
"""!Cleanup routine calls onUnload() directly"""
logging.debug("[%s] onUnload()", self._moduleName)
self._modulesActive -= 1
self._modulesActive.remove(self)
self.onUnload()
def _run(self, bwPacket):
"""!start an rund of the module.
"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance
@return bwPacket or False"""
self._runCount += 1
logging.debug("[%s] run #%d", self._moduleName, self._runCount)
self._tmpTime = time.time()
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() - self._tmpTime
self._moduleTime = time.time() - tmpTime
self._cumTime += self._moduleTime
@ -76,7 +78,8 @@ class Module:
"""!Returns statistical information's from last module run
@return Statistics as pyton dict"""
stats = {"runCount": self._runCount,
stats = {"type": "module",
"runCount": self._runCount,
"cumTime": self._cumTime,
"moduleTime": self._moduleTime,
"moduleErrorCount": self._moduleErrorCount}
@ -84,17 +87,31 @@ class Module:
def onLoad(self):
"""!Called by import of the module
Must be inherit"""
can be inherited"""
pass
def doWork(self, bwPacket):
"""!Called module run
Must be inherit
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("no functionality in module %s", self._moduleName)
def onUnload(self):
"""!Called by destruction of the module
Must be inherit"""
"""!Called on shutdown of boswatch
can be inherited"""
pass
@staticmethod
def registerWildcard(newWildcard, bwPacketField):
"""!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)

View file

@ -15,7 +15,7 @@
@description: Template Module File
"""
import logging
from module.module import Module
from module.moduleBase import ModuleBase
# ###################### #
# Custom plugin includes #
@ -25,21 +25,21 @@ from module.module import Module
logging.debug("- %s loaded", __name__)
class BoswatchModule(Module):
class BoswatchModul(ModuleBase):
"""!Description of the Module"""
def __init__(self, config):
"""!Do not change anything here!"""
super().__init__(__name__, config) # you can access the config class on 'self.config'
def onLoad(self):
"""!Called by import of the plugin"""
"""!Called by import of the plugin
Remove if not implemented"""
pass
def doWork(self, bwPacket):
"""!start an run of the module.
@param bwPacket: A BOSWatch packet instance
@return bwPacket or False"""
@param bwPacket: A BOSWatch packet instance"""
if bwPacket.get("mode") == "fms":
pass
elif bwPacket.get("mode") == "zvei":
@ -52,5 +52,6 @@ class BoswatchModule(Module):
return bwPacket
def onUnload(self):
"""!Called by destruction of the plugin"""
"""!Called by destruction of the plugin
Remove if not implemented"""
pass

View file

@ -9,29 +9,30 @@
German BOS Information Script
by Bastian Schroll
@file: plugin.py
@file: pluginBase.py
@date: 08.01.2018
@author: Bastian Schroll
@description: Plugin main class to inherit
"""
import logging
import time
from abc import ABC
from boswatch import wildcard
logging.debug("- %s loaded", __name__)
class Plugin:
class PluginBase(ABC):
"""!Main plugin class"""
_pluginsActive = 0
_pluginsActive = []
def __init__(self, pluginName, config):
"""!init preload some needed locals and then call onLoad() directly"""
self._pluginName = pluginName
self.config = config
self._pluginsActive += 1
self._pluginsActive.append(self)
# to save the packet while alarm is running for other functions
self._bwPacket = None
@ -42,7 +43,6 @@ class Plugin:
self._setupTime = 0
self._alarmTime = 0
self._teardownTime = 0
self._tmpTime = 0
# for statistics
self._runCount = 0
@ -53,10 +53,10 @@ class Plugin:
logging.debug("[%s] onLoad()", pluginName)
self.onLoad()
def __del__(self):
"""!Destructor calls onUnload() directly"""
def _cleanup(self):
"""!Cleanup routine calls onUnload() directly"""
logging.debug("[%s] onUnload()", self._pluginName)
self._pluginsActive -= 1
self._pluginsActive.remove(self)
self.onUnload()
def _run(self, bwPacket):
@ -70,7 +70,7 @@ class Plugin:
self._bwPacket = bwPacket
self._tmpTime = time.time()
tmpTime = time.time()
try:
logging.debug("[%s] setup()", self._pluginName)
self.setup()
@ -78,8 +78,8 @@ class Plugin:
self._setupErrorCount += 1
logging.exception("[%s] error in setup()", self._pluginName)
self._setupTime = time.time() - self._tmpTime
self._tmpTime = time.time()
self._setupTime = time.time() - tmpTime
tmpTime = time.time()
try:
if bwPacket.get("mode") == "fms":
@ -98,8 +98,8 @@ class Plugin:
self._alarmErrorCount += 1
logging.exception("[%s] alarm error", self._pluginName)
self._alarmTime = time.time() - self._tmpTime
self._tmpTime = time.time()
self._alarmTime = time.time() - tmpTime
tmpTime = time.time()
try:
logging.debug("[%s] teardown()", self._pluginName)
self.teardown()
@ -107,7 +107,7 @@ class Plugin:
self._teardownErrorCount += 1
logging.exception("[%s] error in teardown()", self._pluginName)
self._teardownTime = time.time() - self._tmpTime
self._teardownTime = time.time() - tmpTime
self._sumTime = self._setupTime + self._alarmTime + self._teardownTime
self._cumTime += self._sumTime
@ -124,7 +124,8 @@ class Plugin:
"""!Returns statistical information's from last plugin run
@return Statistics as pyton dict"""
stats = {"runCount": self._runCount,
stats = {"type": "plugin",
"runCount": self._runCount,
"sumTime": self._sumTime,
"cumTime": self._cumTime,
"setupTime": self._setupTime,
@ -137,50 +138,50 @@ class Plugin:
def onLoad(self):
"""!Called by import of the plugin
Must be inherit"""
can be inherited"""
pass
def setup(self):
"""!Called before alarm
Must be inherit"""
can be inherited"""
pass
def fms(self, bwPacket):
"""!Called on FMS alarm
Must be inherit
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("ZVEI not implemented in %s", self._pluginName)
def pocsag(self, bwPacket):
"""!Called on POCSAG alarm
Must be inherit
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("POCSAG not implemented in %s", self._pluginName)
def zvei(self, bwPacket):
"""!Called on ZVEI alarm
Must be inherit
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("ZVEI not implemented in %s", self._pluginName)
def msg(self, bwPacket):
"""!Called on MSG packet
Must be inherit
can be inherited
@param bwPacket: bwPacket instance"""
logging.warning("MSG not implemented in %s", self._pluginName)
def teardown(self):
"""!Called after alarm
Must be inherit"""
can be inherited"""
pass
def onUnload(self):
"""!Called by destruction of the plugin
Must be inherit"""
"""!Called on shutdown of boswatch
can be inherited"""
pass
def parseWildcards(self, msg):

View file

@ -15,7 +15,7 @@
@description: Template Plugin File
"""
import logging
from plugin.plugin import Plugin
from plugin.pluginBase import PluginBase
# ###################### #
# Custom plugin includes #
@ -25,46 +25,56 @@ from plugin.plugin import Plugin
logging.debug("- %s loaded", __name__)
class BoswatchPlugin(Plugin):
class BoswatchPlugin(PluginBase):
"""!Description of the Plugin"""
def __init__(self, config):
"""!Do not change anything here!"""
super().__init__(__name__, config) # you can access the config class on 'self.config'
def onLoad(self):
"""!Called by import of the plugin"""
"""!Called by import of the plugin
Remove if not implemented"""
pass
def setup(self):
"""!Called before alarm"""
"""!Called before alarm
Remove if not implemented"""
pass
def fms(self, bwPacket):
"""!Called on FMS alarm
@param bwPacket: bwPacket instance"""
@param bwPacket: bwPacket instance
Remove if not implemented"""
pass
def pocsag(self, bwPacket):
"""!Called on POCSAG alarm
@param bwPacket: bwPacket instance"""
@param bwPacket: bwPacket instance
Remove if not implemented"""
pass
def zvei(self, bwPacket):
"""!Called on ZVEI alarm
@param bwPacket: bwPacket instance"""
@param bwPacket: bwPacket instance
Remove if not implemented"""
pass
def msg(self, bwPacket):
"""!Called on MSG packet
@param bwPacket: bwPacket instance"""
@param bwPacket: bwPacket instance
Remove if not implemented"""
pass
def teardown(self):
"""!Called after alarm"""
"""!Called after alarm
Remove if not implemented"""
pass
def onUnload(self):
"""!Called by destruction of the plugin"""
"""!Called by destruction of the plugin
Remove if not implemented"""
pass

View file

@ -10,4 +10,5 @@ mkdocs
pytest
pytest-cov
pytest-pep8
pytest-flakes
pytest-randomly

View file

@ -1,3 +0,0 @@
python -m pytest -c "_gen/pytest.ini"
_bin\win\doxygen\doxygen.exe _gen/doxygen.ini
_bin\win\cloc_1_72\cloc-1.72.exe . --exclude-lang=XML --exclude-dir=_docu,_config,_info,doxygen.ini --by-file-by-lang

View file

@ -23,10 +23,11 @@ import pytest
from boswatch.network.server import TCPServer
from boswatch.network.client import TCPClient
import threading
def setup_method(method):
logging.debug("[TEST] %s.%s", method.__module__, method.__name__)
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
@pytest.fixture
@ -137,7 +138,7 @@ def test_clientMultiCommunicate(getServer):
assert testClient2.receive() == "[ack]"
assert testClient1.receive() == "[ack]"
# check server msg queue
assert dataQueue.qsize() == 3
assert getRunningServer._alarmQueue.qsize() == 3
# disconnect all
assert testClient1.disconnect()
assert testClient2.disconnect()
@ -181,11 +182,11 @@ def test_serverStopsWhileConnected(getRunningServer, getClient):
"""!Shutdown server while client is connected"""
getClient.connect()
getRunningServer.stop()
timeout = 10
timeout = 5
while getClient.isConnected:
time.sleep(0.1)
timeout = timeout - 1
if timeout is 0:
if timeout == 0:
break
assert timeout
@ -206,10 +207,41 @@ def test_serverGetOutput(getRunningServer):
assert testClient1.receive() == "[ack]"
assert testClient2.receive() == "[ack]"
# _check server output data
assert dataQueue.qsize() == 2
assert dataQueue.get(True, 1)[1] == "test1"
assert dataQueue.get(True, 1)[1] == "test2"
assert dataQueue.qsize() is 0 # Last _check must be None
assert getRunningServer._alarmQueue.qsize() == 2
assert getRunningServer._alarmQueue.get(True, 1)[1] == "test1"
assert getRunningServer._alarmQueue.get(True, 1)[1] == "test2"
assert getRunningServer._alarmQueue.qsize() == 0 # Last _check must be None
# disconnect all
assert testClient1.disconnect()
assert testClient2.disconnect()
def test_serverHighLoad(getRunningServer):
"""!High load server test with 10 send threads each will send 100 msg with 324 bytes size"""
logging.debug("start sendThreads")
threads = []
for thr_id in range(10):
thr = threading.Thread(target=sendThread, name="sendThread-" + str(thr_id))
thr.daemon = True
thr.start()
threads.append(thr)
for thread in threads:
thread.join()
logging.debug("finished sendThreads")
assert getRunningServer._alarmQueue.qsize() == 1000
def sendThread():
client = TCPClient()
client.connect()
time.sleep(0.1)
for i in range(100):
# actually this string is 324 bytes long
client.transmit("HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-"
"HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-"
"HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-HigLoadTestString-")
if not client.receive() == "[ack]":
logging.error("missing [ACK]")
time.sleep(0.1)
client.disconnect()

View file

@ -23,8 +23,8 @@ from boswatch.network.broadcast import BroadcastServer
from boswatch.network.broadcast import BroadcastClient
def setup_method(method):
logging.debug("[TEST] %s.%s", method.__module__, method.__name__)
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
@pytest.fixture()

View file

@ -23,8 +23,8 @@ from boswatch.utils import paths
from boswatch.configYaml import ConfigYAML
def setup_method(method):
logging.debug("[TEST] %s.%s", method.__module__, method.__name__)
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
@pytest.fixture
@ -89,7 +89,7 @@ def test_configIterationList(getFilledConfig):
for item in getFilledConfig.get("list"):
assert type(item) is str
counter += 1
assert counter is 3
assert counter == 3
def test_configIterationListWithNestedList(getFilledConfig):

View file

@ -21,8 +21,8 @@ import logging
from boswatch.decoder.decoder import Decoder
def setup_method(method):
logging.debug("[TEST] %s.%s", method.__module__, method.__name__)
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
def test_decoderNoData():

View file

@ -21,8 +21,8 @@ import logging
from boswatch.utils import header
def setup_method(method):
logging.debug("[TEST] %s.%s", method.__module__, method.__name__)
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
def test_logoToLog():

View file

@ -22,8 +22,8 @@ import pytest
from boswatch.packet import Packet
def setup_method(method):
logging.debug("[TEST] %s.%s", method.__module__, method.__name__)
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
@pytest.fixture()
@ -34,19 +34,19 @@ def buildPacket():
def test_createPacket(buildPacket):
"""!Create a packet"""
assert buildPacket is not ""
assert buildPacket != ""
def test_copyPacket(buildPacket):
"""!Copy a packet to an new instance"""
bwCopyPacket = Packet(buildPacket.__str__())
assert bwCopyPacket is not ""
assert bwCopyPacket != ""
def test_getPacketString(buildPacket):
"""!get the intern packet dict as string"""
assert type(buildPacket.__str__()) is str
assert buildPacket.__str__() is not ""
assert buildPacket.__str__() != ""
def test_getNotSetField(buildPacket):
@ -57,4 +57,4 @@ def test_getNotSetField(buildPacket):
def test_setGetField(buildPacket):
"""!set and get a field"""
buildPacket.set("testField", "test")
assert buildPacket.get("testField") is "test"
assert buildPacket.get("testField") == "test"

View file

@ -22,8 +22,8 @@ import os
from boswatch.utils import paths
def setup_method(method):
logging.debug("[TEST] %s.%s", method.__module__, method.__name__)
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
def test_fileExists():

View file

@ -23,8 +23,8 @@ import pytest
from boswatch.timer import RepeatedTimer
def setup_method(method):
logging.debug("[TEST] %s.%s", method.__module__, method.__name__)
def setup_function(function):
logging.debug("[TEST] %s.%s", function.__module__, function.__name__)
def testTargetFast():

View file

@ -8,7 +8,7 @@
# by Bastian Schroll
[pytest]
addopts = -v --pep8 --cov=boswatch/ --cov-report=term-missing --log-level=CRITICAL
addopts = -v --pep8 --flakes --cov=boswatch/ --cov-report=term-missing --log-level=CRITICAL
# classic or progress
console_output_style = progress

View file

@ -90,16 +90,16 @@ POCSAG1200: Address: 9000000 Function: 1 Alpha: BOSWatch-Test: out of filter
POCSAG1200: Address: 0871004 Function: 1 Alpha: Dies ist ein Probealarm!
## Multicast Alarm
POCSAG1200: Address: 0871002 Function: 0 Alpha: <EOT><FF>
POCSAG1200: Address: 0860001 Function: 0
POCSAG1200: Address: 0860002 Function: 0
POCSAG1200: Address: 0860003 Function: 0
POCSAG1200: Address: 0860004 Function: 0
POCSAG1200: Address: 0860005 Function: 0
POCSAG1200: Address: 0860006 Function: 0
POCSAG1200: Address: 0860007 Function: 0
POCSAG1200: Address: 0860008 Function: 0
POCSAG1200: Address: 0860009 Function: 0
POCSAG1200: Address: 0860010 Function: 0
POCSAG1200: Address: 0860001 Function: 0
POCSAG1200: Address: 0860002 Function: 0
POCSAG1200: Address: 0860003 Function: 0
POCSAG1200: Address: 0860004 Function: 0
POCSAG1200: Address: 0860005 Function: 0
POCSAG1200: Address: 0860006 Function: 0
POCSAG1200: Address: 0860007 Function: 0
POCSAG1200: Address: 0860008 Function: 0
POCSAG1200: Address: 0860009 Function: 0
POCSAG1200: Address: 0860010 Function: 0
POCSAG1200: Address: 0871003 Function: 0 Alpha: B2 Feuer Gebäude Pers in Gefahr. bla bla bla<NUL>
# regEx-Filter?