From 0d338dc10d9943c813fc34215d5c18f61330b776 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Tue, 22 Oct 2019 17:16:38 +0200 Subject: [PATCH 01/70] stderr of process to logfile --- bw_client.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bw_client.py b/bw_client.py index 2d2a2f5..085a68e 100644 --- a/bw_client.py +++ b/bw_client.py @@ -93,8 +93,9 @@ try: 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() - sdrProc.skipLinesUntil("Output at") + # sdrProc.skipLinesUntil("Output at") mmProc = ProcessManager(str(sdrConfig.get("mmPath", default="multimon-ng")), textMode=True) if decoderConfig.get("fms", default=0): @@ -110,19 +111,20 @@ try: mmProc.addArgument("-f alpha") mmProc.addArgument("-t raw -") mmProc.setStdin(sdrProc.stdout) + mmProc.setStderr(open(paths.LOG_PATH + "multimon-ng.log", "a")) mmProc.start() - mmProc.skipLinesUntil("Available demodulators:") + # 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 + # 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 + # mmProc.skipLinesUntil("Available demodulators:") # last line from mm before data elif sdrProc.isRunning and mmProc.isRunning: line = mmProc.readline() if line: From 3cdd0297dd40b75dd235fce9ffee8685ac0352d0 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Tue, 22 Oct 2019 17:31:01 +0200 Subject: [PATCH 02/70] fix some pep8 in bw_client --- bw_client.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bw_client.py b/bw_client.py index 085a68e..f460beb 100644 --- a/bw_client.py +++ b/bw_client.py @@ -137,7 +137,8 @@ try: # ========== INPUT CODE ========== - mmThread = threading.Thread(target=handleSDRInput, name="mmReader", args=(inputQueue, bwConfig.get("inputSource", "sdr"), bwConfig.get("decoder"))) + mmThread = threading.Thread(target=handleSDRInput, name="mmReader", + args=(inputQueue, bwConfig.get("inputSource", "sdr"), bwConfig.get("decoder"))) mmThread.daemon = True mmThread.start() @@ -146,8 +147,9 @@ try: 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(): @@ -169,8 +171,9 @@ try: 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: time.sleep(0.1) # reduce cpu load (wait 100ms) From f88c3ea03f499cc978a2b79a64725940724fb23e Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 08:10:50 +0200 Subject: [PATCH 03/70] improve logging --- boswatch/decoder/fmsDecoder.py | 1 - boswatch/decoder/pocsagDecoder.py | 1 - boswatch/decoder/zveiDecoder.py | 1 - boswatch/network/client.py | 4 +++- boswatch/network/server.py | 4 +++- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/boswatch/decoder/fmsDecoder.py b/boswatch/decoder/fmsDecoder.py index 6384295..5836b9d 100644 --- a/boswatch/decoder/fmsDecoder.py +++ b/boswatch/decoder/fmsDecoder.py @@ -62,7 +62,6 @@ class FmsDecoder: bwPacket.set("directionText", directionText) bwPacket.set("tacticalInfo", tacticalInfo) - logging.debug(bwPacket) return bwPacket logging.warning("no valid data") diff --git a/boswatch/decoder/pocsagDecoder.py b/boswatch/decoder/pocsagDecoder.py index fdd7634..d3c674c 100644 --- a/boswatch/decoder/pocsagDecoder.py +++ b/boswatch/decoder/pocsagDecoder.py @@ -56,7 +56,6 @@ class PocsagDecoder: bwPacket.set("subricText", subricText) bwPacket.set("message", message) - logging.debug(bwPacket) return bwPacket logging.warning("no valid data") diff --git a/boswatch/decoder/zveiDecoder.py b/boswatch/decoder/zveiDecoder.py index 6ccd65b..00f21a3 100644 --- a/boswatch/decoder/zveiDecoder.py +++ b/boswatch/decoder/zveiDecoder.py @@ -44,7 +44,6 @@ class ZveiDecoder: bwPacket.set("mode", "zvei") bwPacket.set("zvei", ZveiDecoder._solveDoubleTone(data[7:12])) - logging.debug(bwPacket) return bwPacket logging.warning("no valid data") diff --git a/boswatch/network/client.py b/boswatch/network/client.py index 7c8cd3c..00bead8 100644 --- a/boswatch/network/client.py +++ b/boswatch/network/client.py @@ -17,6 +17,7 @@ import logging import socket import select +from pprint import pformat logging.debug("- %s loaded", __name__) @@ -72,7 +73,7 @@ class TCPClient: @param data: data to send to the server @return True or False""" try: - logging.debug("transmitting: %s", data) + logging.debug("transmitting:\n%s", pformat(data)) header = str(len(data)).ljust(HEADERSIZE) self._sock.sendall(bytes(header + data, "utf-8")) logging.debug("transmitted...") @@ -92,6 +93,7 @@ class TCPClient: header = self._sock.recv(HEADERSIZE) if not len(header): # check if there data return False + logging.debug("recv header: '%s'", header) length = int(header.decode("utf-8").strip()) received = self._sock.recv(length).decode("utf-8") logging.debug("received %d bytes: %s", length, received) diff --git a/boswatch/network/server.py b/boswatch/network/server.py index 6f92846..631a9ac 100644 --- a/boswatch/network/server.py +++ b/boswatch/network/server.py @@ -20,6 +20,7 @@ import socketserver import threading import time import select +from pprint import pformat logging.debug("- %s loaded", __name__) @@ -50,13 +51,14 @@ class _ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): if not len(header): break # empty data -> socked closed + logging.debug("%s recv header: '%s'", req_name, header) length = int(header.decode("utf-8").strip()) data = self.request.recv(length).decode("utf-8") if data == "": continue - logging.debug("%s recv %d bytes: %s", req_name, length, data) + logging.debug("%s recv %d bytes:\n%s", req_name, length, pformat(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 From 361afe2ff1282b9dad1791a07c026c4f9202f4db Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 08:28:26 +0200 Subject: [PATCH 04/70] fix bug --- bw_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bw_client.py b/bw_client.py index f460beb..6324691 100644 --- a/bw_client.py +++ b/bw_client.py @@ -168,7 +168,7 @@ 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 sendDelay = bwConfig.get("client", "sendDelay", default="3") From 3cd5a59bb58ccfa907889adf53d760144a19d118 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 08:59:19 +0200 Subject: [PATCH 05/70] improve client isConnected method --- boswatch/network/client.py | 10 ++-------- boswatch/network/server.py | 3 --- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/boswatch/network/client.py b/boswatch/network/client.py index 00bead8..f4fb5de 100644 --- a/boswatch/network/client.py +++ b/boswatch/network/client.py @@ -105,13 +105,7 @@ class TCPClient: @property def isConnected(self): """!Property of client connected state""" - try: - aliveMsg = "" - header = str(len(aliveMsg)).ljust(HEADERSIZE) - self._sock.sendall(bytes(header + aliveMsg, "utf-8")) + _, write, _ = select.select([], [self._sock], [], 0.1) + if write: return True - 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 diff --git a/boswatch/network/server.py b/boswatch/network/server.py index 631a9ac..67cc6b7 100644 --- a/boswatch/network/server.py +++ b/boswatch/network/server.py @@ -55,9 +55,6 @@ class _ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): length = int(header.decode("utf-8").strip()) data = self.request.recv(length).decode("utf-8") - if data == "": - continue - logging.debug("%s recv %d bytes:\n%s", req_name, length, pformat(data)) # add a new entry and the decoded data dict as an string in utf-8 and an timestamp From 096d6fefe3c870e770db73bbc40d20ba7a60c1fe Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 09:01:11 +0200 Subject: [PATCH 06/70] add serverHigLoad test (10 clients a 100 packets) --- test/boswatch/test_ServerClient.py | 42 ++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/test/boswatch/test_ServerClient.py b/test/boswatch/test_ServerClient.py index 358510e..6ed56b1 100644 --- a/test/boswatch/test_ServerClient.py +++ b/test/boswatch/test_ServerClient.py @@ -23,6 +23,7 @@ import pytest from boswatch.network.server import TCPServer from boswatch.network.client import TCPClient +import threading def setup_method(method): @@ -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() @@ -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() is 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() From 50095bf4a75ded85bbba5abee540dc48d648b1dc Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 09:19:32 +0200 Subject: [PATCH 07/70] fix errors --- boswatch/network/client.py | 13 +++++++++---- test/boswatch/test_ServerClient.py | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/boswatch/network/client.py b/boswatch/network/client.py index f4fb5de..1225087 100644 --- a/boswatch/network/client.py +++ b/boswatch/network/client.py @@ -105,7 +105,12 @@ class TCPClient: @property def isConnected(self): """!Property of client connected state""" - _, write, _ = select.select([], [self._sock], [], 0.1) - if write: - return True - return False + try: + if self._sock: + read, write, _ = select.select([], [self._sock], [], 0.1) + if read and write: + return True + return False + except: + logging.exception("cannot check connection status") + return False diff --git a/test/boswatch/test_ServerClient.py b/test/boswatch/test_ServerClient.py index 6ed56b1..0c8fdcc 100644 --- a/test/boswatch/test_ServerClient.py +++ b/test/boswatch/test_ServerClient.py @@ -182,7 +182,7 @@ 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 @@ -221,7 +221,7 @@ def test_serverHighLoad(getRunningServer): logging.debug("start sendThreads") threads = [] for thr_id in range(10): - thr = threading.Thread(target=sendThread, name="sendThread-"+str(thr_id)) + thr = threading.Thread(target=sendThread, name="sendThread-" + str(thr_id)) thr.daemon = True thr.start() threads.append(thr) From bf3914a6936c6c0b0632c73c215eece549e7aefc Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 09:35:24 +0200 Subject: [PATCH 08/70] safe test artifacts --- .github/workflows/run_pytest.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/run_pytest.yml b/.github/workflows/run_pytest.yml index bf2f891..0b059ff 100644 --- a/.github/workflows/run_pytest.yml +++ b/.github/workflows/run_pytest.yml @@ -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 From 504728191de466eaeca9dc12d37e419be6cd2633 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 14:17:12 +0200 Subject: [PATCH 09/70] add recv timeout for client --- boswatch/network/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/boswatch/network/client.py b/boswatch/network/client.py index 1225087..6bdd81e 100644 --- a/boswatch/network/client.py +++ b/boswatch/network/client.py @@ -82,12 +82,13 @@ class TCPClient: 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) From 549fbafc294ab6a065da85e092eb4f6c7f700058 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 15:26:35 +0200 Subject: [PATCH 10/70] fix client.isConnected() --- boswatch/network/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/boswatch/network/client.py b/boswatch/network/client.py index 6bdd81e..737f8b2 100644 --- a/boswatch/network/client.py +++ b/boswatch/network/client.py @@ -108,8 +108,9 @@ class TCPClient: """!Property of client connected state""" try: if self._sock: - read, write, _ = select.select([], [self._sock], [], 0.1) - if read and write: + _, write, _ = select.select([], [self._sock], [], 0.1) + if write: + self._sock.send(bytes("", "utf-8")) return True return False except: From 464c0c2298d5f5b09ef095b159219f38070ef990 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 19:05:09 +0200 Subject: [PATCH 11/70] fix server/client connection --- boswatch/network/client.py | 11 ++++++++--- boswatch/network/server.py | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/boswatch/network/client.py b/boswatch/network/client.py index 737f8b2..7ea1f21 100644 --- a/boswatch/network/client.py +++ b/boswatch/network/client.py @@ -110,9 +110,14 @@ class TCPClient: if self._sock: _, write, _ = select.select([], [self._sock], [], 0.1) if write: - self._sock.send(bytes("", "utf-8")) + data = "" + header = str(len(data)).ljust(HEADERSIZE) + self._sock.sendall(bytes(header + data, "utf-8")) return True return False - except: - logging.exception("cannot check connection status") + except socket.error as e: + if e.errno != 32: + logging.exception(e) + return False + except ValueError: return False diff --git a/boswatch/network/server.py b/boswatch/network/server.py index 67cc6b7..2752b35 100644 --- a/boswatch/network/server.py +++ b/boswatch/network/server.py @@ -51,10 +51,13 @@ class _ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): if not len(header): break # empty data -> socked closed - logging.debug("%s recv header: '%s'", req_name, header) length = int(header.decode("utf-8").strip()) data = self.request.recv(length).decode("utf-8") + if data == "": + continue + + logging.debug("%s recv header: '%s'", req_name, header) logging.debug("%s recv %d bytes:\n%s", req_name, length, pformat(data)) # add a new entry and the decoded data dict as an string in utf-8 and an timestamp @@ -66,12 +69,12 @@ class _ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): data = "[ack]" header = str(len(data)).ljust(HEADERSIZE) self.request.sendall(bytes(header + data, "utf-8")) - self.request.close() 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]) @@ -114,7 +117,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 @@ -143,6 +146,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 From 1d90b3d38b2dde6d3380a7bf89de9d35a57a5b2d Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 22:11:57 +0200 Subject: [PATCH 12/70] fix some pyflakes --- test/boswatch/test_ServerClient.py | 4 ++-- test/boswatch/test_config.py | 2 +- test/boswatch/test_packet.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/boswatch/test_ServerClient.py b/test/boswatch/test_ServerClient.py index 0c8fdcc..ce70247 100644 --- a/test/boswatch/test_ServerClient.py +++ b/test/boswatch/test_ServerClient.py @@ -186,7 +186,7 @@ def test_serverStopsWhileConnected(getRunningServer, getClient): while getClient.isConnected: time.sleep(0.1) timeout = timeout - 1 - if timeout is 0: + if timeout == 0: break assert timeout @@ -210,7 +210,7 @@ def test_serverGetOutput(getRunningServer): 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() is 0 # Last _check must be None + assert getRunningServer._alarmQueue.qsize() == 0 # Last _check must be None # disconnect all assert testClient1.disconnect() assert testClient2.disconnect() diff --git a/test/boswatch/test_config.py b/test/boswatch/test_config.py index 80fb346..b58139b 100644 --- a/test/boswatch/test_config.py +++ b/test/boswatch/test_config.py @@ -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): diff --git a/test/boswatch/test_packet.py b/test/boswatch/test_packet.py index ee638fd..fc36c02 100644 --- a/test/boswatch/test_packet.py +++ b/test/boswatch/test_packet.py @@ -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" From 7d2e7597f770292badfdb1154ae8e97ec0c1bf69 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 22:12:42 +0200 Subject: [PATCH 13/70] add pyflakes to pytest --- requirements.txt | 1 + test/pytest.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 795804f..b401509 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ mkdocs pytest pytest-cov pytest-pep8 +pytest-flakes pytest-randomly diff --git a/test/pytest.ini b/test/pytest.ini index c75fe6c..6e8fc02 100644 --- a/test/pytest.ini +++ b/test/pytest.ini @@ -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 From 3dddcdcff240920ec8cd46d399bf5239ef995578 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 23 Oct 2019 22:23:24 +0200 Subject: [PATCH 14/70] fix pyflakes py3.5 build --- boswatch/router/routerManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/boswatch/router/routerManager.py b/boswatch/router/routerManager.py index c03bee4..71ad4ec 100644 --- a/boswatch/router/routerManager.py +++ b/boswatch/router/routerManager.py @@ -89,7 +89,8 @@ class RouterManager: logging.error("unknown type '%s' in %s", routeType, route) return False - except ModuleNotFoundError: + # except ModuleNotFoundError: # only since Py3.6 + except ImportError: logging.error("%s not found: %s", route.get("type"), route.get("name")) return False From 6233e9fbd486312856b368160bbf03ce7b44d80d Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Thu, 24 Oct 2019 08:45:18 +0200 Subject: [PATCH 15/70] added testmode to bw_client --- bw_client.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/bw_client.py b/bw_client.py index 6324691..4acf92b 100644 --- a/bw_client.py +++ b/bw_client.py @@ -60,7 +60,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() @@ -79,9 +79,6 @@ try: ip = broadcastClient.serverIP port = broadcastClient.serverPort - inputQueue = queue.Queue() - inputThreadRunning = True - # ========== INPUT CODE ========== def handleSDRInput(dataQueue, sdrConfig, decoderConfig): # todo exception handling inside sdrProc = ProcessManager(str(sdrConfig.get("rtlPath", default="rtl_fm"))) @@ -134,13 +131,25 @@ try: 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() + inputQueue = queue.Queue() + + if not args.test: + inputThreadRunning = True + mmThread = threading.Thread(target=handleSDRInput, name="mmReader", + args=(inputQueue, bwConfig.get("inputSource", "sdr"), bwConfig.get("decoder"))) + mmThread.daemon = True + mmThread.start() + else: + logging.warning("STARTING TESTMODE!") + logging.debug("reading testdata from file") + testFile = open("test/testdata.list", "r") + 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) From a3494b54a66c8fd19e16b05966ae891d2882d6b4 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Thu, 24 Oct 2019 08:59:33 +0200 Subject: [PATCH 16/70] improve testmode --- bw_client.py | 2 ++ test/testdata.list | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/bw_client.py b/bw_client.py index 4acf92b..651132a 100644 --- a/bw_client.py +++ b/bw_client.py @@ -185,6 +185,8 @@ try: 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 diff --git a/test/testdata.list b/test/testdata.list index 6b742c0..12b71c0 100644 --- a/test/testdata.list +++ b/test/testdata.list @@ -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: -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 # regEx-Filter? From 60d05dc23558f7af97d62c26ec2fac02b7d29005 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Thu, 24 Oct 2019 10:08:07 +0200 Subject: [PATCH 17/70] fix some log messages --- boswatch/network/client.py | 5 +++-- boswatch/network/server.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/boswatch/network/client.py b/boswatch/network/client.py index 7ea1f21..9a530dc 100644 --- a/boswatch/network/client.py +++ b/boswatch/network/client.py @@ -58,6 +58,7 @@ class TCPClient: @return True or False""" try: if self.isConnected: + self._sock.shutdown() self._sock.close() logging.debug("disconnected") return True @@ -94,10 +95,10 @@ class TCPClient: header = self._sock.recv(HEADERSIZE) if not len(header): # check if there data return False - logging.debug("recv header: '%s'", header) + logging.debug("recv header: %s", header) length = int(header.decode("utf-8").strip()) received = self._sock.recv(length).decode("utf-8") - logging.debug("received %d bytes: %s", length, received) + logging.debug("received %d bytes: %s", len(received), received) return received except socket.error as e: logging.error(e) diff --git a/boswatch/network/server.py b/boswatch/network/server.py index 2752b35..3aedd88 100644 --- a/boswatch/network/server.py +++ b/boswatch/network/server.py @@ -57,8 +57,8 @@ class _ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): if data == "": continue - logging.debug("%s recv header: '%s'", req_name, header) - logging.debug("%s recv %d bytes:\n%s", req_name, length, pformat(data)) + logging.debug("%s recv header: %s", req_name, header) + logging.debug("%s recv %d bytes:\n%s", req_name, len(data), pformat(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 From 72f4f3c8d8a90f654f59b1fa09e5deda7fa3f395 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Thu, 24 Oct 2019 10:55:25 +0200 Subject: [PATCH 18/70] fix false socket.shutdown call --- boswatch/network/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boswatch/network/client.py b/boswatch/network/client.py index 9a530dc..185631b 100644 --- a/boswatch/network/client.py +++ b/boswatch/network/client.py @@ -58,7 +58,7 @@ class TCPClient: @return True or False""" try: if self.isConnected: - self._sock.shutdown() + self._sock.shutdown(socket.SHUT_RDWR) self._sock.close() logging.debug("disconnected") return True From 41a8fc740d7a7f868ccf2292feaff20d3e75b63c Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Thu, 24 Oct 2019 13:29:16 +0200 Subject: [PATCH 19/70] add encoding for testdata reader --- bw_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bw_client.py b/bw_client.py index 651132a..7d555c6 100644 --- a/bw_client.py +++ b/bw_client.py @@ -48,7 +48,6 @@ from boswatch.decoder.decoder import Decoder from boswatch.utils import header from boswatch.utils import misc - header.logoToLog() header.infoToLog() @@ -144,7 +143,7 @@ try: else: logging.warning("STARTING TESTMODE!") logging.debug("reading testdata from file") - testFile = open("test/testdata.list", "r") + 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')) From bb1a7f21f96ff5405ef777a639fcb276944a99c2 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Thu, 24 Oct 2019 18:46:22 +0200 Subject: [PATCH 20/70] fix server/client encoding --- boswatch/network/client.py | 23 +++++++++++++---------- boswatch/network/server.py | 15 +++++++-------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/boswatch/network/client.py b/boswatch/network/client.py index 185631b..b8f04f8 100644 --- a/boswatch/network/client.py +++ b/boswatch/network/client.py @@ -17,7 +17,6 @@ import logging import socket import select -from pprint import pformat logging.debug("- %s loaded", __name__) @@ -74,9 +73,10 @@ class TCPClient: @param data: data to send to the server @return True or False""" try: - logging.debug("transmitting:\n%s", pformat(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: @@ -92,12 +92,15 @@ class TCPClient: 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 - logging.debug("recv header: %s", header) - length = int(header.decode("utf-8").strip()) + + length = int(header.strip()) received = self._sock.recv(length).decode("utf-8") + + logging.debug("recv header: '%s'", header) logging.debug("received %d bytes: %s", len(received), received) return received except socket.error as e: @@ -111,9 +114,9 @@ class TCPClient: if self._sock: _, write, _ = select.select([], [self._sock], [], 0.1) if write: - data = "" - header = str(len(data)).ljust(HEADERSIZE) - self._sock.sendall(bytes(header + data, "utf-8")) + data = "".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: diff --git a/boswatch/network/server.py b/boswatch/network/server.py index 3aedd88..eda41fc 100644 --- a/boswatch/network/server.py +++ b/boswatch/network/server.py @@ -20,7 +20,6 @@ import socketserver import threading import time import select -from pprint import pformat logging.debug("- %s loaded", __name__) @@ -47,18 +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 == "": continue - logging.debug("%s recv header: %s", req_name, header) - logging.debug("%s recv %d bytes:\n%s", req_name, len(data), pformat(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 @@ -66,9 +65,9 @@ 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")) + 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) From 543719a91729ebcf4a448265a7b81a9d01621bf1 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Thu, 24 Oct 2019 19:01:35 +0200 Subject: [PATCH 21/70] fix some not defined warnings --- bw_client.py | 9 +++++++-- bw_server.py | 15 ++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/bw_client.py b/bw_client.py index 7d555c6..2118c8c 100644 --- a/bw_client.py +++ b/bw_client.py @@ -68,6 +68,9 @@ if not bwConfig.loadConfigFile(paths.CONFIG_PATH + args.config): exit(1) # ========== CLIENT CODE ========== +mmThread = None +bwClient = None + try: ip = bwConfig.get("server", "ip", default="127.0.0.1") port = bwConfig.get("server", "port", default="8080") @@ -198,7 +201,9 @@ except: # pragma: no cover logging.exception("BOSWatch interrupted by an error") finally: logging.debug("Starting shutdown routine") - bwClient.disconnect() + if bwClient: + bwClient.disconnect() inputThreadRunning = False - mmThread.join() + if mmThread: + mmThread.join() logging.debug("BOSWatch client has stopped ...") diff --git a/bw_server.py b/bw_server.py index abf0fef..82ccbf7 100644 --- a/bw_server.py +++ b/bw_server.py @@ -46,7 +46,6 @@ from boswatch.network.broadcast import BroadcastServer from boswatch.router.routerManager import RouterManager from boswatch.utils import misc - header.logoToLog() header.infoToLog() @@ -67,8 +66,11 @@ 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): logging.fatal("Error while building routers") @@ -109,7 +111,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: + del bwRoutMan + if bwServer: + bwServer.stop() + if bcServer: + bcServer.stop() logging.debug("BOSWatch server has stopped ...") From 7d8544123b627bba425ebeab10fdcec0699cfd09 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Thu, 24 Oct 2019 23:11:57 +0200 Subject: [PATCH 22/70] fix test start log message --- test/boswatch/test_ServerClient.py | 4 ++-- test/boswatch/test_broadcast.py | 4 ++-- test/boswatch/test_config.py | 4 ++-- test/boswatch/test_decoder.py | 4 ++-- test/boswatch/test_header.py | 4 ++-- test/boswatch/test_packet.py | 4 ++-- test/boswatch/test_paths.py | 4 ++-- test/boswatch/test_timer.py | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/boswatch/test_ServerClient.py b/test/boswatch/test_ServerClient.py index ce70247..52b7d49 100644 --- a/test/boswatch/test_ServerClient.py +++ b/test/boswatch/test_ServerClient.py @@ -26,8 +26,8 @@ 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 diff --git a/test/boswatch/test_broadcast.py b/test/boswatch/test_broadcast.py index 9bda27d..32aa0ac 100644 --- a/test/boswatch/test_broadcast.py +++ b/test/boswatch/test_broadcast.py @@ -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() diff --git a/test/boswatch/test_config.py b/test/boswatch/test_config.py index b58139b..ab8b9d5 100644 --- a/test/boswatch/test_config.py +++ b/test/boswatch/test_config.py @@ -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 diff --git a/test/boswatch/test_decoder.py b/test/boswatch/test_decoder.py index 71bed94..bb610b1 100644 --- a/test/boswatch/test_decoder.py +++ b/test/boswatch/test_decoder.py @@ -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(): diff --git a/test/boswatch/test_header.py b/test/boswatch/test_header.py index b5908a3..4aa0a69 100644 --- a/test/boswatch/test_header.py +++ b/test/boswatch/test_header.py @@ -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(): diff --git a/test/boswatch/test_packet.py b/test/boswatch/test_packet.py index fc36c02..ee3d170 100644 --- a/test/boswatch/test_packet.py +++ b/test/boswatch/test_packet.py @@ -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() diff --git a/test/boswatch/test_paths.py b/test/boswatch/test_paths.py index 8b72b13..e2a4e31 100644 --- a/test/boswatch/test_paths.py +++ b/test/boswatch/test_paths.py @@ -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(): diff --git a/test/boswatch/test_timer.py b/test/boswatch/test_timer.py index 64157d2..a5ef2c9 100644 --- a/test/boswatch/test_timer.py +++ b/test/boswatch/test_timer.py @@ -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(): From c4fa8c489876e5a0592c56bb7ff35367bbe92d8b Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Thu, 24 Oct 2019 23:12:20 +0200 Subject: [PATCH 23/70] edit comments in plugin/module base class --- module/module.py | 16 ++++++++-------- plugin/plugin.py | 24 ++++++++++++------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/module/module.py b/module/module.py index 814ddaf..a7bbb84 100644 --- a/module/module.py +++ b/module/module.py @@ -23,13 +23,13 @@ logging.debug("- %s loaded", __name__) class Module: """!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 @@ -46,11 +46,11 @@ class Module: def __del__(self): """!Destructor 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""" @@ -84,17 +84,17 @@ 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 diff --git a/plugin/plugin.py b/plugin/plugin.py index f0a6de6..b6a5898 100644 --- a/plugin/plugin.py +++ b/plugin/plugin.py @@ -25,13 +25,13 @@ logging.debug("- %s loaded", __name__) class Plugin: """!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 @@ -56,7 +56,7 @@ class Plugin: def __del__(self): """!Destructor calls onUnload() directly""" logging.debug("[%s] onUnload()", self._pluginName) - self._pluginsActive -= 1 + self._pluginsActive.remove(self) self.onUnload() def _run(self, bwPacket): @@ -137,50 +137,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): From 39aeb6c93a2f306ee44f9f9b5b3313521c848522 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Thu, 24 Oct 2019 23:42:52 +0200 Subject: [PATCH 24/70] mod/plug remove tmpTime member var --- module/module.py | 5 ++--- plugin/plugin.py | 13 ++++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/module/module.py b/module/module.py index a7bbb84..37b793b 100644 --- a/module/module.py +++ b/module/module.py @@ -34,7 +34,6 @@ class Module: # for time counting self._cumTime = 0 self._moduleTime = 0 - self._tmpTime = 0 # for statistics self._runCount = 0 @@ -57,14 +56,14 @@ class Module: 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 diff --git a/plugin/plugin.py b/plugin/plugin.py index b6a5898..98721bb 100644 --- a/plugin/plugin.py +++ b/plugin/plugin.py @@ -42,7 +42,6 @@ class Plugin: self._setupTime = 0 self._alarmTime = 0 self._teardownTime = 0 - self._tmpTime = 0 # for statistics self._runCount = 0 @@ -70,7 +69,7 @@ class Plugin: self._bwPacket = bwPacket - self._tmpTime = time.time() + tmpTime = time.time() try: logging.debug("[%s] setup()", self._pluginName) self.setup() @@ -78,8 +77,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 +97,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 +106,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 From 5dcd71eb8d12fccecfae1289e6dca1c86ea32892 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 11:12:41 +0200 Subject: [PATCH 25/70] add resource and name to route point --- boswatch/router/router.py | 6 +++--- boswatch/router/routerManager.py | 16 +++++++++------- config/server.yaml | 3 ++- docu/docs/config.md | 6 ++++-- docu/docs/modul/mode_filter.md | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/boswatch/router/router.py b/boswatch/router/router.py index 9f84b19..496cf38 100644 --- a/boswatch/router/router.py +++ b/boswatch/router/router.py @@ -28,7 +28,7 @@ class Router: @param name: name of the router""" self._name = name self._routeList = [] - logging.debug("[%s] new router", self._name) + logging.debug("[%s] add new router", self._name) def addRoute(self, route): """!Adds a route point to the router @@ -46,8 +46,8 @@ class Router: """ logging.debug("[%s] started", self._name) for routeObject in self._routeList: - logging.debug("[%s] -> run route: %s", self._name, routeObject) - bwPacket_tmp = routeObject.callback(copy.deepcopy(bwPacket)) # copy bwPacket to prevent edit the original + logging.debug("[%s] -> run route: %s", self._name, routeObject.name) + bwPacket_tmp = routeObject.callback._run(copy.deepcopy(bwPacket)) # copy bwPacket to prevent edit the original if bwPacket_tmp is None: # returning None doesnt change the bwPacket continue diff --git a/boswatch/router/routerManager.py b/boswatch/router/routerManager.py index 71ad4ec..7ba76fa 100644 --- a/boswatch/router/routerManager.py +++ b/boswatch/router/routerManager.py @@ -64,23 +64,25 @@ 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)) 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)) elif routeType == "router": routerDict_tmp[routerName].addRoute(Route(routeName, routerDict_tmp[routeName].runRouter)) @@ -91,7 +93,7 @@ class RouterManager: # except ModuleNotFoundError: # only since Py3.6 except ImportError: - logging.error("%s not found: %s", route.get("type"), route.get("name")) + logging.error("%s not found: %s", route.get("type"), route.get("res")) return False logging.debug("finished building routers") diff --git a/config/server.yaml b/config/server.yaml index bf17173..651b259 100644 --- a/config/server.yaml +++ b/config/server.yaml @@ -19,7 +19,8 @@ router: - name: Router 1 route: - type: module - name: filter.modeFilter + res: filter.modeFilter + name: Filter Fms/Zvei config: allowed: - fms diff --git a/docu/docs/config.md b/docu/docs/config.md index 3a10798..3a327f5 100644 --- a/docu/docs/config.md +++ b/docu/docs/config.md @@ -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 diff --git a/docu/docs/modul/mode_filter.md b/docu/docs/modul/mode_filter.md index 5f167f7..03d1a2d 100644 --- a/docu/docs/modul/mode_filter.md +++ b/docu/docs/modul/mode_filter.md @@ -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 From c80c9227d5113d9aa91d9c5f30ac40bce373e11e Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 12:12:40 +0200 Subject: [PATCH 26/70] add missed pass statements --- plugin/template_plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin/template_plugin.py b/plugin/template_plugin.py index 87b7ff3..2c876b3 100644 --- a/plugin/template_plugin.py +++ b/plugin/template_plugin.py @@ -55,11 +55,13 @@ class BoswatchPlugin(Plugin): """!Called on ZVEI alarm @param bwPacket: bwPacket instance""" + pass def msg(self, bwPacket): """!Called on MSG packet @param bwPacket: bwPacket instance""" + pass def teardown(self): """!Called after alarm""" From 869bc141de28beadc3ba35b3bb5633ed026e0797 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 13:25:56 +0200 Subject: [PATCH 27/70] add type to mod/plug stats --- module/module.py | 3 ++- plugin/plugin.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/module/module.py b/module/module.py index 37b793b..137f1aa 100644 --- a/module/module.py +++ b/module/module.py @@ -75,7 +75,8 @@ class Module: """!Returns statistical information's from last module run @return Statistics as pyton dict""" - stats = {"runCount": self._runCount, + stats = {"type": "plugin", + "runCount": self._runCount, "cumTime": self._cumTime, "moduleTime": self._moduleTime, "moduleErrorCount": self._moduleErrorCount} diff --git a/plugin/plugin.py b/plugin/plugin.py index 98721bb..9101a96 100644 --- a/plugin/plugin.py +++ b/plugin/plugin.py @@ -123,7 +123,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, From 341f05d25a478970df89eed4334e89215c86a100 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 13:27:48 +0200 Subject: [PATCH 28/70] refactoring in routerManager --- boswatch/router/routerManager.py | 2 +- bw_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/boswatch/router/routerManager.py b/boswatch/router/routerManager.py index 7ba76fa..2613238 100644 --- a/boswatch/router/routerManager.py +++ b/boswatch/router/routerManager.py @@ -101,7 +101,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 diff --git a/bw_server.py b/bw_server.py index 82ccbf7..e9db7ee 100644 --- a/bw_server.py +++ b/bw_server.py @@ -99,7 +99,7 @@ try: bwPacket.set("clientIP", data[0]) misc.addServerDataToPacket(bwPacket, bwConfig) - bwRoutMan.runRouter(bwConfig.get("alarmRouter"), bwPacket) + bwRoutMan.runRouters(bwConfig.get("alarmRouter"), bwPacket) incomingQueue.task_done() From d40432e5b8fc59f0223d0c5352c56b3cde702ae9 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 13:29:26 +0200 Subject: [PATCH 29/70] add route statisticsCallback --- boswatch/router/route.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/boswatch/router/route.py b/boswatch/router/route.py index fb581bc..8ff305b 100644 --- a/boswatch/router/route.py +++ b/boswatch/router/route.py @@ -18,14 +18,16 @@ class Route: """!Class for single routing points""" - def __init__(self, name, callback): + def __init__(self, name, callback, statsCallback=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) """ self._name = name self._callback = callback + self._statsCallback = statsCallback @property def name(self): @@ -36,3 +38,8 @@ class Route: def callback(self): """!Porperty to get the callback function instance""" return self._callback + + @property + def statistics(self): + """!Porperty to get the statistics from instance""" + return self._statsCallback From 04a8303ca9498c7d11440e809fc9cfdf43cc1a45 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 13:30:07 +0200 Subject: [PATCH 30/70] set route statistics callback --- boswatch/router/router.py | 2 +- boswatch/router/routerManager.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/boswatch/router/router.py b/boswatch/router/router.py index 496cf38..d88def1 100644 --- a/boswatch/router/router.py +++ b/boswatch/router/router.py @@ -47,7 +47,7 @@ class Router: 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._run(copy.deepcopy(bwPacket)) # copy bwPacket to prevent edit the original + 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 diff --git a/boswatch/router/routerManager.py b/boswatch/router/routerManager.py index 2613238..a057872 100644 --- a/boswatch/router/routerManager.py +++ b/boswatch/router/routerManager.py @@ -77,12 +77,12 @@ class RouterManager: if routeType == "plugin": importedFile = importlib.import_module(routeType + "." + routeRes) loadedClass = importedFile.BoswatchPlugin(routeConfig) - routerDict_tmp[routerName].addRoute(Route(routeName, loadedClass)) + routerDict_tmp[routerName].addRoute(Route(routeName, loadedClass._run, loadedClass._getStatistics)) elif routeType == "module": importedFile = importlib.import_module(routeType + "." + routeRes) loadedClass = importedFile.BoswatchModule(routeConfig) - routerDict_tmp[routerName].addRoute(Route(routeName, loadedClass)) + routerDict_tmp[routerName].addRoute(Route(routeName, loadedClass._run, loadedClass._getStatistics)) elif routeType == "router": routerDict_tmp[routerName].addRoute(Route(routeName, routerDict_tmp[routeName].runRouter)) From 1776f91c9ed4bf9066721817d3775ce1ca1a83d7 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 13:43:50 +0200 Subject: [PATCH 31/70] remove unneded properties --- boswatch/network/netCheck.py | 11 +++-------- boswatch/router/route.py | 21 +++------------------ boswatch/router/router.py | 32 +++++++++++--------------------- 3 files changed, 17 insertions(+), 47 deletions(-) diff --git a/boswatch/network/netCheck.py b/boswatch/network/netCheck.py index 5a4f3b8..0674ec6 100644 --- a/boswatch/network/netCheck.py +++ b/boswatch/network/netCheck.py @@ -30,7 +30,7 @@ class NetCheck: @param timout: timout 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 diff --git a/boswatch/router/route.py b/boswatch/router/route.py index 8ff305b..1161e0e 100644 --- a/boswatch/router/route.py +++ b/boswatch/router/route.py @@ -25,21 +25,6 @@ class Route: @param callback: instance of the callback function @param statsCallback: instance of the callback to get statistics (None) """ - self._name = name - self._callback = callback - self._statsCallback = statsCallback - - @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 - - @property - def statistics(self): - """!Porperty to get the statistics from instance""" - return self._statsCallback + self.name = name + self.callback = callback + self.statistics = statsCallback diff --git a/boswatch/router/router.py b/boswatch/router/router.py index d88def1..69c18db 100644 --- a/boswatch/router/router.py +++ b/boswatch/router/router.py @@ -26,17 +26,17 @@ class Router: """!Create a new router @param name: name of the router""" - self._name = name - self._routeList = [] - logging.debug("[%s] add new router", self._name) + self.name = name + self.routeList = [] + 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 +44,19 @@ 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.name) + 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: %s", self.name, bwPacket) + logging.debug("[%s] finished", self.name) return bwPacket - - @property - def name(self): - """!Property to get the name of the router""" - return self._name - - @property - def routeList(self): - """!Property to get a list of all route points of this router""" - return self._routeList From 9a33975f52127a0c786065252074476bce2513eb Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 14:32:24 +0200 Subject: [PATCH 32/70] fix module stats type name --- module/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/module.py b/module/module.py index 137f1aa..ad0ede2 100644 --- a/module/module.py +++ b/module/module.py @@ -75,7 +75,7 @@ class Module: """!Returns statistical information's from last module run @return Statistics as pyton dict""" - stats = {"type": "plugin", + stats = {"type": "module", "runCount": self._runCount, "cumTime": self._cumTime, "moduleTime": self._moduleTime, From 1f01aaf2c38bb5848080dbd2e4890b21d5a98c9b Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 14:33:18 +0200 Subject: [PATCH 33/70] save router run stats to stats file --- .gitignore | 1 + boswatch/router/routerManager.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/.gitignore b/.gitignore index 0bf01fb..7873218 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ \venv/ # generated files +stats_* log/ docu/docs/api/html docu/site/ diff --git a/boswatch/router/routerManager.py b/boswatch/router/routerManager.py index a057872..2df75ec 100644 --- a/boswatch/router/routerManager.py +++ b/boswatch/router/routerManager.py @@ -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,6 +31,7 @@ class RouterManager: def __init__(self): """!Create new router""" self._routerDict = {} + self._startTime = int(time.time()) def __del__(self): """!Destroy the internal routerDict @@ -115,6 +117,8 @@ class RouterManager: else: logging.warning("unknown router: %s", routerName) + self._saveStats() # write stats to stats file + def _showRouterRoute(self): """!Show the routes of all routers""" for name, routerObject in self._routerDict.items(): @@ -123,3 +127,20 @@ class RouterManager: for routePoint in routerObject.routeList: counter += 1 logging.debug(" %d. %s", counter, routePoint.name) + + def _saveStats(self): + lines = [] + + for name, routerObject in self._routerDict.items(): + lines.append("[" + name + "]") + lines.append("loaded route points: " + str(len(routerObject.routeList))) + for routePoint in routerObject.routeList: + lines.append("[+] " + routePoint.name) + if routePoint.statistics: + if routePoint.statistics()['type'] == "plugin": + lines.append(" Runs: " + str(routePoint.statistics()['runCount'])) + lines.append("") + + with open("stats_" + str(self._startTime) + ".txt", "w") as stats: + for line in lines: + stats.write(line + "\n") From 9ce4fd74205e6237e7e9a9890cb449c9e96e5fa8 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 14:40:49 +0200 Subject: [PATCH 34/70] improve router stats writer --- boswatch/router/routerManager.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/boswatch/router/routerManager.py b/boswatch/router/routerManager.py index 2df75ec..bf515d1 100644 --- a/boswatch/router/routerManager.py +++ b/boswatch/router/routerManager.py @@ -129,16 +129,22 @@ class RouterManager: 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("loaded route points: " + str(len(routerObject.routeList))) for routePoint in routerObject.routeList: lines.append("[+] " + routePoint.name) if routePoint.statistics: - if routePoint.statistics()['type'] == "plugin": - lines.append(" Runs: " + str(routePoint.statistics()['runCount'])) + 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: From 512d72e97a212065156bf133c9e6607e9d9dbda1 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 15:36:10 +0200 Subject: [PATCH 35/70] improve plug/mod cleanup strategy --- boswatch/router/route.py | 4 +++- boswatch/router/routerManager.py | 27 +++++++++++++++++---------- bw_server.py | 2 +- module/module.py | 4 ++-- plugin/plugin.py | 4 ++-- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/boswatch/router/route.py b/boswatch/router/route.py index 1161e0e..4077076 100644 --- a/boswatch/router/route.py +++ b/boswatch/router/route.py @@ -18,13 +18,15 @@ class Route: """!Class for single routing points""" - def __init__(self, name, callback, statsCallback=None): + 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 self.statistics = statsCallback + self.cleanup = cleanupCallback diff --git a/boswatch/router/routerManager.py b/boswatch/router/routerManager.py index bf515d1..60a0b13 100644 --- a/boswatch/router/routerManager.py +++ b/boswatch/router/routerManager.py @@ -33,13 +33,6 @@ class RouterManager: self._routerDict = {} self._startTime = int(time.time()) - 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 - # if there is an error, router list would be empty (see tmp variable) def buildRouter(self, config): """!Initialize Routers from given config file @@ -79,12 +72,18 @@ class RouterManager: if routeType == "plugin": importedFile = importlib.import_module(routeType + "." + routeRes) loadedClass = importedFile.BoswatchPlugin(routeConfig) - routerDict_tmp[routerName].addRoute(Route(routeName, loadedClass._run, loadedClass._getStatistics)) + routerDict_tmp[routerName].addRoute(Route(routeName, + loadedClass._run, + loadedClass._getStatistics, + loadedClass._cleanup)) elif routeType == "module": importedFile = importlib.import_module(routeType + "." + routeRes) loadedClass = importedFile.BoswatchModule(routeConfig) - routerDict_tmp[routerName].addRoute(Route(routeName, loadedClass._run, loadedClass._getStatistics)) + 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)) @@ -119,6 +118,14 @@ class RouterManager: 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(): @@ -133,7 +140,7 @@ class RouterManager: lines = [] for name, routerObject in self._routerDict.items(): lines.append("[" + name + "]") - lines.append("loaded route points: " + str(len(routerObject.routeList))) + lines.append(" - Route points: " + str(len(routerObject.routeList))) for routePoint in routerObject.routeList: lines.append("[+] " + routePoint.name) if routePoint.statistics: diff --git a/bw_server.py b/bw_server.py index e9db7ee..4c62bed 100644 --- a/bw_server.py +++ b/bw_server.py @@ -112,7 +112,7 @@ except: # pragma: no cover finally: logging.debug("Starting shutdown routine") if bwRoutMan: - del bwRoutMan + bwRoutMan.cleanup() if bwServer: bwServer.stop() if bcServer: diff --git a/module/module.py b/module/module.py index ad0ede2..05fd317 100644 --- a/module/module.py +++ b/module/module.py @@ -42,8 +42,8 @@ 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.remove(self) self.onUnload() diff --git a/plugin/plugin.py b/plugin/plugin.py index 9101a96..bf60771 100644 --- a/plugin/plugin.py +++ b/plugin/plugin.py @@ -52,8 +52,8 @@ 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.remove(self) self.onUnload() From c07237f2ea98fe48eb19c24400dbf652401f6c91 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 15:54:26 +0200 Subject: [PATCH 36/70] add statistics for routers --- boswatch/router/router.py | 28 ++++++++++++++++++++++++++++ boswatch/router/routerManager.py | 1 + docu/docs/develop/ModulPlugin.md | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/boswatch/router/router.py b/boswatch/router/router.py index 69c18db..c0555ae 100644 --- a/boswatch/router/router.py +++ b/boswatch/router/router.py @@ -16,6 +16,7 @@ """ import logging import copy +import time logging.debug("- %s loaded", __name__) @@ -28,6 +29,14 @@ class Router: @param name: name of the router""" 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): @@ -44,7 +53,11 @@ class Router: @param bwPacket: instance of Packet class @return a instance of Packet class """ + 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 @@ -59,4 +72,19 @@ class Router: bwPacket = bwPacket_tmp logging.debug("[%s] <- bwPacket returned: %s", self.name, bwPacket) logging.debug("[%s] finished", self.name) + + self._routerTime = time.time() - tmpTime + self._cumTime += self._routerTime + return bwPacket + + + def _getStatistics(self): + """!Returns statistical information's from last router run + + @return Statistics as pyton dict""" + stats = {"type": "router", + "runCount": self._runCount, + "cumTime": self._cumTime, + "moduleTime": self._routerTime} + return stats diff --git a/boswatch/router/routerManager.py b/boswatch/router/routerManager.py index 60a0b13..b8574a6 100644 --- a/boswatch/router/routerManager.py +++ b/boswatch/router/routerManager.py @@ -141,6 +141,7 @@ class RouterManager: 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: diff --git a/docu/docs/develop/ModulPlugin.md b/docu/docs/develop/ModulPlugin.md index 1b6b58e..822af74 100644 --- a/docu/docs/develop/ModulPlugin.md +++ b/docu/docs/develop/ModulPlugin.md @@ -78,7 +78,7 @@ Eine Auflistung der bereitgestellten Informationen findet sich im entsprechenden 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 Module können Pakete beliebig verändern. Diese Änderungen werden im Router entsprechend weitergeleitet. From 277cd9db12d57e8a8b5c09c0d7449ac3db9b0193 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 15:56:20 +0200 Subject: [PATCH 37/70] refactoring in routerManager --- boswatch/router/routerManager.py | 2 +- bw_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/boswatch/router/routerManager.py b/boswatch/router/routerManager.py index b8574a6..341fad4 100644 --- a/boswatch/router/routerManager.py +++ b/boswatch/router/routerManager.py @@ -34,7 +34,7 @@ class RouterManager: 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 diff --git a/bw_server.py b/bw_server.py index 4c62bed..894d11f 100644 --- a/bw_server.py +++ b/bw_server.py @@ -72,7 +72,7 @@ bcServer = None try: bwRoutMan = RouterManager() - if not bwRoutMan.buildRouter(bwConfig): + if not bwRoutMan.buildRouters(bwConfig): logging.fatal("Error while building routers") exit(1) From d2052a155808a3be0b9ea7dfb67e3cf83534243e Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 15:57:13 +0200 Subject: [PATCH 38/70] fix pep8 --- boswatch/router/router.py | 1 - 1 file changed, 1 deletion(-) diff --git a/boswatch/router/router.py b/boswatch/router/router.py index c0555ae..e1c878a 100644 --- a/boswatch/router/router.py +++ b/boswatch/router/router.py @@ -78,7 +78,6 @@ class Router: return bwPacket - def _getStatistics(self): """!Returns statistical information's from last router run From 7a852cad0278a7739e9f3a903d9bcfad2ab0ff36 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 17:21:41 +0200 Subject: [PATCH 39/70] remove bwPaket from router return log --- boswatch/router/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boswatch/router/router.py b/boswatch/router/router.py index e1c878a..0c736d8 100644 --- a/boswatch/router/router.py +++ b/boswatch/router/router.py @@ -70,7 +70,7 @@ class Router: break bwPacket = bwPacket_tmp - logging.debug("[%s] <- bwPacket returned: %s", self.name, bwPacket) + logging.debug("[%s] bwPacket changed", self.name) logging.debug("[%s] finished", self.name) self._routerTime = time.time() - tmpTime From a098cfcc4d36dc2e0f7cd26946cf4e00190836f7 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 18:32:19 +0200 Subject: [PATCH 40/70] edit log msg --- boswatch/router/router.py | 2 +- config/server.yaml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/boswatch/router/router.py b/boswatch/router/router.py index 0c736d8..21d30b6 100644 --- a/boswatch/router/router.py +++ b/boswatch/router/router.py @@ -70,7 +70,7 @@ class Router: break bwPacket = bwPacket_tmp - logging.debug("[%s] bwPacket changed", self.name) + logging.debug("[%s] bwPacket returned", self.name) logging.debug("[%s] finished", self.name) self._routerTime = time.time() - tmpTime diff --git a/config/server.yaml b/config/server.yaml index 651b259..03e260d 100644 --- a/config/server.yaml +++ b/config/server.yaml @@ -25,3 +25,6 @@ router: allowed: - fms - zvei + - type: plugin + name: test plugin + res: template_plugin From b0a5cb57fe525f878e4a4864fe22ed9b9e9c59ea Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 18:40:25 +0200 Subject: [PATCH 41/70] add import log msg --- boswatch/router/route.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/boswatch/router/route.py b/boswatch/router/route.py index 4077076..42072f5 100644 --- a/boswatch/router/route.py +++ b/boswatch/router/route.py @@ -15,6 +15,10 @@ @description: Class for a single BOSWatch packet router route point """ +import logging + +logging.debug("- %s loaded", __name__) + class Route: """!Class for single routing points""" From 51e2b1e2584d1ea44621690323e136d09482f1ec Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 21:58:35 +0200 Subject: [PATCH 42/70] implement registerWildcard() for modules --- boswatch/wildcard.py | 24 ++++++++++++++++-------- module/module.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/boswatch/wildcard.py b/boswatch/wildcard.py index a57cce6..cf8ced0 100644 --- a/boswatch/wildcard.py +++ b/boswatch/wildcard.py @@ -12,19 +12,24 @@ @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 + +_additionalWildcards = {} + + +def registerWildcard(wildcard, bwPacketField): + 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): @@ -78,7 +83,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 diff --git a/module/module.py b/module/module.py index 05fd317..7598ce7 100644 --- a/module/module.py +++ b/module/module.py @@ -17,6 +17,8 @@ import logging import time +from boswatch import wildcard + logging.debug("- %s loaded", __name__) @@ -98,3 +100,17 @@ class Module: """!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) From 62fadd6fb3682c8a0d3001b3f7af78bf50054a00 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 21:59:04 +0200 Subject: [PATCH 43/70] add docs for registerWildcards() function --- docu/docs/develop/ModulPlugin.md | 42 +++++++++++++++++++++++--------- docu/docs/modul/mode_filter.md | 5 ++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/docu/docs/develop/ModulPlugin.md b/docu/docs/develop/ModulPlugin.md index 822af74..7e84a46 100644 --- a/docu/docs/develop/ModulPlugin.md +++ b/docu/docs/develop/ModulPlugin.md @@ -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,7 +76,7 @@ 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 dokumentiert werden. @@ -85,20 +86,37 @@ Module können Pakete beliebig verändern. Diese Änderungen werden im Router en 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 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 + +### 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. + +Der erste Parameter `newWildcard` muss im folgenden Format angegeben werden: `{WILDCARD}` + +Der zweite Parameter `bwPacketField` entspricht dem Namen des Feldes welches dem bwPacket per `bwPacket.set(FIELDNAME, VALUE)` hinzugefügt wurde. + +**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 `TEXT = self.parseWildcards(TEXT)`. +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 Plugin 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. + \ No newline at end of file diff --git a/docu/docs/modul/mode_filter.md b/docu/docs/modul/mode_filter.md index 03d1a2d..e837261 100644 --- a/docu/docs/modul/mode_filter.md +++ b/docu/docs/modul/mode_filter.md @@ -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 From 2498bdd5d47a2f348a67a4456e2a43b6fffd6d4e Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 23:22:15 +0200 Subject: [PATCH 44/70] edit docs --- docu/docs/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docu/docs/index.md b/docu/docs/index.md index a6840b4..f8f2e15 100644 --- a/docu/docs/index.md +++ b/docu/docs/index.md @@ -6,7 +6,8 @@ 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) + +[![](https://www.paypalobjects.com/de_DE/DE/i/btn/btn_donate_LG.gif)](https://www.paypal.me/BSchroll) **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.** From 2e188e6f7aabb3cfb592c14fcd234eaa388472ee Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Fri, 25 Oct 2019 23:26:12 +0200 Subject: [PATCH 45/70] remove log messages --- bw_client.py | 1 - bw_server.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bw_client.py b/bw_client.py index 2118c8c..587e22e 100644 --- a/bw_client.py +++ b/bw_client.py @@ -51,7 +51,6 @@ 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_client.py", description="""BOSWatch is a Python Script to receive and diff --git a/bw_server.py b/bw_server.py index 894d11f..5b0e6af 100644 --- a/bw_server.py +++ b/bw_server.py @@ -49,7 +49,6 @@ 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 @@ -99,7 +98,9 @@ try: bwPacket.set("clientIP", data[0]) misc.addServerDataToPacket(bwPacket, bwConfig) + logging.debug("[ --- ALARM --- ]") bwRoutMan.runRouters(bwConfig.get("alarmRouter"), bwPacket) + logging.debug("[ --- END ALARM --- ]") incomingQueue.task_done() From 2939bb93898819da1a7c04323fd2e7e004a7dd8c Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 08:22:36 +0200 Subject: [PATCH 46/70] fix docs --- docu/docs/develop/ModulPlugin.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docu/docs/develop/ModulPlugin.md b/docu/docs/develop/ModulPlugin.md index 7e84a46..0c5aaaa 100644 --- a/docu/docs/develop/ModulPlugin.md +++ b/docu/docs/develop/ModulPlugin.md @@ -81,7 +81,7 @@ Eine Auflistung der bereitgestellten Informationen findet sich im entsprechenden - 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 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: @@ -90,7 +90,7 @@ Mögliche Rückgabewerte eines Moduls: - `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. From a92dd8d94c699aba3c1c6fabfed95910b80e1d4f Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 10:18:55 +0200 Subject: [PATCH 47/70] update docs --- boswatch/wildcard.py | 2 +- docu/docs/develop/ModulPlugin.md | 19 +++++++++++++++---- docu/docs/develop/packet.md | 9 +-------- docu/docs/img/client.drawio | 1 + docu/docs/img/client.png | Bin 0 -> 28358 bytes docu/docs/img/server.drawio | 1 + docu/docs/img/server.png | Bin 0 -> 37573 bytes docu/docs/information/serverclient.md | 6 +++++- 8 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 docu/docs/img/client.drawio create mode 100644 docu/docs/img/client.png create mode 100644 docu/docs/img/server.drawio create mode 100644 docu/docs/img/server.png diff --git a/boswatch/wildcard.py b/boswatch/wildcard.py index cf8ced0..814d7a4 100644 --- a/boswatch/wildcard.py +++ b/boswatch/wildcard.py @@ -38,7 +38,7 @@ def replaceWildcards(message, bwPacket): "{BR}": "\r\n", "{LPAR}": "(", "{RPAR}": ")", - "{TIME}": time.time(), + "{TIME}": time.strftime("%d.%m.%Y %H:%M:%S"), # info wildcards # server diff --git a/docu/docs/develop/ModulPlugin.md b/docu/docs/develop/ModulPlugin.md index 0c5aaaa..1afa1f8 100644 --- a/docu/docs/develop/ModulPlugin.md +++ b/docu/docs/develop/ModulPlugin.md @@ -98,20 +98,31 @@ was zur weiteren Ausführung des Routers mit dem original Paket führt. Daher ma --- ## 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. -Der erste Parameter `newWildcard` muss im folgenden Format angegeben werden: `{WILDCARD}` - -Der zweite Parameter `bwPacketField` entspricht dem Namen des Feldes welches dem bwPacket per `bwPacket.set(FIELDNAME, VALUE)` hinzugefügt wurde. +- `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 `TEXT = self.parseWildcards(TEXT)`. +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 Plugin Dokumentation diff --git a/docu/docs/develop/packet.md b/docu/docs/develop/packet.md index 5a20bb1..11b29ac 100644 --- a/docu/docs/develop/packet.md +++ b/docu/docs/develop/packet.md @@ -1,4 +1,4 @@ -#
BOSWatch Packet Format
+#
BOSWatch Paket Format
Ein BOSWatch Datenpaket wird in einem Python Dict abgebildet. In der nachfolgenden Tabelle sind die genutzten Felder abgebildet. @@ -51,10 +51,3 @@ Ein BOSWatch Datenpaket wird in einem Python Dict abgebildet. In der nachfolgend |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 diff --git a/docu/docs/img/client.drawio b/docu/docs/img/client.drawio new file mode 100644 index 0000000..efe7227 --- /dev/null +++ b/docu/docs/img/client.drawio @@ -0,0 +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= \ No newline at end of file diff --git a/docu/docs/img/client.png b/docu/docs/img/client.png new file mode 100644 index 0000000000000000000000000000000000000000..fea551233ea142846c3e2802bf97b6f7a3fd9665 GIT binary patch literal 28358 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 d(uusmTADa^IT_{sVQkIRw+P3{{t@)y5|4@ literal 0 HcmV?d00001 diff --git a/docu/docs/img/server.drawio b/docu/docs/img/server.drawio new file mode 100644 index 0000000..7a1cd1b --- /dev/null +++ b/docu/docs/img/server.drawio @@ -0,0 +1 @@ +7VrbkuI2EP0aHpcCC3N5HGBmt1IzlcmSyu48pYQtbGVki5XlNeTr07Il3wGTZRYqE6gy0tHFLXX3UUuihxbB7qPAW/+Ju4T1rIG766Flz7Km9gieCthnwEjlFOAJ6mbQsABW9G+iwYFGY+qSqFJRcs4k3VZBh4chcWQFw0LwpFptw1n1rVvskQawcjBrol+oK309LGtS4J8I9Xzz5uF4lpUE2FTWI4l87PKkBKH7HloIzmWWCnYLwtTcmXnJ2j0cKM0FEySUXRokyWozn6Pl6y/y98gh3sP0j9UHpLv5jlmsR6yllXszBYLHoUtUL4Memic+lWS1xY4qTUDngPkyYJAbQjKSgr+SBWdcpK3ROP1AyYYyVsI3tvoqnIeyhGcfwDGjXggYIxsY31zLSYQku4MzMMznFeyR8IBIsYcqusFMa0Kb4nCi80mh2PFAY35ZqQbE2pi8vOtiviGhp/yM6bfsxmwTF+xPZ7mQPvd4iNl9gc6r+ijqPHK+1Vr4i0i5186EY8mrOoLpEvuvqn3fNtkX3V2aWe4qub3OZbIqAY8rAMbDY+GQIwPXrCCx8Ig8NkGjdo0KwrCk36uCtKknbXonBN6XKmw5DWVU6vlZAYWhjAd21VLsmmvV6g+nR+tDIpOgMJR8KP/edkYtnjtmUnsUpL3MbTJsXQeySiXLG3+LuSn4EKW2cwcVhqPtrig0vTxhGir9+YJg13QJw2h7NcCNt6/FaSTa4rCThKhNwiUFKqLrWJJUTpIaqvNKlN6V3Dz2/LwEcpKGnqJt4vg4pFFQkj4T5JS4jdFflEYH6adJow8L9b0MP9pWlSBNDyV+zDnz5/Dj8Dr8uKPyayn9UlAl5ApyVBnDjQWnVhi1INi351S7I6f+KKX+kEbtg6yVc8RvMYGyVvKo2QKEVFuVdPaMgsrFab9aZ7bxuM4B4AQvtZhfYwm9kEMOuIZQ1m5xwM3UIY5zGQcc1RwQNR1w3OJ/0zdzP3Td8ORqrjTr6EpD65q+NDsduytzpLCbudPBtFQ6mBv0Ea8Je+YRlZSr0jWXkgdQgdUKHJjB1L1MUJ4DJb3xzIEW+U7smLcc9i9PYJdC90sqoJvs/aEyo7yV2YhZCtEUEOw8tf/sh0QmXLxG/QSaMxJFfwawJQ0u45/T2vo47dvNFbJtB2Hiw8t76OT/BbKzVw+tjm5tX9OrjZTXCew/E4eoId50bF8WUj1dLLE63BFAXtZgzaMES0dF9w5TTKK0FLrKaGKVpukjBKNWODy+1QKO9xLv54dsNxPvm2O1Y0vaT+C3C3KO1ZVzLHRN0rGuSjpP3I1hrb5JrlmkjcELVbxjDbib/kBQQTcOlg0O0sciClmnxw3vkFdQ/fhs2jxobSWWyZvxCrqmeT+z2KPhbZr3fehu9WEm31SMuQ+/K6xW2YXqnqQrKBcp6YFW/Oy8jLDoPS6ddm3pvAETH//nls7OlwRX3YVb5x3Ed3bXVippMNRn2HWrFekwh3T2yktTyxMOsUeixno42DCeKNScwQdm+TdheoMv3wut5JH1zdAKagsM67QSunfqqh1yDsNRRJ1e63HBoG+ZM4KXUtGp84L+pHJLmR8Rvv2BQffg/QADldRmt2jNYOfdZrZcV1aMBqGaMWTj1I0Ke2j0M5qd6Cibh0ZH516votG08h5zMX9QLoSO1T/3ehWyxb8usurFX1fQ/T8= \ No newline at end of file diff --git a/docu/docs/img/server.png b/docu/docs/img/server.png new file mode 100644 index 0000000000000000000000000000000000000000..5ac8e715ac196e71084f78be3251322e3cc2d5b7 GIT binary patch literal 37573 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
    STx6G+U)*vSMFF#hKn-}or`CUKq8 z^wxNgcqh_-<+GOZDTC;m#)t2%b0>7PAaYgRmp>9Vs21h!;DGn7cL~v&8S}*?l zgQsg?a552_K*MsPG82aUBewrcj}=Wb06gQYNf6raUl6@m$V0;ao)y`xtw*#x2f_Jn zMaeV&zt0G4G_a<*u(dX@8{KOXDE{{ch+ECjmo|Z4k2#t>(WEpBdrb)3E+u|1_E)6Cj|byQ@#{*IeVc2>AwXHB#B!9En-=Z4)1U(|7~&Lw zzoU!Ty0wYa?KHmpQ@cu2dpv2KS#dJS_3!<9O1Mi3u4W@WssHLN$VN5!QB-U!NuF+L zhRsBoEHFqg)w%A(x3)^%a$lYUAP}0^F!R4l4Q!l=x2upj8Tu7Rojk4V^73+Fa(HNH zDcQQ1FO8q;-<9M?PjGR2JNJd#mB#?6qpPQf;%dD#`D({=Ja9g#N&8-`_iw0cl4= z8+7_r+^xb{NVd%Y`P#cCqJeQK-TWX4n+?jr%DP+9d0s(JV2L5L*!MsZTfUkO&dxvo zRDX&UAB~-VSyFvgRR+T=j)DpPzDDG2?+?f+JL|lk%749KKlpX4xD$??5Z}LJYyn=o z8AZ6%_ShI+q_Gao;IW}%wWeH_eQJ?Y581eQAM95G%S3PvY6nN?U?&ydkxi$9oSr+f z$rFn=<;#T+Xsibk$~5aF0VP|D;!qAlq+dw@QAFBCC)5A)am3)?F-iQL=;k&%v#C^1 zFY_PriA!hW%Mtu%J{T;DDG^W58t*9ydF0b3IHVWa#y+?cm$Xm+b%*64T4Q%OQJKt3 z3K63t&(;++O%A9RHhGPKV-U=&%1KfNWa z)jF?Y7WK>Ip>!m%v1s1!I+pfy0HhBOkXCR7AMf|@Bmh**#N|~FPgAUUgfKJRuQ=pg zjFbVMyxvpX7e8rJMO-!{<>Wvs{uYw9qSWx`cU?+3ZlJ9R2`On8Kq~cd@d?g)Isfb= z-61_^=LZ#qZ$yQW{lJRC9YW*r^|dK5HQj=&z{-u0r*W6DtC1l)e&RZXg7BLecg7|RBt)(@;%;>0IA+kt+Ulcm2Juh@SS~sx6KaRD+0FapT!M< z1rR|%M|kz(94usVMS8-rciWR)O*hvZfC>bgSpen`qX?kOpPzlerT<(7MQ4R-16a7e zas+AAm)q?KI8^#7>d!X{oCd^B5-YudK`q(Wb2&~YOgjF7u~+GBIi|I9t72GHRe4OGKe@lMf7>_d*0NwaD`suLIT00ln&X_cGXibx;x;scjXs|6QzT6YPM%1>a~VR*waj zS)uXj_~oKmF;I&cIQKoiH9y@__<-dy@=;TfLG2I+bWu8p7y2ZG$|-c(W3wnaE{+Kp z_oDgCfp=uwhyysFDsG*7WmW?;z!p`Sm7R@E>k~?afo%La+lD8~iJ>6l8+ZF4%0(+6 zwr=Y#^Iav4=iP;x;W)U72LlquMxNW1W>Nwkhp#esqp(qA9q6>!ke>j0!wnF0QGnx? z;^GZzm?%K-vK40(eYXWkL`n`JLx9ngLhpPf#Xel};HNZxm#W}zc<98{$t+J>2)>|P zKQJ3`0hgH`cI$(PBceQk=a%$A^@)!$*Bx2lsD4lxf&vv_<&)!I=03zQ+%%5j8mZ*R z)|R+9C>l&0%q!sdVnE1lGui{#+{#5u^#@i0Zw~;F`*_)~+lte1@oCKoysw;`o@H2k zKvIK3>kpX_{C0pzVm~`O>nOW!y19@L1}pN`))XBR!vKI&k6h-UDg0Iuy{W>(NIrWS z7>b-W3xF5NJb!a!quZdtE(%l2l z)X~!*pyW(G-T&U`aFJ++)PxWgEAhuYl(Zc)2=9x2gNF+rAYcToCd_J_Yqw+`187oG zeG)Jp54nv%UM?7B>ZYKBR{3=>Cr`~B+TxKD54j+C=?lPdVc431j6g!8<<{9K39bJc zT>UsMX?MZazEA5})6qp~Ck@AEul@Icw0NP4Lh?He(zz1w-3u0<0x~<*fj7f0;_;%rYQi-(Bg01|VZasGa z!0W}i`Pu3#u{3J96BFmAsCouwR0jOgmF>EFje9N9G_$u>s~73CTT%=+!IxETJnZ~Y zDW)HgUphUHHY`K}96zdjyT<-qw^LT(gsS(J=XC!@3~OZYQU|_~3r#+^%5|-q>nl}~ z&}h_u*8M^pa*tsNf|KU;pwKfmnY4^?;`21mJ)GnqRkw1jTs3C7fzP*HNQ9YF->)Pv zK_Pm-lR_lZ0KEca6YPW**R;=ci}W8e!K)^QB9+~OI@C}QO`i>@o^Ug4LPB<27v5xzq$K70;$|9N9HRXKLgRr_|F|x zOahiB%sLLu8AgVf*h>INOmVtwG-PhJC~GO(>yk>iuOb6!5! zxnp#9G8YdHFpnO{HC>ES(A0=5qAbd`+BWuCz#7_l* z9xRInL`9aip4`Dd2%+_P1;;^W)xiIsPZ^6d!R}~dw2$wB?gI!pnfQk$5Hd5XydRB} zl{O6-2hqKX*XO(ae;(DpMsS@$H0YcWufF2z@cprRmrB?+3%V|)8`z(NH-0v=`irI8 zt#vZd{X+BU`h?UI2H0#Vsixo3m{0~1)FhK$lGY*gh0PudZS+s*Ex?nnj%E+fnGv_H ziPr;SvF}>{+RJ7)asH!uH6x|rt`u>9Del9%N_8Ppiu3L2S(Oa&FA6gZj2Na2Hy<6C z+!GJq8&KfV7pUJsCqd`Q@0GEOMq`3oc*`TH!@b~$qllKkmI2p$nV;`-y_239o-h9c z?U6|!W`ak>5ZemN5&n!+4p&QkCu#=5V;S6Y`ubZWB>ai_eoPk&!-)MdyL^WF z-bl{hs5i~huya8WM51A{q*%KFL}mXx9oA`$0P*uLD8753TPuy%u4MVkr2FuX4kleGIw>5d$Up zgp2&o&rL80cVAcnrlmu4#X5>%t{@#FGgn>gD7Ka)?SqFH=~WE|x~~n2rX?ClyFP-f=2%T+wjkL z@C9urpH~6=T$%zvYe@EPjF<4-y?eLijnbwj251I!t0beCe|CZopl4Gsz+hxAV9C!h26eXw))+#;O;>UJngk+>FsM7sGe z{br9|XrS3OADhvwBCkNc1#?OLP0Uh+Wgwy9of$SeJhk-{5(YY_0-Q6ubR@rYc@DTv9s2J0Ej!aHRiH zPZiQs6J90(O~-iSow^>g5IpKz#EH}7>)f*!kyrDt5-18vZhg+v4sG|NQ&YWu>Kc1) zKNh~&D!*zq0ZHcyD)|;AT5>W?Th$X&9oEfYJu{Z-&1L8^`oi*e8FPT%*1n(5i8h+y z)E^gMPEfzCjCA$&#Q{T2;(e{#sQfmtbwWWyFr3|d#+M6vWtgD1{L^|sw{QX!iU;_q zO|yV(JXEMxo*s?Y6m`E9=N46}$C_Z^q-X$8RlUe*no7ms2|_@!!;Y4xcekOx&7Yd z1~X9?0K8{~*47}LTP%%~@vaG%URnD{kGunbpsa}1ySs^rxc;%}kN*xT1r|V5Zn*%B zyL<;w#__u7x7qF-5^R~sE3iulx z8;QK80RTnQulr%HUEUt(@;hPvnI<#BffNo?;OZ1uhZVQ&O8wyYn zOgF%(N~nq=2{{EZw!o(Lm@%?7wF%&50*(g2S331#+g`l%A|>(Nyuajr64`TG2l1Nz z{hsU8RIFTpP1nyYVz>7jbJvT&-4gvurO8TbtLq3!aL60-63&A+EbV6yBl^5IXSQGazqlXj!|aj$bh6-VwGEPlBI8&Gn&uSg^A<%W$b~j%{UERKCZp+0?Dm8T01q18f^F(ZTy`1Avs9ykj?zCf4AyGkugI z=-xbcY-O8}GtfPh2ew$)=9xbh4XaPA{S`=Y!?)5olnO2o#A>{=FE;CX+#IgcZyyz- z{6@9{^|77CZk6XAC^`~E-RaYVsD-uwGxXkDJ_YBySS+Q7L~T)b0N#dJ+~f(Ux)@6C z&E?1lqhoS=kg?N3VtKkRZ`EdD1d`J02b%JE;1+2!1C)^)HdyuM;^YvhDZ}|upWrOH zqznZ*9R}KRC!EMEK&UT~uZ`6SmFj6)dP~IkK?4(i0y&*)3G8c?^`kT4WJVkV?)sx` zfPb1@k?uL2s1WFD}b2x z?#k&c$%WP)RT=cSJt5@|olzl6Ju}kKb5DTh9e$Gcwnq$>xW*Q%g&HsJbc1>7k6^Dx zz@f49BEO4b+3)p$8|hIp$X)t+x)OU#xy0Y`x+}Bx)g?V$LyMkJ*7{LLbh;E5-ZKU6 zp{IN32t0l^yoIM2rRaH*O7ylhUtXt74h;n(t6tmU=sXm8&Au+LfSu>B`y~+4^^HzR z^Nf1u9>-Mq_{8egmBgxVzdISi)4qRZb5U=n^PtgsyYkwGNf|ALL#Mb9lEgPejPVKA z9aC86!Gap*M?&tlXXt!2?m7+M1jt$-tzoYD3}T^wQ)n)d@3V~~XMysl!7t~QHq4R_c=nEPy#n|AxVovKe#d)f zbHO7YQMk%NHQoE5IZVuBRdlwNp4*1e^TV&`yD&!dHoN`14%=PtMpygGEN#_YhoDqHS&S0e?lk zWJqhj-ns}>aAlfuN2Q{2#*?G4(4_CDUcqXQ?nt|NeDg$;EZIFN^(BjHIDc#~=T z2s9rQ?K9@gXZ)$S3suWyL@_vuK^OJvc>I+5qQ|%jDpWrF7b58JPud<*> z(E5DXZBk8uam6REvTuWE_dwpftnV`}_C%u3F+@*Y;Owgahm#mCXbP*tEGYR$cDroX&LCFxIvPq9W4_8RqwUu~1?|XS#Sw z;eTwJ9t!C3b|etNGRlllN?a1>gOS=DzwLF8+VCX|0O&051%lDc?xbbZH5kwVMrPS@ z(cp@?L`*wc0y32%vHC1hIkrhf`UJWW;Q=2_gJOw<*c_5ds=f|8<%v7)f75_>lVd2O zDo2)KORpbDVK$jcz$s+Epy=D~VNswB-jffz46`|uUHaZb`$698ozsVmdVM3u zJ7(eW=4I%ETqP?Zw1d!I29{0wWj=-`9O&jc`ju&@g~wf`HZzJB0?L+kWFE|IA9;Ua zY6$+g_-z;Ry+9BDime2X zB1tkqXbKRmjGA7k&f3(>%AkT>s=TR&H81YIl&b2&t8^)5fN$qqAD_?^Z+C3<!V^)*k`hpE3(>2~ zYqAW+n3_US42k66pTk~Mc^=!mykNi6#uAM#{+aq=dE7?4fAxN#fzl)0{vTpF8?q(7 zS}rR?M6iAu^SjKQJSt8iXlCoRyG9kU!uVl1I_&C8cnpFI*yh()1ojxsth7tE{;YpN%o@mxM zmHwFd+B-HaQj3J|)!tS$Au2hfip4TDV<8ox)AP{4d$j!lrrG#ZC~>gJX+3(FJ0ug1 z8scSwHw@^MTt7S(PCvdL$a(X8BHoX@+)B*zjfJ99a39H|%AiOoB$JSb zi)V02?g5~V`-;5aQAnABh86^q0Z6FIaR-G&Cuc#BLD!?BZ&CzNx~CK|*%DW)j2kpC z3T01KPQ^D%X6j@H>u8sKZ0?oQE}&923d>%LFr}{xJ^0Bx z(1)xnn*w_eh4Z}?#aV(kGLu-MC_IW0S93Dkm#vTN28(>AUUsoLAz-V=rh_l{xyn|z z;u+yp|GbjF3V8UP8(ro47ZzMToz<Uaxz5_a+qs+LEH*tq z82^R)c*_ib+?f$xb`W27O3eYa`R1!5)iG1%xTRrj-_@OHGX>*~LsotlsIV2A*t#oi zC=jJ0ajSHjLGiu4PeYH87{Nde%Nq^X3#{zz99fINwWam_U(I2FQm`D7i3{ zML)e{rj*HzOEcOX_bL&aX<&GrbDpG}0+mK>0K%s><`fYOb9TiAk(onp(FyN|Yh4P& zCs)twY((#w0iqT|$KF}pGA~}hGevW=E0>a|SKqs69ufNY4IT44`cY+nL^jJ)!Drrl zuY1ADIqmtY{#|E0r>RC$863hI<4A~{U%kW_NYI**%sEtlr#dTODywirU0eC6b8Qr^#nmZre!IC&_89xqz z5-AgxX}?<^Ocb~2My}WKK@FCMpMbYyuI@dUT#xYaPri!dFAxHh07DPO!|G;P0SrC8 zXe2h;8d`|i4R$o^TtME_r@j_evP)@;&0WP-o5Jum)vIvvPCADn*I)y(nVX#n{yU6Q za=F4`eA6Lif+Mm?aeQ^Z+KzOwCw>XT1B4`1BGbEn(nI3EPCTfJ{b)X%Z;M#{Ib#>S zorkdbYQ0T$2zQS8gKbp62~99pj-O0nw-WQ7tIO@KnltZBQzW?U)fL!K`ab4VQy{3v zNo3?Xz3-z)ur!cX^t{@vkxAz?yU}!ew(h7$%79nc;>Tz z`t-|+MqG3;a@f1YJ(3Dyj{Th`6F)qCn*)MFx*D-DEAC?V8zDlZMJ%~)wRdjBtNeZh z@;=SOvRsiQm+;~>hjk>Vd-vh|eNLAs-4tE$}Wbog}qv9JWEdJClU0pHIQQFmN zA6Mw)Y^Xpc{*h%xA?$_qrXoE=v zWwo%kc01~$;BU?`;o&DP5S16P-pyqHV;pC~-iy`X=I5WVp`wCaC^u;oJ-Va`CYsi# zo^WR7rer!x*mL(?KZwfhG0@eXC5t~n1KTsL*hlAye}}1stXw%uo8=&wOnR$h~{xA$Eo>F5Rxl!tr*jS z4eO+vqomNzoP0)A(CRHX6*Wq&S+ToP0y(oOybc|Sk*_wc%2LuwnyR-y(LAYTH<;H%;sD{x}m9OY0coEH=v+CmeVDoUzrp zK(q38MP^Lhr|LdUJE^yNypjw($g5dRJuMMB9saSEnkPXmvqqp|?&83mw5wR!hWpYW zb!^K|1&iuU&%uU^)WLIu3fk5E8`a-A?yj8Sfg08ZO*-|Am)bXXRtZwWX&hb-T(CrZZ~DhDG(zD}B!zTGdf_ulM|_{L|GH6@M` z*Ry(wXQ-xWm49P$K;67eV3P2>{6wr)uiNL?>wSuTuuoZ>ztQZ33v7qxRoU{~i+z-Y z{o-Z^@7LV37#F8a0|8ip9ah{ylbvo}Rqg0O+WnoggD;mSE4j7h57Ih>q>MdzTPVNbJ;Sw)H{>jbb*@D3rE)wFxD$MQY6qRI z4ft*>gv6LEe$FG@lFJ%MBF5Z?bn;z1cZTQ8kzeYsj;C~jN3k!}Dym1iDWI}yeauMQ zY!C7mh#_BY))OizeVd{{S?j3ZnAY7F2WcVjz6)K^J>_n;7I% zzxUI9A4f{Nel}4?u{Kyyu($rP{HJUI6OmVgKo$u;Zg6K|_#>Vj^LEUjRwVfHtZ39; zWQhXfw;jZ5^lk%GN@jFk!C}+Y(C@h7=?zn?xzTwe>x2fs7(GxcuXJW7T8`OM zn5f!_TvS}Rl`AUqBC^Z6Z3KM-FCoRBM7w5@k!jPM780I~@HLi@#KUuL_xbP4LR0M)H$BJ;>v&$6^a|jx8{Ao*6dM<-^bogfyQ?VXD^BHGVsGo|fc%DJoU;z6 zm2|d0PW)`>Dm%=aWN>ov{F5*%RGNk$E;&NbzgR{R_C_)bUPwMj10^0L`0g?PZIuwj z$s-@ASh?w$STnwuw!e<{W68{^kyDIq=jEU`iuf!kYXirEUyx|>Iw20}%s289is-eN zy(y)Fb-!m-8BJx~PlDKLNChkFC_%PSf#GT>^rcms*!aQf_Nic>`V@_^WbRt-KLtkXEDBTy z>=Ou1AW5FNGRH2$!_U6BazHnKKbSyMQ~0gQ z)T*uW)#mvpa=G${8dCYj^#90?^l5fM2p&l>5oroBFPTwFOmA_?7Y%GQc6~Guqdd5+>2)ijG z33xo{F2d@eu$5pm@sM*jLXnC{QwN z!zYQ)`{5))-@Yj3er5r0OxKIG>YH)Ps*jQ;t(XYpN9C<(5e8x&MH9PUZ=L!rkC!aj zE+!6`{wUg9Er~oDJ2K>L))^F~9b!Yf!$DEzujT#8B&)*cv9cvd zqe@gBIMH8V*jz}FX!@b%>9Hv!pT0rf4Qi<>v|J~hC!`q1P4YpJ^YxNo43=hTmgC0H z!b2v-;1~+*b(Y~C#P;jYMJ7L`*e1Wg@PDFL^+h*qnV8&;9&W4M?72-2*6ag+=bB2? zKXg^`;F}N18U5Pzi#6Y94?1ZvMHB_fm>DKE+=p5_K5WYt=YOG@!}@#LD*i3E^Z8JU zYrnsmenhOHTsORVE0c-{CNmi=h8{}i-Z>U14N_NU<$TzS2GP|y?Jlhg*^0taTz>DZ z_2Hk)ukiOWH{Ukj?Mx-W#XyGH62ML$*Uk1D#&cdodl8BRA|_m!PAkVtSOmqSsy<$A zoe?qSAH|5tOg0{fp?o+xFIjvQd2)$hi)*G1grL$lFts=JZe(-nSB_8PWIVNbjBM(L zm1|{z)>ZEeb`ok4d^w~6Vf_4fWV5V_kc}Q`I^TYVqcVP@z(QgX&$u`BQznyN+?iJP zIA3H$kWn8P;bZMwY#QVp`oZ^-9H#1h2%b+!WVPy7dcTa7#_Y#@7eyYOgSYtjPdrkw zfJlY0!;RdL@&pJzXI|v=Eb(n@RI7xAPF#$FLd0|S9);a4V<;l^&2M?$_>XHJO{t-a z6+*_==cy&jY}@=caY#qrNkJYM8iX>H0H$}x)63))?z3?Ra}scZPLJ;<5R^F%gKOQ> zT_*il;hTzW)5q{fWuqH{pJGj6z5zJOU*0D9j4dBbelX;R+j?#`mf7&XIu0LXgM}#E zn}Zi_3@L3ZGr^aJe77J&?fnj^Z=F;=KT-d@-S+%YUE7fNxr?8;?#%MxdfC$s(c@20 z@-1vJLWDloH{X%#UrbHhGoz($_W909!yO~83G{FqB4aIyFtaHO_;I-e_We9w{_wn4_Fqgc`&JU&?lM5V5PBvZL zI~)9#7!fl^r!Q$m{`m*nUQtH!`QiKDQ51~Tlj-(lM-YRHxa@Rvy}$=nbkZGP+~wPM zdhC{>s4K9ieCT@4;*IbPb3_un51rYhg1xX0&+vOXZ5S_J)hVglc_3^kZ}+woh+fH# zHr+3EpOEd!uvXt)->K8@G?-tKBJiY}7<_O)jy zpzSX?)NB8vR6vK!lj$baY&CQiK0-Mddjl3)?qa92W$ z{0sw`sWy=WC-Azno)j_$S>3AW$pzIJ+u1>$h%*e)TlOAe_?`A5&bvTfY)dP&zp^M9 z8sj)s8EPPtRpt}#z}1v2Klq#XL#Z`o+X+Z9pKTv)VE>Qw?l$jIg`_4+e9q@*kw#Hf zz3J+Y&V7{Gz!2Za`5?#O37Rqitf8WPvkh;l_gVgxcziWaK1cvRylE;DMA*=>=6D`{(sr$PvdFs}k^2oveQxUPlrzOdqsMSh}Oqu6nz|Mq9;R60Cs=Cf;P)vzVO zFHG=j0q20SIG35Ot%h8=o} zp8;G!uKC5&Kdx`@-u|}1;8MMq&UOuRKdCC4BpoD`7Ff+!1y?B?2@-?+(19}GY84%fDQT4zX33BN})g?SRm2DN4TkRyVxmtu#UHAL(!IPwRmD$zkwyAq=^ntO1zWh z-nr|~FFVKo`Antu-L`%y7P|!nM!-5uC?}rIp%>gz>VvK99m%H zVW?#U)BvK;fN=vtThcHcmdKR%;F6J!ATp$YP|`Rp1JsDGfsQDlZH7|HAmLRan4V>( zGj#v$-E+>KbH4Ap_s^YsKZXVjmq7y#-#=-58}Tx?;KAXo8&k7kNnsl!ywlPb_0cS0 zmfg@+(KZ7GP6m5Dr#je?llQa}+^q>d{v zJ~|X!PEsDF;XY}W!%y}hayB5I(Kxsg%5*d{7dPbmDS^!=Hsi*#{_Hqj{%OB<&h5Pr z;#qS|W@$K@T#`N)#uYd66%l4_T26O>_j|zpApkhTm?KLV0ZzG+^ec>#B+eAns?fjG5Zr5RbfErw<4gtEVD8TCIf zK)N;rz1yXDK6UyK-0ZSG@ReaL8!BQRgNo4J13m}=7^}i{mvKyziZ5l3`ZCpqEsC5@ zYwL%)r0T2ZwXoyYx}Ik!XU!ZH*AQgx8)%T$W2 zz_AO#ndp9Ju}_-A_z4ywx23yN-?I|LbhIW?xmXp+zp^Y!3+z*r&W*&MwX{c6F`Tj- z@h^sGG%yo6k5RoBA2wlQ9J4Wr8?|ZleM%`H&;Q2}fK|X;lFh`m>g=G}#wqf%@Ccz8 zh@=|!xPTD((Yot6CMQk$e9QEiF2>_)=J5p)ItO|h`8kE#RH~jqd;3U36-5d{PO%a? z^uo{c62CQngIEBQXU^RNwuqKZ)Zy3!jtz;l9FLGSVZTrFO)sXSD^xrjm_3(|20B^< zDEN(@)aH|!eo>w$<8gJ}qdjiSoGM`8TCM(Tj!4VgoGOT1^PAnH(PWr5_@jvrKka_{U8QdWI|iNH3oU#u8cU7P z)!PDu>d$9te#th;@Zo+KM>J{xFgFH1?+xqD^;iXr1T{O9Wu`L>Z=(B2f%ce7oLqTAR5LVI9 zEvb3J>h{~V3pz>9Memmx#Qzz=u$Kd%X=NGs6eLQ=r9TPnfK+wp=;99FULj=e<)Ay^ ovg0T5G;Wb@LpywRQrmw7H!kKXe@}YL-VC0PK1_(}h!jfy1&i|_m;e9( literal 0 HcmV?d00001 diff --git a/docu/docs/information/serverclient.md b/docu/docs/information/serverclient.md index f0d6bca..6916b67 100644 --- a/docu/docs/information/serverclient.md +++ b/docu/docs/information/serverclient.md @@ -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. +
    ![](../img/client.png)
    + --- ## 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. \ No newline at end of file +erklärt werden. Diese steuern die Verteilung der Daten an die einzelnen Plugins. + +
    ![](../img/server.png)
    \ No newline at end of file From 61e085d55544e3879f322ed5bbafe63aa5473504 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 13:41:17 +0200 Subject: [PATCH 48/70] add regexFilter and docs --- boswatch/configYaml.py | 4 ++ boswatch/wildcard.py | 1 + docu/docs/modul/mode_filter.md | 2 +- docu/docs/modul/regex_filter.md | 70 +++++++++++++++++++++++++++++++++ docu/mkdocs.yml | 1 + module/filter/modeFilter.py | 3 +- module/filter/regexFilter.py | 65 ++++++++++++++++++++++++++++++ module/template_module.py | 3 +- 8 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 docu/docs/modul/regex_filter.md create mode 100644 module/filter/regexFilter.py diff --git a/boswatch/configYaml.py b/boswatch/configYaml.py index 74af87e..3eb6521 100644 --- a/boswatch/configYaml.py +++ b/boswatch/configYaml.py @@ -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) diff --git a/boswatch/wildcard.py b/boswatch/wildcard.py index 814d7a4..bedaac6 100644 --- a/boswatch/wildcard.py +++ b/boswatch/wildcard.py @@ -35,6 +35,7 @@ def registerWildcard(wildcard, bwPacketField): def replaceWildcards(message, bwPacket): _wildcards = { # formatting wildcards + # todo check if br and par are needed - if not also change config "{BR}": "\r\n", "{LPAR}": "(", "{RPAR}": ")", diff --git a/docu/docs/modul/mode_filter.md b/docu/docs/modul/mode_filter.md index e837261..f805835 100644 --- a/docu/docs/modul/mode_filter.md +++ b/docu/docs/modul/mode_filter.md @@ -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` diff --git a/docu/docs/modul/regex_filter.md b/docu/docs/modul/regex_filter.md new file mode 100644 index 0000000..8b19aba --- /dev/null +++ b/docu/docs/modul/regex_filter.md @@ -0,0 +1,70 @@ +#
    Regex Filter
    +--- + +## 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 + +## Resource +`filter.regexFilter` + +## Konfiguration + +|Feld|Beschreibung|Default| +|----|------------|-------| +|filter|Enthält eine Liste der einzelnen Filter|| + +#### `filter:` + +|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: + filter: + - 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 diff --git a/docu/mkdocs.yml b/docu/mkdocs.yml index 296b21e..1942050 100644 --- a/docu/mkdocs.yml +++ b/docu/mkdocs.yml @@ -19,6 +19,7 @@ nav: - Changelog: changelog.md - Module: - Mode Filter: modul/mode_filter.md + - Regex Filter: modul/regex_filter.md - Plugins: tbd.md - Entwickler: - Eigenes Modul/Plugin schreiben: develop/ModulPlugin.md diff --git a/module/filter/modeFilter.py b/module/filter/modeFilter.py index 4e08623..2e97d1b 100644 --- a/module/filter/modeFilter.py +++ b/module/filter/modeFilter.py @@ -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: diff --git a/module/filter/regexFilter.py b/module/filter/regexFilter.py new file mode 100644 index 0000000..7f9556c --- /dev/null +++ b/module/filter/regexFilter.py @@ -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.module import Module + +# ###################### # +# Custom plugin includes # +import re +# ###################### # + +logging.debug("- %s loaded", __name__) + + +class BoswatchModule(Module): + """!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""" + pass + + def doWork(self, bwPacket): + """!start an run of the module. + + @param bwPacket: A BOSWatch packet instance""" + for filter in self.config.get("filter"): + checkFailed = False + logging.debug("try filter '%s' with %d check(s)", filter.get("name"), len(filter.get("checks"))) + + for check in filter.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'", filter.get("name")) + return None # None -> Router will go on with this packet + logging.debug("[FAILED] filter '%s'", filter.get("name")) + + return False # False -> Router will stop further processing + + def onUnload(self): + """!Called by destruction of the plugin""" + pass diff --git a/module/template_module.py b/module/template_module.py index fe6a557..a0d7d15 100644 --- a/module/template_module.py +++ b/module/template_module.py @@ -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""" if bwPacket.get("mode") == "fms": pass elif bwPacket.get("mode") == "zvei": From c65120c022fb911c29fa695e75203ac25d7c67a6 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 14:06:20 +0200 Subject: [PATCH 49/70] fix docs --- docu/docs/develop/packet.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docu/docs/develop/packet.md b/docu/docs/develop/packet.md index 11b29ac..14d2e88 100644 --- a/docu/docs/develop/packet.md +++ b/docu/docs/develop/packet.md @@ -48,6 +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)| From 2c6973c122977939ae05b41257d160a5a8917322 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 15:18:41 +0200 Subject: [PATCH 50/70] Autobuild Docs --- .github/workflows/docs.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..349b522 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,30 @@ +name: BW Docs + +on: + push: + branches: + - master + - develop + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.6 + uses: actions/setup-python@v1 + with: + python-version: 3.6 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + mkdir log/ + - name: Build doxygen + run: echo 'tbd...' + - name: Build mkDocs + run: mkdocs build -f docu/mkdocs.yml + - name: Deploy docs + run: echo 'tbd...' From 37f6a5f3be8d15a2456dbc84e4d73b6288506646 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 15:39:33 +0200 Subject: [PATCH 51/70] Update docs.yml --- .github/workflows/docs.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 349b522..7221620 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,10 +20,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt - mkdir log/ + pip install mkdocs + apt install doxygen - name: Build doxygen - run: echo 'tbd...' + run: doxygen docu/doxygen.ini - name: Build mkDocs run: mkdocs build -f docu/mkdocs.yml - name: Deploy docs From facd7be644062adf862a16a4b0537c43bfe86070 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 15:46:23 +0200 Subject: [PATCH 52/70] Delete docs.yml --- .github/workflows/docs.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 7221620..0000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: BW Docs - -on: - push: - branches: - - master - - develop - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.6 - uses: actions/setup-python@v1 - with: - python-version: 3.6 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install mkdocs - apt install doxygen - - name: Build doxygen - run: doxygen docu/doxygen.ini - - name: Build mkDocs - run: mkdocs build -f docu/mkdocs.yml - - name: Deploy docs - run: echo 'tbd...' From e8c0446943b0b29c6279ee8a026ca817da101666 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 16:23:42 +0200 Subject: [PATCH 53/70] edit docs --- boswatch/network/client.py | 2 +- boswatch/network/netCheck.py | 2 +- docu/docs/img/server.drawio | 2 +- docu/docs/img/server.png | Bin 37573 -> 45722 bytes docu/docs/index.md | 14 +++++++++----- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/boswatch/network/client.py b/boswatch/network/client.py index b8f04f8..7792eec 100644 --- a/boswatch/network/client.py +++ b/boswatch/network/client.py @@ -86,7 +86,7 @@ class TCPClient: def receive(self, timeout=1): """!Receive data from the server - @param: timeout to wait for incoming data in seconds + @param timeout: to wait for incoming data in seconds @return received data""" try: read, _, _ = select.select([self._sock], [], [], timeout) diff --git a/boswatch/network/netCheck.py b/boswatch/network/netCheck.py index 0674ec6..2c208dc 100644 --- a/boswatch/network/netCheck.py +++ b/boswatch/network/netCheck.py @@ -27,7 +27,7 @@ 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 diff --git a/docu/docs/img/server.drawio b/docu/docs/img/server.drawio index 7a1cd1b..7273c8c 100644 --- a/docu/docs/img/server.drawio +++ b/docu/docs/img/server.drawio @@ -1 +1 @@ -7VrbkuI2EP0aHpcCC3N5HGBmt1IzlcmSyu48pYQtbGVki5XlNeTr07Il3wGTZRYqE6gy0tHFLXX3UUuihxbB7qPAW/+Ju4T1rIG766Flz7Km9gieCthnwEjlFOAJ6mbQsABW9G+iwYFGY+qSqFJRcs4k3VZBh4chcWQFw0LwpFptw1n1rVvskQawcjBrol+oK309LGtS4J8I9Xzz5uF4lpUE2FTWI4l87PKkBKH7HloIzmWWCnYLwtTcmXnJ2j0cKM0FEySUXRokyWozn6Pl6y/y98gh3sP0j9UHpLv5jlmsR6yllXszBYLHoUtUL4Memic+lWS1xY4qTUDngPkyYJAbQjKSgr+SBWdcpK3ROP1AyYYyVsI3tvoqnIeyhGcfwDGjXggYIxsY31zLSYQku4MzMMznFeyR8IBIsYcqusFMa0Kb4nCi80mh2PFAY35ZqQbE2pi8vOtiviGhp/yM6bfsxmwTF+xPZ7mQPvd4iNl9gc6r+ijqPHK+1Vr4i0i5186EY8mrOoLpEvuvqn3fNtkX3V2aWe4qub3OZbIqAY8rAMbDY+GQIwPXrCCx8Ig8NkGjdo0KwrCk36uCtKknbXonBN6XKmw5DWVU6vlZAYWhjAd21VLsmmvV6g+nR+tDIpOgMJR8KP/edkYtnjtmUnsUpL3MbTJsXQeySiXLG3+LuSn4EKW2cwcVhqPtrig0vTxhGir9+YJg13QJw2h7NcCNt6/FaSTa4rCThKhNwiUFKqLrWJJUTpIaqvNKlN6V3Dz2/LwEcpKGnqJt4vg4pFFQkj4T5JS4jdFflEYH6adJow8L9b0MP9pWlSBNDyV+zDnz5/Dj8Dr8uKPyayn9UlAl5ApyVBnDjQWnVhi1INi351S7I6f+KKX+kEbtg6yVc8RvMYGyVvKo2QKEVFuVdPaMgsrFab9aZ7bxuM4B4AQvtZhfYwm9kEMOuIZQ1m5xwM3UIY5zGQcc1RwQNR1w3OJ/0zdzP3Td8ORqrjTr6EpD65q+NDsduytzpLCbudPBtFQ6mBv0Ea8Je+YRlZSr0jWXkgdQgdUKHJjB1L1MUJ4DJb3xzIEW+U7smLcc9i9PYJdC90sqoJvs/aEyo7yV2YhZCtEUEOw8tf/sh0QmXLxG/QSaMxJFfwawJQ0u45/T2vo47dvNFbJtB2Hiw8t76OT/BbKzVw+tjm5tX9OrjZTXCew/E4eoId50bF8WUj1dLLE63BFAXtZgzaMES0dF9w5TTKK0FLrKaGKVpukjBKNWODy+1QKO9xLv54dsNxPvm2O1Y0vaT+C3C3KO1ZVzLHRN0rGuSjpP3I1hrb5JrlmkjcELVbxjDbib/kBQQTcOlg0O0sciClmnxw3vkFdQ/fhs2jxobSWWyZvxCrqmeT+z2KPhbZr3fehu9WEm31SMuQ+/K6xW2YXqnqQrKBcp6YFW/Oy8jLDoPS6ddm3pvAETH//nls7OlwRX3YVb5x3Ed3bXVippMNRn2HWrFekwh3T2yktTyxMOsUeixno42DCeKNScwQdm+TdheoMv3wut5JH1zdAKagsM67QSunfqqh1yDsNRRJ1e63HBoG+ZM4KXUtGp84L+pHJLmR8Rvv2BQffg/QADldRmt2jNYOfdZrZcV1aMBqGaMWTj1I0Ke2j0M5qd6Cibh0ZH516votG08h5zMX9QLoSO1T/3ehWyxb8usurFX1fQ/T8= \ No newline at end of file +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 \ No newline at end of file diff --git a/docu/docs/img/server.png b/docu/docs/img/server.png index 5ac8e715ac196e71084f78be3251322e3cc2d5b7..f19189f0729015384f7ecaeca2a55a4721373883 100644 GIT binary patch literal 45722 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 eCPO50U{x56+wYR^bJWm54pH+|i6g}sN4xs*1 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
      STx6G+U)*vSMFF#hKn-}or`CUKq8 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 diff --git a/docu/docs/index.md b/docu/docs/index.md index f8f2e15..114a3a5 100644 --- a/docu/docs/index.md +++ b/docu/docs/index.md @@ -3,11 +3,6 @@
      ![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_donate_LG.gif)](https://www.paypal.me/BSchroll)
      **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.** @@ -16,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.** + +--- + +
      +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) +
      \ No newline at end of file From edb0fae58a68028aa400cc1b71126328436a40dc Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 16:31:08 +0200 Subject: [PATCH 54/70] fix typos in docs --- docu/docs/information/broadcast.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docu/docs/information/broadcast.md b/docu/docs/information/broadcast.md index 1491410..70aad2f 100644 --- a/docu/docs/information/broadcast.md +++ b/docu/docs/information/broadcast.md @@ -1,6 +1,6 @@ #
      Broadcast Service
      -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.
      ![](../img/broadcast.png)
      @@ -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 `` 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 `` 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 ``. 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: `;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 ``. 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: `;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 ``. 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 ``. 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. From 3b7847a5153cafd7273c32cdbef474bb0d9c6205 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 18:35:31 +0200 Subject: [PATCH 55/70] add some documentation --- boswatch/wildcard.py | 11 ++++++++++- docu/docs/modul/regex_filter.md | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/boswatch/wildcard.py b/boswatch/wildcard.py index bedaac6..71d1c42 100644 --- a/boswatch/wildcard.py +++ b/boswatch/wildcard.py @@ -19,12 +19,16 @@ import time logging.debug("- %s loaded", __name__) -# todo check function and document + write an test +# 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 @@ -33,6 +37,11 @@ def registerWildcard(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 diff --git a/docu/docs/modul/regex_filter.md b/docu/docs/modul/regex_filter.md index 8b19aba..172901f 100644 --- a/docu/docs/modul/regex_filter.md +++ b/docu/docs/modul/regex_filter.md @@ -13,6 +13,8 @@ Folgendes gilt: - 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` From 960f2c48f0adef28f819de79a94dc14a17380c6b Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 18:56:14 +0200 Subject: [PATCH 56/70] some changes on decoder --- boswatch/decoder/decoder.py | 1 - boswatch/decoder/fmsDecoder.py | 2 +- boswatch/decoder/pocsagDecoder.py | 2 +- boswatch/decoder/zveiDecoder.py | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/boswatch/decoder/decoder.py b/boswatch/decoder/decoder.py index 8b60cb0..2ff2714 100644 --- a/boswatch/decoder/decoder.py +++ b/boswatch/decoder/decoder.py @@ -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) diff --git a/boswatch/decoder/fmsDecoder.py b/boswatch/decoder/fmsDecoder.py index 5836b9d..d374b28 100644 --- a/boswatch/decoder/fmsDecoder.py +++ b/boswatch/decoder/fmsDecoder.py @@ -64,7 +64,7 @@ class FmsDecoder: return bwPacket - logging.warning("no valid data") + logging.warning("no valid FMS") return None logging.warning("CRC Error") return None diff --git a/boswatch/decoder/pocsagDecoder.py b/boswatch/decoder/pocsagDecoder.py index d3c674c..ff481b9 100644 --- a/boswatch/decoder/pocsagDecoder.py +++ b/boswatch/decoder/pocsagDecoder.py @@ -58,7 +58,7 @@ class PocsagDecoder: return bwPacket - logging.warning("no valid data") + logging.warning("no valid POCSAG") return None @staticmethod diff --git a/boswatch/decoder/zveiDecoder.py b/boswatch/decoder/zveiDecoder.py index 00f21a3..312435a 100644 --- a/boswatch/decoder/zveiDecoder.py +++ b/boswatch/decoder/zveiDecoder.py @@ -46,7 +46,7 @@ class ZveiDecoder: return bwPacket - logging.warning("no valid data") + logging.warning("no valid ZVEI") return None @staticmethod From e7b3a6335b1788cc4196d351b4ffb805a0ec0c2d Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sat, 26 Oct 2019 18:56:39 +0200 Subject: [PATCH 57/70] remove unnecessary property in repTimer --- boswatch/timer.py | 18 ++++-------------- docu/docs/develop/ModulPlugin.md | 2 +- some_old_stuff.txt | 3 --- 3 files changed, 5 insertions(+), 18 deletions(-) delete mode 100644 some_old_stuff.txt diff --git a/boswatch/timer.py b/boswatch/timer.py index 92d1a62..fe600ea 100644 --- a/boswatch/timer.py +++ b/boswatch/timer.py @@ -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 diff --git a/docu/docs/develop/ModulPlugin.md b/docu/docs/develop/ModulPlugin.md index 1afa1f8..30a85c1 100644 --- a/docu/docs/develop/ModulPlugin.md +++ b/docu/docs/develop/ModulPlugin.md @@ -125,7 +125,7 @@ Das parsen der Wildcars funktioniert komfortabel über die interne Methode `msg 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 Plugin Dokumentation +Sollten Module zusätzliche Wildcards registrieren, findet man Informationen dazu in der jeweiligen Modul Dokumentation --- ## Richtiges Logging diff --git a/some_old_stuff.txt b/some_old_stuff.txt deleted file mode 100644 index 8858604..0000000 --- a/some_old_stuff.txt +++ /dev/null @@ -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 From 42209615abef669ae0832bea790ffc7e3c7ad705 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sun, 27 Oct 2019 20:12:54 +0100 Subject: [PATCH 58/70] refactor regexFilter config --- docu/docs/modul/regex_filter.md | 27 ++++++---------- module/descriptor.py | 55 +++++++++++++++++++++++++++++++++ module/filter/regexFilter.py | 12 +++---- 3 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 module/descriptor.py diff --git a/docu/docs/modul/regex_filter.md b/docu/docs/modul/regex_filter.md index 172901f..f554452 100644 --- a/docu/docs/modul/regex_filter.md +++ b/docu/docs/modul/regex_filter.md @@ -20,12 +20,6 @@ Vereinfacht kann man sagen, dass einzelnen Router ODER-verknüpft und die jeweil ## Konfiguration -|Feld|Beschreibung|Default| -|----|------------|-------| -|filter|Enthält eine Liste der einzelnen Filter|| - -#### `filter:` - |Feld|Beschreibung|Default| |----|------------|-------| |name|Beliebiger Name des Filters|| @@ -43,17 +37,16 @@ Vereinfacht kann man sagen, dass einzelnen Router ODER-verknüpft und die jeweil - type: module res: filter.regexFilter config: - filter: - - 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 + - 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 ``` --- diff --git a/module/descriptor.py b/module/descriptor.py new file mode 100644 index 0000000..a0d7d15 --- /dev/null +++ b/module/descriptor.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""! + ____ ____ ______ __ __ __ _____ + / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / + / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < + / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / +/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ + German BOS Information Script + by Bastian Schroll + +@file: template_module.py +@date: 01.03.2019 +@author: Bastian Schroll +@description: Template Module File +""" +import logging +from module.module import Module + +# ###################### # +# Custom plugin includes # + +# ###################### # + +logging.debug("- %s loaded", __name__) + + +class BoswatchModule(Module): + """!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""" + pass + + def doWork(self, bwPacket): + """!start an run of the module. + + @param bwPacket: A BOSWatch packet instance""" + if bwPacket.get("mode") == "fms": + pass + elif bwPacket.get("mode") == "zvei": + pass + elif bwPacket.get("mode") == "pocsag": + pass + elif bwPacket.get("mode") == "msg": + pass + + return bwPacket + + def onUnload(self): + """!Called by destruction of the plugin""" + pass diff --git a/module/filter/regexFilter.py b/module/filter/regexFilter.py index 7f9556c..b0f74bf 100644 --- a/module/filter/regexFilter.py +++ b/module/filter/regexFilter.py @@ -26,7 +26,7 @@ logging.debug("- %s loaded", __name__) class BoswatchModule(Module): - """!Description of the Module""" + """!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' @@ -39,11 +39,11 @@ class BoswatchModule(Module): """!start an run of the module. @param bwPacket: A BOSWatch packet instance""" - for filter in self.config.get("filter"): + for regexFilter in self.config: checkFailed = False - logging.debug("try filter '%s' with %d check(s)", filter.get("name"), len(filter.get("checks"))) + logging.debug("try filter '%s' with %d check(s)", regexFilter.get("name"), len(regexFilter.get("checks"))) - for check in filter.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): @@ -54,9 +54,9 @@ class BoswatchModule(Module): logging.debug("[+] field '%s' with regex '%s'", check.get("field"), check.get("regex")) if not checkFailed: - logging.debug("[PASSED] filter '%s'", filter.get("name")) + logging.debug("[PASSED] filter '%s'", regexFilter.get("name")) return None # None -> Router will go on with this packet - logging.debug("[FAILED] filter '%s'", filter.get("name")) + logging.debug("[FAILED] filter '%s'", regexFilter.get("name")) return False # False -> Router will stop further processing From 157f6b5c104207efb427a6e5010dbac4a8009c18 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sun, 27 Oct 2019 21:08:23 +0100 Subject: [PATCH 59/70] add descriptor module and docu --- docu/docs/modul/descriptor.md | 65 +++++++++++++++++++++++++++++++++++ docu/mkdocs.yml | 1 + module/descriptor.py | 31 +++++++++-------- 3 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 docu/docs/modul/descriptor.md diff --git a/docu/docs/modul/descriptor.md b/docu/docs/modul/descriptor.md new file mode 100644 index 0000000..30d9b13 --- /dev/null +++ b/docu/docs/modul/descriptor.md @@ -0,0 +1,65 @@ +#
      Descriptor
      +--- + +## Beschreibung +Mit diesem Modul können einem Alarmpaket beliebige Beschreibung in Abhänigkeit der enthaltenen Informationen im Paket 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 + +--- +## Zusätzliche Wildcards + +- Von der Konfiguration abhängig \ No newline at end of file diff --git a/docu/mkdocs.yml b/docu/mkdocs.yml index 1942050..c8432d3 100644 --- a/docu/mkdocs.yml +++ b/docu/mkdocs.yml @@ -20,6 +20,7 @@ nav: - 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 diff --git a/module/descriptor.py b/module/descriptor.py index a0d7d15..35d41e5 100644 --- a/module/descriptor.py +++ b/module/descriptor.py @@ -9,10 +9,10 @@ German BOS Information Script by Bastian Schroll -@file: template_module.py -@date: 01.03.2019 +@file: descriptor.py +@date: 27.10.2019 @author: Bastian Schroll -@description: Template Module File +@description: Module to add descriptions to bwPackets """ import logging from module.module import Module @@ -26,28 +26,31 @@ logging.debug("- %s loaded", __name__) class BoswatchModule(Module): - """!Description of the Module""" + """!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""" - pass + for descriptor in self.config: + if descriptor.get("wildcard"): + self.registerWildcard(descriptor.get("wildcard"), descriptor.get("descrField")) def doWork(self, bwPacket): """!start an run of the module. @param bwPacket: A BOSWatch packet instance""" - if bwPacket.get("mode") == "fms": - pass - elif bwPacket.get("mode") == "zvei": - pass - elif bwPacket.get("mode") == "pocsag": - pass - elif bwPacket.get("mode") == "msg": - pass - + 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")) == str(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): From 0d81c350926a7cea9861e93f43f69cbd0e170732 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sun, 27 Oct 2019 21:53:00 +0100 Subject: [PATCH 60/70] edit docs --- docu/docs/modul/descriptor.md | 3 ++- module/descriptor.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docu/docs/modul/descriptor.md b/docu/docs/modul/descriptor.md index 30d9b13..dd53b5d 100644 --- a/docu/docs/modul/descriptor.md +++ b/docu/docs/modul/descriptor.md @@ -2,7 +2,7 @@ --- ## Beschreibung -Mit diesem Modul können einem Alarmpaket beliebige Beschreibung in Abhänigkeit der enthaltenen Informationen im Paket hinzugefügt werden. +Mit diesem Modul können einem Alarmpaket beliebige Beschreibungen in Abhänigkeit der enthaltenen Informationen hinzugefügt werden. ## Resource `descriptor` @@ -58,6 +58,7 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md) ## 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 diff --git a/module/descriptor.py b/module/descriptor.py index 35d41e5..bbf1d4f 100644 --- a/module/descriptor.py +++ b/module/descriptor.py @@ -34,7 +34,7 @@ class BoswatchModule(Module): def onLoad(self): """!Called by import of the plugin""" for descriptor in self.config: - if descriptor.get("wildcard"): + if descriptor.get("wildcard", default=None): self.registerWildcard(descriptor.get("wildcard"), descriptor.get("descrField")) def doWork(self, bwPacket): From d0876ca40471259cb64b8db5e635617c24e6360e Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Sun, 27 Oct 2019 21:58:06 +0100 Subject: [PATCH 61/70] edit descriptor module --- boswatch/packet.py | 4 ++-- module/descriptor.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/boswatch/packet.py b/boswatch/packet.py index 815d30b..014eed3 100644 --- a/boswatch/packet.py +++ b/boswatch/packet.py @@ -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 diff --git a/module/descriptor.py b/module/descriptor.py index bbf1d4f..7ef926f 100644 --- a/module/descriptor.py +++ b/module/descriptor.py @@ -46,9 +46,9 @@ class BoswatchModule(Module): 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")) == str(bwPacket.get(descriptor.get("scanField"))): - logging.debug("Description '%s' added in packet field '%s'", description.get("add"), - descriptor.get("descrField")) + 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 From 2d365823d84ea7536132712579f2cf05b54ee15a Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Mon, 28 Oct 2019 08:59:54 +0100 Subject: [PATCH 62/70] add routing docu --- docu/docs/img/router.drawio | 1 + docu/docs/img/router.png | Bin 0 -> 48160 bytes docu/docs/information/router.md | 93 ++++++++++++++++++++++++++++++++ docu/mkdocs.yml | 2 +- 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 docu/docs/img/router.drawio create mode 100644 docu/docs/img/router.png create mode 100644 docu/docs/information/router.md diff --git a/docu/docs/img/router.drawio b/docu/docs/img/router.drawio new file mode 100644 index 0000000..dd94468 --- /dev/null +++ b/docu/docs/img/router.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docu/docs/img/router.png b/docu/docs/img/router.png new file mode 100644 index 0000000000000000000000000000000000000000..61af27570b6f81c6ca6ff7d1ba72a595280d44e3 GIT binary patch literal 48160 zcmeFZcT|(j_Am;F3erIYM35q&h_nEq*Az+;dT)k=009z0Ndi(7kzN!mZKfm=`cipwJ!aVcL?Af#To?R!znW5lpr_P?D zqoZRpGK5&r(J|2I=;;42od9}R<72Jp=uRyMLhS;5sW>kmcRC4>!Cx&2MR`wRK%fK& zBB7{Ap-^P8{%DG<7ZG>@UilKSUN|pz>|d`HY}X|0ZXI=08Nw?@TDLRbc6JP7Vttv>8~bL zMd=dIqvz-6<8JHjX5tliG#><(1*rht7flSUkO&Dy1EB5W z;;+4s-Tec+h=jj}DJaS+$b$cR5g6>}{#P&7o$Q4H7Ci##2kbrI--C~OKv00jSTD4P zKiU^~?e%XOB7)4lkU>^fR1+n4n2DdCjkO2vF8~BIfd9YPUtAbR7 znkgxmk+5oBdJ>BIMqbJah5&aI-NAlf1(E_d$j{x*(9;`AGPbsW*%^DOsbTGue2s!U zy@E~M+${nK`Zx$u&&1Q$Ru!0zwzBZFM^bHlK^UY(0L)Ou%P3gSm;{Dfc!KmO`tsJ) zAcSEs!NJeZ#KMncZ9q_jf~GQx#w~DkwdQg`NsI z*gzkzX5vdy_JLXAY>7~un>oQ2;$;qo0(^(rD`N~vcIHS6qMa&C$x6-05I`o_4dAi9 zuZ@B|#YPqEZmg%`VX3TUVibe|y6uq^dp|3Rnys&buNM-pAFPV;al_%vF$6n1q`iXy zh-9a)q>2jgz=jxutuU$p(e(9>wurI?zCg+zARDs1I}!^l?(3#R#URZg4kotNszitq z%E--|><2@^6;$8|Ww^Wo#>S0^GtrZWdK+U6%+%lnxM~Pm)kwil-pE>2-_F>dWQ0=o z0l)?;lMza!5Q;mV6cV5YSGEcwpg|$lY6c>sA@19Mlz;`Gd^P#bwqc}s)^-V17AYN!%`^>FY# zq7XhvjY7nG86nK@dRA_zBO&)T3bq0`uLrTWFtY%`y)hJj7~TzdLBJfH7+~`-lDq>1 zM>XAow6y`$Jh?4q6f9Y6D^>?Ti`=*BLLHJ zWSp9^U9gv(7t8^J3h=ii1i9gTi9~CNrLUTb0bbwGi{jzyM+mUAN8?Fu`bxeIPnX-L=_QWqKCIPwDqCtQLF(Wf*E1GJQd+a@?ZzJ3Jwo)pkfRy zKsb`OCCJPRZx*C#=o11C0#p3SFo5+CQ$=qTdvj`#C531~wS=2lg52FL?7*h8$n6tI|JMHN2_6E8sb zDZ(6Jc!(0w4H>BK?yfFA@qkTpro*V@`%&CdrWPYOl_;4M*rM%Gs`lDG7ex1t#0EKLk;)a=zP zAaE~h9L0m|U<(2%1sQsV*s27<0X=98f(HhH?e#nq3~h{Yz7%uQ02OZlf+9H>M=%VM zC)-+rjUhlspr^h&NJZX(L_sSN?a=P3ZbUtZKPgB>PtnIf4;N?xBHCC%0Id&083u+} z!NGP8pb!&Z3o0g1(b~-1&EEm)PVh7L4>l8O3JD-PP!%Bteu@Ul09L>Uq_2;| z>nmC#kT?X=-4<=@OHj2pRl^%WRP-@s{=pOvVhA(o0lDWKfk0^zCzI}}XKj9_JF zLm`k1Kzc`_qh{-As|XFW!(zQmgUswLFve6=kYb=ONeKlqRQ2#6P~bL-s#p*erw8(P zC*f=jsTiE0FUUmSmWsAkg(+I1ElFmoL}LoVJ&@pzA*xxKB2fN5Fq{e`#M9SZ8A7%* zQ}aYoU|{b+8y^F2yxP&Zv``B$39$`95|3zZWlvI21bD4aHna3r#TtfKfz4pbC@q_(Gv`h46+Y!IIP*U_lo5>qce1d_AaA2gqQV<-4GBc7_QB}iI z5Cqjgq%jolVWEUUg_s+fnt02@FiLvV02O%>5@(0>^7Pl&vsK08)v&e}-sV;qck?3* ze2*XnshHS-0;s-Fc|2Lwz=xoS2dD&tQVhuQR=zM5vXuoXz%!6yhKA^=nHrP*eB}`c z1)K!|iBNDbkw-!xwl;t_26G?*u*f!U9ziDl25KM^Qn0-#)RIcEx1^eQ8ezRnRek+| zVJ61jYE*9!#@dVG8)#r}>S1E&reNTYK!lhpx?>Q=mIhP{c~5{@kN|57WfasBY6Hx& zg&RW+NtRfM1IXJnMBmR0EUyquwYA3>W0b7OY9StWfJ(Q)6GDP*f|VeC%1R0rp2j$= zIb6Y4o@@j{TNv8=!7vyDYrvUMG_!;wpuP?eaF8X@PT$VX+a|yV>tg^X8KEJz_FldQ zaI}$;DoD>z5oTxQ?qF(VZ{cQaYha}cw-2-q_CvcXSosB7LkWRo2*na*>UDHZ-2701 zK2%G01H2g-c+@jL@|2D~M?T5lUe>={F5vm!o-GKP|In15j!uNm2%=|2b)mgx&gOkr zqZ5wfKXD?uo8Bo_CO-e7KI_@UZ;_|sf68$kZ_BBFp-J_DadOeWjaq)20!?baXpZGd z--a;0yT~86XLTWe>IA>l&>i&EtNe3^I)zUNczgqK`}OPb#g(7GLk~K}9m5s}wzJiL zE%z6e1Q@26a&b+3dTex8+uhyWcl+n$q{tn^nZ-wEt3L_d$j;90YzSIfxpML1NPWPM zo_CNMe^jmS7ufmGRr)LTFCGW>lYg2vYldC+E9>j) zrMP@I8#p?S{RhHzT{9+ZLPgyb85t)H_s)B;n!TE8T3RxJ$G&X?A~?18;ZQf=~=+>_h{WW-i(owG^m=I zng+0&?1apq=Sb#Y8zw*GwXXYDF7w1eEVG4=>HL=vEz!E4qQW|YhMl)LUo4>m?yfF% z*>C;Gy|$t)mML`Dca`y9Y&cp#H=C2`UIp0eGj>@#MbkeVKxryUp}mBj0RUal__Jw0 zCr5*_e!?pBzw8s?Jj=)+LA!P9)-~rqh z{%l=n`|aLo-n9h#(ukcpV$<%^OoG43{nuCDG8FubjEua8l1y269`XC~gkLLLa!%@d zjqn5Nl%+-4rZUg{g`n^*e@(xH=mY;JnabY&LK68Qnhzw%g7L=^)fdUyE&BD)Zo z1(V7Nv`FO)$osRQ^QTo{^f5vTJ8mRc7?tzonak_3c~_w-9y*Z3*9@0J4YbE^e}7gA z$JmdY2Q0q-Apg}k-A^;`l+k=xM^2y13^EaazQ>$D%jn#b3{ntM#4>f~YGsj^$I?EA zR2`fYroIu`>($zJQZ(ourYw&Q?zr^qCHQD}eJj(9Xyc(%E0t5+D`W8(SsH$BYFUVS zvNF+F_;HM-i}Yt5d27dIyh3wtzM$PUJFfX)y(=jzW!UA~l}mp%dUZ?}-!HfIA8U{M zHPIBN5j6klZGBwu?_a}rhr6@Da=SAvv=1k`EP^;Ovx=wHx`@$&I>*0PhV3mBwp(%y z<>gtNK6B>m-bTc@TuM$}^#d&}l6te=NRHSo@_&T$I*KVOxgwx8!?nH*Vs={#BzBo+ zy*)`#ka6q%`G_(b>DKnE8QT+-^IDsq45EE^CNb}ml{*f6>n32KujlW^y+j=DP9A(m z)MumXK34%p>w|w*Rj+pASU)}JhhZApcK`JBRjuRpa}fzyJne{Z{97>R%@b_KQ(_(? z88Zp%LL~84YR41U)CuFOB1@h1%tChec?s+N?6N#l&AYJ3U)YJ4_ZBj}%N6ABuc0Vn zH!2Df_{WaX|9x`RML18jln+&O{TjE)@JN#fjo1jd+!38qCm+UIoF??Z`}mA0E6SL!T~^vNFkY_Ne+x@4JF`#5CpDs*IdarqXg%dRuZ`B_2w zFB_x!gE~B1WPx*%&!kCbngh;co47)R z)Y4m+xS!vTSXIuqf=K)<`8sYRv5sOca_Evvfqu=FHg>jZk*)jgJ;*Io$!zl))%2lq zY^Q`U@4j0WgBV#FD8^_aIiF}K5=z)+0gm$mb7e8Gr=Mr$==>4P%G4gWc(?5C|^u|FFF7u>#ir=eXd z-JogRt@vnXS#JjPw&YoPw9axT$1AQAm;OZ!<{Q1P3x*;zKV}ima}19z(EL9{^;^2l zoFzo3GmG@o@*vYu1@q$+IQ6L?qT2vdZSLFw@=y{9%sGP({3(cVxi5Ks1&7>jm){&{wt+FW}N}`>8*0Etb;>K=m+Xq6!lT|XZ@b8v3JJFEr z>*7S2S@odOCA(y6qeM-VuttO`rl;nQSXb}l2dUwX`r`L^vDOtNCmnkG1l$lX7Z24L zL};t0L^x#_#(1`_e%B?M!$b&KD7}#aS~tHSXNN5Ce)Qe>*Y7##Ik$F3HEYFBpi`bL zDEI*qt_h=yn!xCavNMf{(6zAh#jyc{D;1PaoMOCwy_e~F#XhTi+cEm%^qk~+h!3!6 zN>Z_c889*QXrjk;QDI
      &dBjI;Lw*S5ip8vH7M9fN&8%6|5_w%uv}~Q5eEg#D@~k z%aTR8WoQN~CJBI|caloh?dM!JJtPgZd45;-qYB>MJSkXMt3>=dPK~?6qcy|hSoZbn zV)~&=*KzdrS@R?FE>aGx^?q=jpHd4Tk_URP>N>HMB7o-e$hkI=&J(w@5M?}C-6X4BMhyM97FQWXf5VQ4tQVhQ^|Yk- z3{jjn>a|I_?1@Y1RpUNBYc{5`m3O>no+;97e#<%5CN}NPWnp0xi@rrKT(WoPdIdbP z(FYN~76&hE)`-z?(-q>h^`yl4h;MIhoDfC~?at|y=WyiPOfEoI@pUh%XL2LDuh^|Q)x zO32$po($894xg3vZ*D=LVR!%EwWf8rzGk(;8~VV zy~eq#yltQie$^eRiqqaZ0mz>*AWiTOnfxaFfc)t+ELcg>vcCh)Ly|ep!p!XIZNDCB zp%^U#JMxUFv7c4L<3+}JUeJ)em4kz^kOo-^KeWC41ztBnc1-KPdLT)gVmzB;e06}C zsUaplap4eMXUkbi$S)0MX<2!fc%4<`+d2B@%==7Y zA&IeD_C>c1FL3wNY*ao8DJ$Sb>-c@UZocwX;FndDvyUOKyJU7oZ7I4=jE3?a5fE-F zwySizRmpsRsWamZ2pzHd_9WY8+gegMH@OB-ludx=N)UN(s~*H7*y55ByK*6a0X4hu zwP*Q~g4-h^*~v2GFl}(qke=zWMZ7qYT1#Fk66ZDzjEBn@mj7U$aHe%`3i&|~(! zex1TZMUHgE7PXsy28~A5$3#}Jr1nS%hjMpbr$077XzMER>s(OHJ&3jhu02uU1M*=p zDlP+)`kl`{O#4an{*OPZH)Zps-mruntwt#4hEUF)+yXAzADuSLG%LYT^+ z`{yaNrv$#B{Njz=a`H{lNs%?AL5bjP1ZY`=wm{VxW1!zY+HVYZUn}IE2_)M%&j_MO z8jh}LjFIJMj$}&s7$*N3R>gO#f+fG_{o$(S)fd{Cu{fyvCV{!pX-~oTFcR-Bf{U7v zsk-RFlMyRpw|8prx$T1sXO9sUklToC=rQ6lk^>QT4`P!V2YJ8cnkP42Xu}97l1nb{7wyRlOVxP7SnkU{w-6K48AL~1)z|!X-`nfZ(uB?J z<9X04tKZ9=5s+vH37TRsPN1-cka1xa_q$~I<&5FH8}5Ajjs5#wZXdB7?lOyZjQ3^! ztYkfZ;uw<*_A~Etvsn24u3Lranw8C|L~)rTdIgU+#?3z{pu2au?KLDll|P5A?TJ!o zX#P%z$LpoCx|IeY3EHm|+`w`3UY!!~zAt9t{q5v%0#l<1mslymrsa|B9!|hYlFjnR z6~k0!Jr!*Jg>sE@FZ0w6CVdGW!eR3Bx>EZ4aj79lb@fijhs8PfrP}jJg7Mzvw9K+K zzOclz)VpgC{cmlxJdC48Mu~_Ko$=M+H14w*BPG{ZqEd=5%B{Iy)<g;3qK3-Dl9Y@Z^B+mPfT#H-ROriaA}32miOgDE|Hm2!48iuPSl7?Ng3`*A zsHK_NLkq6MKsUpZTENDOGOOvn5nv!k{YHv4bYv9Wd&bVw*98e=V`@CfZW!%Pk-_&kiWXd-l=~gkyyaTHte@VQ`JoOm&!*VdJ zv@Pjm`B$oH-yLSIh_cuUk)N19OXP;IHb{ZC6T!Ro(jYT?$s??D`W08_D}ufrc2Z#u zX~lk%V*9VXdrePx@q$IhiIQ99`-+VVxA$kmRa?uayoX>{hEu;h@LjDgMe~90xn-kN z64sT)PmW`aALfi1mj{Ed<`YJY)tno&V&q3ekI zlV>RFO0jP^_4u-KV>nCO zqs6OjBcV{z{jsBBtlifrF(a=dyyQOBe!{cj+;>-hqr_3R*l~}NX>lNUe21uJV9*2n zNHj^SjpS!UIDd&$1=GatCjVN#{=qd>H&b7WcTJKmdv+~+R0^>F`s=4k1MC}txz;$vwtjm5z_D2-l#R$#%G}uQw zVMl_eD+Ma~nnX~R+yUK-BStk^inJ{f*Olwn*^gA*a{K8{jL>nlxq#8#fMI8ffSk+} zNArVNorAj2@Wyde=;RA`ld)I9Rej+dw?zYI(`-K{$Tb;m{OFas|9ie5<#WvWaL#@RvaRR`r{YHjy74?${L;w(USi zo-ID>ZmKJ}X}Wz0t{#=6fUGeZEZb}^vvI0TYTyt}3++=$>+xq!M7&wvyqc4}5@%L$ z5+KytX@F4afP?tu;iXP<#!Gx{4?f5>YgvV{l-M@{*$&Ierm%(j>131p6|oj)Wxdg# zKQiXVfx2cn?h^H4yE_R3d}oW7Gt5|8dKnLPf8?1SUWo<&={Y~-J5XfaZnsl+oaGGv zpC5eMl}>t_$M0|mn)6AXK-X~yNk6iFemmu2wq}TYQifs-#Ku`GH#shmx{%tDx|f6g z!ItQBMn3d_)gb6stfrLudJz6uW5}z=p*$j3!r@8`HaI>cOj@fW?@|l3dbcKC}(fpxTRfaw01>RCig)B=$yuw}_zRR~f^FfbhW@1XZkE zF#;%57S~=Un1TF6?FHMQ_Q%a{Td0(r>iDxF|DaD{+|%r5XWGWf!Y9ge5cwIOK=x-w z*Pl7o*@*V}w>#R{V)uX)8xH#&s?|uHmcF~{@;byi_|%)>F4N;>1l{^gm~5kfnm;ye zX~(w=y??zTrrT{{!JBf=HRSUr^^1(O&RC0_5uSU=^M(Hnj74bdt7k@O*tu!Xmz^ii zhhDuG9gC>#C*y})B&nfZso_Reus`dMMn{ZXJh#9 zJ?SF9V%Fih(x6TDhy5kYOpP{kt4;!(qy zZq_*@ixCxbz(g0ts@HDsPBpBx3zptXi7f-yeW~@$GWYI@a+r`W8G6t)O0hAO96zXY z`uzD-@CH7ExJ!}z?r(2WLTe0}7BLVn&(ffj6AQoZEbocX!o{JDvky=co zBg?t{%wLENCv`o}s``qXRnFJhjh?1e^{RCI21)>lg$((p;nlpOd${gQfrd{1!9r$q zwz>!nU41qX2#Ivzz*<4orvl>@fuLVyh*#FL z6tsW$`InoJp8Fed3I|+m4o*OxQs`#v#)9GjSIu&EclxiXyB<{5ywLTyzVy(XdKh*z z_|>|Rvfeq@g>4`V+!jQ4oCKmU*`qALyU+ZEM~$P1`MU|>x~XKe@^ODdz2O)6K1-m?)m7-Q5EIQSs<-KJ;}** z21v}XuH8?$`q2zxBRefFdY<}?e%gmhLSSu9f6y-!q1n?3IWy3Ua2f#^iF?_w8e@P4 zNIsb=z~!nAWZxIZYD(84vb$RRdKMwRC1Bvu# zKo`L$8Y$o&xSX_f$W9>M&`*71>3lS9oNi`Hp6ERCvrY)wBMm>O#k$4J18sMc))N zH9E6hYlzC3WfM@0!u96nl%YLp_BR*qzKGTmYDqr3#VSOf{;xF2UwFRpI4bV|BrxAXe9%ek^Dn z^HV4PaLaF|gR8{83g<3%hpBmEmxcH7fAiv*qMR4eaxyaG8w-VbDpN@*JM&3&!($sU zrb%%v9zgafuwB68bo%}K!aTRuuCo4})xNG9`o9#bw9lhsQ(Txje!S`$-77k`Q13IU z*s#CSI63+<;74zY_$ocaf2SHAP37sky;R<~^5_-|-C-0(hHPioTA$JJ_4V~@&hag?|3Qk5lj+=zH_RF-73Kvn zSJVUfN58(ky?N}sym}%Z!@-$R(sX;6wUbk*@hezzA8>~My_C70$Yr1gHFoCAnerS( zcVqF}TSfJ$;nP4s9kS`EEf5XPBn+4ucrr5nEq8SE*BDevMwKCe#02=>v>OZvj#B;-kn|zF`+)F2PXSJv)Y+raDPz7k< zxD^9TK7A|w@`Bg;yw^K9=vb|J~zsm6Xt{R~YwGYAZrvl=La_YU!M{ksBuLsjf{ zP)qpfO);^CM@Dxg!#X=V$5tkroAt!*bR-q%$j2~rg{C5Ul6#?Gj0gh1N6H(8up);M`unz{3`rVmlG5{l+J?YaJ_o# zC;|O9Rpt`_HQZFI(9asB<+K%9`;VD^N2=eO=@>6vi}1zq`G20OQ8-`;_Eikw$2hkF zKj}OE10aMB(8hiOT>q!Q8Dw8d`};1y|7=YEf9(Fp2+jWsE1tY%63c`@wz>Oc#JHov zE^=uhAecDMhOY+K|67zjI#vY0&7s`P{WClyo<9Mx(u16gEsBlZ;xwn{nicweE$yN440)(P}S6Lk3juWEP_pY1+r z1gW`T&B0~PBc@Mwj@Z{T_y@pxeV~NI8+yZ8#$)u3GY2a<@E^MF2JOi1g$b(m>Gr%3 zZ(LzisQ#xRfB?92q=>3@^%Syz~zwoB&)`PBrPu&B^TFg$$gpS-LTm z-1E26praSLZO!-sHUe%MD}*P#ibSWu*(t_WHm7mC#{-4c82$Y>j03y}oG6d;eQ;kc zpOH%$r&D-%k4dEIjotx-Ztb0E;IH(go&4scma<3Ip&w46|G3Q}O@9n^{2Z+h{;_u} z$-&%3dne>UjtP9?a;Hgg^^W6MO^~nA?49-Wu|P+^s^k!Tsh?cBp=-jHhGp?|QzoHW z3~LfiwH){vn>7i)mYyD$PY;88c}$xd!p&~Nb=F|t8hc$XUi#9=`&jD);rb5uF>!Ad zNE=@_>r{bytekcvOW#wbjx`ChmY$S5__<-nk-cnsktM$#U9O$Q-@6hqUE9A5f)NiZ zPL2sonLP9>05Q|IrO!7Wn=;w-En8xOhBDu)h^Sn=Z;f!4C97(^MhJ=2~ zK)Tfr6g(O0C26G`!k4vvuY2rP3x$fs0<##C&ZdfrHbu3W$qqn5N%X?9LAS2Q$^jQl zlDhUUpnis?Tz*GV;Osbkqj%`=&e)cn&yS6a`1e8fFuFB~t`o7r+p%(eWV*I=&o8#y zeWcJzaXDtOFDl9JnPVudyl3~W)19$8uRCaXVJZRE9{Wfd7_Fi{Wm3Idx<3yDEOnfl z8>i`IigH_ubL3@;#xgG0oDywIGE)jnS-IEEgc(ErqH$yo6)={lp&wp~m8FC%8JDJigbet9CNf&ppFjz&PXvk0lU7r%=kpA~n=e~hzs8b(j+$&E zADD0?2|}RnmHe(a&Nq{1c>===x5{T1E&>}&wlOj(G&7 z2`vhawW_%*%|FNuzMmkeIaaDgq5YqlCW@=)fTa&#VX4LyQUoDMpGqn&>?#x}`biIeC0|<7Tez-4fm>ubV`7-;{m%VB> z98nVrR$FX2+~m@-yNC)95PsB!yFR-BZ{p%qoRt}VqY)z2g|~TiNSWR%`kJ#|U-@us zpTy)qlg!5?FWSrUYy)TSUiR1BTuxTeFy2;v*9I4A@|`kHuz>%RXuP{ZtlYwWO|-2@ zli_TmGGd1#z(VeqHZPJ(kQ@lk0w8mMSxBqlJ64Ex7>L(gdQqJP|4 z(H(s&Euj!}3NILxpH+8o`=e7_c$?cLkCAR+ctP}pfk$k45jbvMj4g)pAlBt;{i|MO zb152bBu2EU;w!1+`gi8s()sHU6f%CUnYxWHUXU;lVuKQtJsjty?CV>}vFl3!n7 zPbssOA3)kj3;fu^&v}i}j^CG1zyZOWAF|P*>)oW|1Y=Q1WQ7KKS{1yhB#dR6krk7U z_m0-0tx1&pTB1g#fA=X55tvGll7A}K7GDu_t9do{-H8G3^2M{GT8d=sct7*$lP$7} zV{QyZH8Z%jk>m;%`01L5_-HL&G#?M@H8UOBRHx|ssRF{dEN+LHXZQh1tzk`RNSTI? zms{k4=m=fNdST8zeKxF(Z0ePl~ETnOa$r$Vk9UxpL8u2wnc&gu%+> z@NB_6fs(H-$6HoJbIAZEPZY@S)k~V2>;2TbBSJ5&RVGgb9?uvk&7dmCRQY5NUXuR@ z>Jl$V(l2>*V`c6puf3CA16=e8y6^tjSUFkxkDKci>wIa|@U*f7DJuC_Q7-D?MY*C3 z)M+Qaa||8VU$&3+Q@=R~Lg}UwhGX?z)B&^n_~v~t$0L&J>soQ?Mr)Pae^-wqpUo`2V*C=zfd(UBx^6 z(|cmAr{&c_OOabV$tzwL8hk12*IUe!Lf1ndEu=N;O$P_LqF6b!-y_e(*qy03XD(OG z8(jMN(MLwPX<;+Lx~>dAww`YyDEvhhPj%DG6kqM%Vynh%Ou7&QMlI95%1e3{yj4Z!4t1S9k~y;>6^YUEvhrlWYX$Mjsz z5}yhlTI7`y@4WzGFW;5Ea(JKY>+EO|{P-MpLa^Vn8Q2dM<7sG=1jvq@aOBPuOBv@` z6~?kD6B{Oto9|Ped(ZH)mTLOfTjsxDXf$h`GtXSgx|?O2=~yt=AkOO!GCwh$QK?^f z1P4cCdZ7z=j(o#_(|6p4iA|6JaP+*Rw0!8I5tE-7n{IzUO9}dJOWMAk_nt%N`uUw3 zbO)UrR;^Mz;aAfxR#Zw{XUVI==&==cw>(sy(&I0iX--}}_D3NZ!?9Yb>hOB+!S|EE z<*iI+za6^_jf%G|&lzqyEG2(C>5wH=-TC&pzPGw5Z_N4Y71tKc8dz?&E)gF2EuL&X zRXFi#=xq2m<^0<*Qmw1>H|VBVn68!Kj-AuCnPc~;M z5b5gvg&|~I zpkvMP8E>ydv*-Ys*+Inh*7u#`H`vaE^YRZkP#ASSjcGozW)J6e=npPYJph6U%b3s3 z{O4RKEVnFmcC1yG=W2#lk3HwQ1>ABAKMqjz@hbg51w0^9WEI+9J}S(6ApPacr-F!` z8^Yw+%rmQ{MfOTnYi~GiqkBIF==%@;Cyu&gsqim@T?tXrx`4TnxhHmAx8%9+!2s(~ zARy`}0(j%5uDYQlPYQNKh~Y_aRS2XyD^@yoo{o!`t4~-=S|tBfLB!AgP*yY32*9gc zB#P@OK|1Dm1RpRHR9@T7#+4;GzB}pHp0HQu(1q!~SBKdXNHxhdNdR=K*hZ z?@Naq8<*>W{u%!}=ok)Q#ww6MJ}GkRG4tdT;987BYEw*X5i^%)qcFEPzFd;2aqh&r zxmFwB`M0+`j{&~^jyl6s!m%$mD>UdXL69?zR@h+!Z2j$`_~XhKqSuX(ZG3sxZ`pB@ zcj!c$MpS_dHPyJd(DZm(NVQLCt0&+$SLkmA>V|$0-XhHltVyuj)C!9q$qEfeH~D0} z+*_OXM9oqqsQLM^_Q=J{uO4wG?{J?2-Lf!hOE=5xwtT|nYVjtHwXv}2=`Hytld#m- zy#lf`%Qc7*vrLis3mguk7y5w}?Bt#ToHi|(Iq}90_sJG*?4G06y7v$E(xM`o73uK{ z`9+tcwI?u<=P-hoi_1#G`pq ze>P=uL6IgtR~D?U^BmUeTIX%v{Ej)Ph;SqH1$qQ=JnXgZ)d+hv-`%DMhm~j(qu_UU z(w}my_*e1yV`Rnevq(>we0=>CyAvzdw8J>XvXdZ}%&YPUIKP`wIeh&%q~PX33d`6K zN0#|RqCC^V&o6=|waX(cpI3xeMy>!Tt>j29a&x+BR>@B#c$n#%S#jQ;HYsA((0N+! zSU1;l*sR5e)sp_Y(65>sy7oHt_ddtAL2%;b>zEoqb=0q?**PZy>hR@1)ZrsZn)r`$ z`@xs3M(MR$yCX%TC#To=ZzQ=A3v13tZ0uF*Im>R?1!lLs!|Opq==zy{xc!MGm``0hLzJ`h zIPaPSNGfjH8X&G6;6?NBi(bGp#qMz9TePX|S#hGnl(@CX)4ViRSA@X{yCk!PSw%u&acOUf5|>j4wpZoNeP23#(q zYvX&)l~k(<*zzst-@`e{NSXcRVb@QeH1_*`GhxmxrsJ~2lG!gUjcTRcZ zBE$6L@ImF6AV`&eaI@m%%_?8H7p(!93=x*ktStnV0^egY zMYWux%5Ng^F}I_e#hVy0rvY45#R11co=P7p_x37#i!f_X?du|Toz2`eCC?qZy<#dj ziJ!{zEI7cuhP81}IUlq;Cd)@D2(KGQ0vDp7Vy&{}KUYgpAIg4r_3D_2pPa~kg$7jU zP$0?s`f=r`Gw@L6c25ynIq8Uke`6b^#h>kajf*{E{ZgTzugTV#{>@;PB5JQ}fz{KE zTN9qkj+c#YFP*RTMc)6^(E|Oz8UYR-X9`(acwh31ttA;*Sj+A;0mN=Bt1p5s0sHN^ zS<%=o0MH=nEsJg_^N9e?t*d^u%g`QjUf)Xk=112#!62HT_MuKl=vSq)ig?ZLTy<@u zk-P`siJ~ifGyC7(zPO=~gR%Auv|BYxJ?32GcT?U$e&MJ`>L-zt(;nmBH-AbM#c4u`$D)dH9vNxuz z2!_XqTHYod*1ame_h;_&nc$nn4GX6w6l!sLHy4?%>ajfnq7+7q5-0ipi7y@OI({Sk z4aA+CdxjT0hkKUW7@Od|Ut!FwJX>5C$-d1G7&ft=%4l2fA@f#(cSAkE>$Ca^N1XoO zLk!)7WGc)aVt=c7eE1TyW1U*DQJK7OzimLa=&nqLxrtk*l2(t%;Q^PR)4NN`BDC74 z366jyiB_uugogo_T$4b6d&+c^4=^RHb0W02-+aC#IOM)qPvc8WJwuYzVBr)XOVV6? z)U8b)-brIb*D2dO2_9{AmdiC#;Vj^~W^=hZ8SuXXvLaXIflBZRk1sA(F?>-@TD&mr zNQE4XP{z)r-x6Iz!~k7?1KH-b>4WcV&&N+hO$=d4zDH7a|9>lGeF>5PS%Z9;WQxi~ zqplZ;0g>3)i=&fr^Z@wo)?IE+PmhKy^V^v@8!*wELjhN(#{CGTe?3I~%OeNMscV2G zJN$O4m`fKh*fm`LM+Zz`x|$9Ss@iuK7GqKnGaLQh!?LUO@E=oAgsFE%sPdVrv0Vw& zfns5FQhc^kevlxr-R_&m2kE2EH*HfIR!UeB?>oz`*vuxi`6u#+^it0%g_a$Q8_bH; z&hGJ8<5sdxooE!EDZsw$r3{z%*m;!Rh{t>Y zRPWp$$>c&_e=F9R$=$kTo$#O1DDfujOy@c@R!)VpL|e8uVs}E?&81HLC2|bL6;?#F ztmD47m?ImxA4}W9u9cHl|+KaD9Jo^>?!%sUA=P(>W@65qm~8CIeKo zitJ@sk0G1dq*&5);m1c+y}yyL>0{_#CY7axGN#7SmSxg#ZUPT`N91Uq-iTI=Y9d4B zL-mSG#K`O|OJ|=TG1_8FdG51UYj;x;vpB)(j(SpZMdPqnq)}#cI-l9EiPP&0KD=2_ zK{P0+k9>6n%X3szQS?pNNtHyW|0u8?HyV}>=OioJ7jthRxZ_dn5>m%@V7f_MC9Dd6 zENd)@(H1qlTU-zPBQhk3zzr}v%_J2ZN=okY`b;Vx=OF#StsHAJKJL;b_xuT3+x^Y) z{uF@(THF8w(lDAHgfCP?b+UebxThV1!MWMCT3n`x zUF04giXc1ZRFa$dk4n3j@IYc};r*YwWSmy*?VUMDHSe0qW|u;L zcci+<&O0@BCr0E)z_l~FTjAIZO|!J?7U;{-q&(mWZTK#p3ZmgpjN&Kij8}@s4U^%2 z_6PxKos1-&@}|BC8BExbJ~yDwzJpr3J>uuv-Wa>wmAc`Q6fgzTAA0^J%~mRJz+UUE zWt{4UjJ;0fi3jfr5qE)G3BobR(nd(B?RMvcBT}pAlI3Ge~C!^T|aOs`CVx%r)i=?e;P+3yRe= zH>l}tT{RP}rIhKrqxe$RqzAU+voj>|#4O@3@wqDd6QeRu?XH~3;N0@a%{J9!-@Qwa zF~i`1+f#hV71w01gL%gb(gUe{1n*ylUlIp2Afn<`H%`>g31cR0ya9)la4VKcI zg}bmZmix;@97el5>TS5Q*VUVPVX)*JDy-o#IY;`&d*64j0u()f8;iq}A>fIPygHl9 z&XvaJGSlcX7G|R=>Xue7-~7=rd!0d0G~h?_dA879%;0^J&hvm_EOVkApJJFXbIXc) zlNB%UJ1qD28Jg3kN|ZSR>lK&55UXG1W-HJjel^E79>a2aYv|5dELE0%lHZn@!^&!`pR%brCk53b94FOsm!u}9I@};jaOya%G~TS z|H!1;3%$dY3Ub3KKqZFT-2$qUzpnh60`4!{!yMY01Kn<9?wVS2Ty~4E$9;9Z-`7$o z44lqgwuw&>*SW6k0Ka;x>{KueTNLGCM_btcL8P(*7nvmCivab!IJZO#+WTq6({bD3 zOsT=G&yFS1>ZzF*nB4C^eJ?RgQhOxt_VGfdc5vApR}1l*@Xg|kQ#k@^6pKZRSJx<=>pB^EakZ!|?w4WlCyxmC z*mUGQG8f6y&RU%M`iw=}RI#k=y1s8vOwm{RPDs`tUlsHW9O^F^{Fc|h?XlHtXPfT0 zOp&iJwFX9kK!ImI5ylp>n%iD#sf|5kdJ&ffOxagnY_&!?HT2U$)%xN*jF6C)*G zAD1o`gOA%Z1J}zeuY|rp6}#QAx6u%4Ih@JoZJ#DcvirwZ)TIXWUJcGWMw6mzdDO)5WQYGVMpF{CVy` zyX`%2{QR9C#SX29TZ_X8j()>mR<5klzzxw~R=R$omB9ltEV6qlU7n8buBaIh)0x5I zG}MbZR4(MP{|U~m>ZkEx7%3pdI4i0DVy}LvOw#h+6Faq%G|Q2R?_*qBW56*85Ha&j#ndibPs#2(3{W9(*erIw8OwLt-FXWw9$-Y=_W#2G8#r*s?E zGJXdUyKQNKCT?xrRn)}g3s37u1M#qtW#OOd_SvsQXbdKY%_5WewJvRNkn_hTz$+3u zKYnU8@UX$U{pS)WPHr)qOCS&&abhmC}Lpr2Hq!AI2JP1e}X%LX^1_9~rhIbC?^Lu{Z|Np(~UCZTid7pdk znLT^&nYs4f*I0esRdhCb*P#YWjX(Olz)hqa%*`{}?=7*Co$DF~v80*3`oi?$o+?`CudxZJVGj+{=^HNlxgUrX%8(e>GB@^Yx*7B34xtVxCVA3k zZpT6EYR`q4?t5doGKzk^McY(bsw}QB{T;~KfYO%{U;z;vipvbQslBaS;v%ofE?}rI zE@~l&WX8YFzw1Q5CZh=JS9C_($At!OA8QCth zZFG(YW?yIWHjb2XfiF&)$}i2`A0>$Lx(hHu`YJh@0(-&3dbxe7sXCx}iK-T3ay^i; ze5a6jc3aQA((x}+mgiCAMzbe}uDa*Dd?hO6LyNN|FYXi_e#xoY0^!0l+B)O~3uCoK zlQk}Z>nTh&0V=!e-Sf!Vm*4;uMEK2Haw$w!6hp#m7f=6j`DFEG&^tSm)KCt93P|PD z4}Y|qPR2aHKAJi+@WUYOm-*R2Jj*&>f9+5|PS37SHLZ3g%B6x{iZkJnPdy)jLlmAT z-$JZ%XMOeJP@0}P2*?Y~u0=Wme!lPFCFQi}9_&u$T{)#DE=RG7ejo3_lX~s#Y0~kP z($g?mw5qe)pd<1##WeA=UN9eFpL26LU$i7XYEYOS+CN=SG8aXc^x8!qLkmk-P+S4I za+{~_JRVmykccxjWlL#*zs6LW^WQB#o!Il{yfkTk7q=HqPqR)spQoP#tS-XmTONQ< z7sS>G=YOh0f(J7qA*GAa zqOW|!!QlouZTyjR?{x=m%Ua@UUFKPiA%Jk{BwQ3-`>JP)v#xD?Xg6(!BJl;PbClo4 z=Vg1ePOfO_&1M}d*F+wT+9+IweKRA(v#Qsus<=8 z*cWP~NS#H$hd@LyY&ld4i5RIDHj`=2X)B*9T|6(!sNdgB(DFXN%t_6N9&15G%YKcp zWG`qbQ^|JG&;$x_`kg)ay&i2QZ>p_sY1OPJ+Ma*fFpN3m_VFIcAitg;GhF_)G?FL$ zG->Se@<{3W%**MTlG^(!oo4gNNXpe-xs$J>ZUJ+IvZaT$UDQ;nwAo@TH>!HVI%?`^!IJj{H2NhgmPSfXEC zk4C5P*^W84ULSYY*A00zs4DXzKlF7elXpGv2Vb>l+y64eN+GJ)Aoor0&sU>emeUW7 zQ@E8kW8+iw)HU8d_T1gds5|poNc28!8?jZ<8G_~+*(4PzB46+1sMLEcj&2v#>#ie8 z2~6WQTnu;9jrXJafcz55-p;2b4kr%~sdw#Mj#;M<2PZ@fti}tQS3w(7P@mT$)lWxK z6L>KO2*Q4(O}g1BGu+EvbyU(-Rqm-*Uzd~9)X_;A(|fwN_&qA+%%IwRvP~C<p(QCbZ%2nbBIT-_407?AOSkUb8-Pw>Q{y6|vX zt?P_%+YmPwxvM$y^l_IVTQ`2yQ@BhT(%4fdo94>Ge4Nimrc0bhIUc1qX4gU=$Cv32 zv+L_V`)FQ3K{rV6M<5-RhT2&-{nBB!;*;3rL70c8r{(QCGA`l>`&y{1riQ2|0H{8y zeJGCga|bbi@?)_xd>%2Q3P=KLmD9?U3nzC@nJU)G88DlAcsOC$3EnUl-`$E&sMw%% z;CquUOZaKQCmkd$44@BGRfA?_Qv6=L{n2)_e}>BazHs#OaAxacthJ&%!?KYMkWyRy z`K}5Be2mm*XOp$(>|zd<916ygMrLdYEf>QB5BynCfZ%YZRs%L1knxo0vOL2qNBYyA zU!ZyZO~>+x!T3-jPnYRQGYsY{Euh1r)}t-e(`z$;nS7Wf&-H2KFaqEw$vROT&!NVC zcQ{*VkHed0ZEwX!c=L%mR+OuGT;stS=~dk)G$G>V)jA?{6H3xv&drE)EOc2S($B=D z-Q})l63r<4y-)D^M5tDP;!!;}9U*^Y*mXMtO##hgM?G zCKehDOo8oxO^4gQ6>)t9lCNXv!~a`o0yuuS!-Iu9R8IX$F=JVfiC($ES3$V_H5t1#F)@&-813#SBujD_SemvG=uv;*oQ3PaMb@L-Zjpt@FcQ zc644fqt>5|&UkyDRvJFatIV_Oo+uhlWF`HUjvsPeQF)S}=sV$3F>)*TWeu(0X+eri zz#!^HE8++^Q;B1!5uCCUjvwNZvNPnb)n%>$S(zK?7F^wDew24@-8|vK?`a@)uhZ1q z@YG)k_Tz~36Z?foV$UP?yvlC|K56+bTVXA+^RLhOwkKfQd+8wxbp!u$WzaLf`mvu_ zO;m+aXVWS0m`cs8Wt&PJSn9XVtTs8(vr-u=tkWh6jpD^d3n0s*K*@OSeDPUn$Pqcm zIpB!rjel9|DN$?EBa&m6#=Jg!mQ>D+A4Y$UO?A?Cl;L_*f7bVb!|91(SvC){Dmx6= zuQ~Dci(~97q^5LR>O0?j>l7Cuqh2g`tVeH!hz(Hw9^z3Ct^BzoR?4@hC7E{nU-qO@ z>SqnT+&^_NJ!RUg#v&`{|6uo#5aqN&cZ+SZ-lbPX$kVUT#@-wP*TG?~##NN7I3yd> zvDZ#5nI7}7A?C0=G3(LS0m8hOD{vVa-lG7HXuo2u5}=@BF23&LuO{k!Uk{T%Nh(*L zuGz%c$E4!-%Jdr20P>OrUc!7t4R{I4BK$(-pB#=fH+vcSde<#zc=!)xLOB-9*N2=3 zIpj$0JTDdZ_g;=BP#2~Oc(w#}x(sq;pMh_l<;?yN74iHvoG(;3@8F*MBOIe8F-8!o zWxc$#n6Nb5WvJz7dEI3fVFUko^tVXn-%klZ<}>J8c(csg-;s8w#(J;^AColx)G!QD$+SLKFbxK!d< zaghLyt{innZ|w*TB~`|f3i)g%GM9bz`@Z^+VBJc)l>6yBJEQ%@w#^{M^;i|YX)mVc z$idPrUvXcmsKqm!I#*#}yl`5Li>uZ9>Q@e{EMDFptyQ>E0HJ2$*?aPdH^HXF#-SoT zC!_1x^oZ6+80o7EB>xp`ig7Vi6&a4ER$nIO)gH6j!EGVdvLm>R80}7iJT<7Pr2b)| zKRyAr)C%zk?6O|y2){y|Y3D%{>%6_%M$=`>Vc?UkiYPCgdE>(i$>$5BlUAE;v)@E* zRTDce*644)ylLM4P-(Qpdbnc!Q(NUW?yW)NP&?ccqsz8~&9&hYH^|O|dBr7h4?$mU zE&BJY0NHL7z^J{6GhG>$%}ls;YAoN)G8e&MU)?6D%DMcUILmRl{egIo-PwyvO8gjQ zSp8Ddz-_Zm`4%cM_#L-J*r0~KQ;pqB>VRD@n~Yb-)J}TOY}YW_k^+Fq9o|TC(A5Pd zL&0&}+_?P^lJy)Xk`@8kTnrYxbf!kHPfg;vCa6tDeIhB=>E z%1eWA&UC|{(;(P!-*ISZc@4EO?z6AI3J9JVj~2Z?AQ0S)EVCRj!TdbjVS8s&)N6a& zd0K;*Y1TI^O$O{SWkLR2Vwv``RP4|qZ20&XfSpe zL9W;=vK1g;nQu^R+A4D|C%|bc4txBpN%Vh)(ms9o3@D*$WbZt#``Xz$oj1$oHrSx& z8QTtxSQom;SLd?1-rMgTXqVt)wkL@(8f}p{P?Ne*kt}GYuGxo~%U2?h3)rOPlpoBh zi?ULy6RL7GxbVp6ivD0JBO`NPuw2EwT{qQ1Scy^~0KLv6M!;=@#KU>_>6z(Z&aZ$Tohqm=m~g$mKNBlWTh|^4@e$N=UBQh?=1rTK+)+7O zZ&uH9{1~i(c3wQ6vGUzhZm@c@=gD`1*hjdWaq4K4*ULBp}Ak-CaF z!{Sx5<1*ZaCNo^=%t*pi41+Xfb29FuPPJnp?Pt5a*Mn_Ia4*tV?n@egVYD@GkfkF&S;LC+xQ9iDN`B*oJ&56fI_8R8&rWgV2M5aAG5rwswvt4JOfaWXxT)l89it0tm z!ZkE)vb&x5e$osTCnv#Z+u_V=cs?wYSksJ`uDQqKp@i7($wD00ufp11^Ft+JG?;x~ z4zo7X#&pSs%;mX(GC^)nt_q(QNC2LE@;W%`)9y!9@<8vO(DRkLIX4nYG_IcOcYFzZ zv-%Ts_764q(y|zsM94o>`1{=)>1B*0lw?4@qX^~~Te&eqFB6vvrF>(F3i^%DKVCEm z_rlet_djZrO$R7(s?M@|iy3m13f>J{&&VW2#e;f$gomtUyW@p*VL-rG69>qhzubR9 zIh|*Bgzm@6_Z6p2hM{}GF-3e5kRtH?N!Dt78=3Ts4d5^<{1_}EBJ@48;%|RkJmLZ% z!QO*QY#P>pFsh)}6@*Oj{!4d7G>WPSn%J4q#mBlABBqmCbyYI+Qk&Tcj^;ix4@BNS zW?t_VP@E}lE{O|Fi*|WZ$&lkJk!IQvsf$0Dz3O;-cR70OCyVlDfTIZ*hp2oT*=<;7 zMgVST8sO`C>|f076pUkxaAsR@Y-`H_J8 z+rkGaDjuhUuNPKT6cYsnNAP$y$UJ~9NZ^j?;E#w&Kh6e)&PU%(L{Tl<-R#iWb zX#dj5!4lTw-j>vZk9+haR?%SjkCEXTZXgorxb}0gY`=g~yB`w4n-8{;gqLaA3XI9} zDQif=!yKSqfH0WL>RnicLJd5tTiwWX9Qf`xYnbsT=ebNObfWPB7}UUEH4pwwy-_F! z5QdNh8Ia>3LTBMBNG7rQ7W-E0kK=mA*352EE<>ajW9)cYUac~yF{-b^vT9KsK;c%G zN3P`lJiC9i&-43W=A22CZ;IKNS?!Q$km&rP{94sB_xYmyR3mEs4N;IfL0P`kozT%a zl=xs52Smi*dC}2`FJMGTK& z^uAFd`j-{e7_WrqC0>g`zmx(kZF1rh123m53amgLkyc~CZ0ZktTS8KI?znBFt4MYf zctHJ>?zfsu2C$GT_^h@Eo<=jwivF10Z(=19HW=gey%ss7_VoP{H7?0&vDE!#&zxcQ z33c69;NXEZt*0mh(Vc52&zwXTnXDYTjWX7cTza{I0&|P< z_CTkv+KfF)$y64mi4n&eSs&bv=y&->Juu@q+6!q$T|tToEZ}(o*RMM#p`7uSXTM4{ zzFcJ0t~9SPNt$k|1BF29>D%Q$ZB{|#_w1>6!m6#pGfGnt{1mkd6Ar#(<53$9AGlkI+ zkv-gj{c`bMai^h#VvE)uPQ@qYOZm!E)FCLuHG< zIXxWeWPaWwTDy95PA&hgo}Y0;lhg$dq#N)gqrA5GmPGQ>JF$U>i!}`I6LXeAYuW;lK9siZ9Q9mA&X*a=Z*uPZ@N0*{WOe_FUl-5yDxLatPws800}+I2V{Mgl z&C%A}jBCwpZRu&*nc5H(mi}6e2XW#sfqYljw$ zJF4;0^EQKn*dt6(Y)y&;HWm{dG3YQ1Pr(%BQ{BTy#FeD3rD1OmPwYDRhI!M%2d&5` z2T4d>qn08SHJsr`XbzzHpc*7a_|&m{Oq-Dk@HX9dAe!1ZxSh}!QTOXBsFFw?&dR?c z2_7UXT*7_h;!Fw&1$qu&uf&0a8&t|QPWL@-j@J0055kAxFTP#8RcWxRSRIOl`hKxe zC$#y%@ZDm$TiS~kU1*(^I^)%OLo8q!j2kI^^FXTjzE*Vb-N3Z#jMdUNUPb!v7I>T4 z?O77c3MzKzgAquNo(Str+Wf1d%dM)6D{He}MKv9YY$0loVWg!N0@i7;KFmPKq zG0;iKZL?2uplfixAaXmH2_-4<;p6mB$_EzTMLt@dQ0FF7t=}7kc2*pu##Ei8Iz=*v ze^xl$P;LbtR<*hKqef=DJz6Hicb@W_I2!R+l;I+m?jHtIKc3mso!EwH4d%3VQAr>cwpD4+We+>7n(7sp%f4mjl;S z9!9m$XvT=Mn}Fu*kM-i|QLCRnn=YD~9!OF>8FgLGL;Wc8%JV)4+b5U(rMIr0b|B+% z$sDYl=EbrPwZ7l4qt!wa-(Clww3nkC?$PALSXr8il;!(jem9ED>F4Y&-rs< zDthSd`eymjg1tcoZ(gSvRm7Wk{-Om$cl}$ivz&FUinWQufrUx8(DJCo(P$ILuC5`m z!t3{SklM4uV$5^^LEMq7Q#d|}J8geb9I5E#h><)FqjL65iXAMiwq?G-Q({)_5A*)y z1y|PTi92|giQJJ;t&-{WkBj_PiZEpJ?E=2-ZY ztsj5K8Wow&l!-{UFqkCYWmHa2|E_&q?fkf7hE30)=2y}(uHisKclKw1Jf#U<8G)&); zLNqAYx1Z^#`Jc=uDF&&l+8G#F8FXK`5QZG}N|Q!f@E+J+dE|Uh3BWbFrV?S)Muzj zg7g5oE^`SO#Dd2wQY)?c2q+A0(MzkM+{DJ{R`5yiad++@b{Y0&-l?F02S@TK`5}vQ zJF9fTN6NT;@L)?XNG8683`l#I?Dg3UU8dj>8SQRley_qV%CYA+fiTQ2)fF_fKwQMG zSzNMi)C73JhiFwo9o^i{*#uKp@=e5%*nCpTjpipG())TJdJ61QcsmZS^5t}!=0uS2 zjq_NG?&xe>9cS)EXMwQo9&#C=x~35ix3VV^Hhc3;8_ zbQ*?Z_o+ml|6AQx^fL3`k(lwLK1wve=Cf2Xixwg!3U7xZeJ%E#!>ee&!1pX@EASX7 zsvFV`uTGcYD^z*}c-hFo6A}sO_|O#tx@1Oy3spLg_o3hM`w!&l{Tflg!QB6u+hqV0 z?+gLSNCter@SRgG-${hj#ZHcTh`7~EAr;;xQx*oDpt!V*ba_Z9M(DlmmD%%0)VGEP zsS$q%K4zQxaM4V0fTT8zGP=_n4Jon!ZibtO`zS?Hx;gBg;Xp-ds5x%`3hoLH;h`Kr zDK*?K4=Nu%LPi>w0^jr;vw&Ib5Tg=-BM!n-`Q#nQnlfEJd6l39B=2{jM(7=4d=0Ssav9}dFb_@^r(uQv6vfq6 z5XHBY69o;%TelWQw+CVQvYsxEL5mO1Y5+pB_#?F0SDvUR|7$_Vh_?iYQyE6xniHq_ z1=6pmQ!)|HN>EC7c$vR9IYroO|6A=V@1f(kvYynB=MzMFUKsiU< z?y#tc^?$0SvK~H|K^M7{pkd}Kc~mLe?IOqW!~AQl@$O4skFd07HxiCWYkuA!PJ@MP zEaa;N_jVT04`C#-L`Ad5jkAgh!FJ8E`{dbWDue3E+Pe;btOn^%V)XA)y>KxrOT2|Ax9bexnj>1FAD7vJt z*enHbyh)(^gcL|B8-~=EBetQ4)5yXHTwHZj85Z$9GAh=c^BEu7h3#IpgS{Y%k(4;*uEaZ$E-%P z;d!+6N$-2)Cynhoy>7Pu6)UGc{}=cjQ$z`eN|dsGPYMme!+2i8W>@nXuI2EwzR1ds^7Wmw^BlFrB z6_*KsqGIp~cJhMpkAL;szHGB@Jf>6v%xZuoUvr;NA#l1dI1M6a+BY0W0=skGmf>w@ zx*`+&Ez@_4~4&8b~w>E6;X*Oo&(XEmJ4@hyf|5O2S0m`h1ehdO~ zxg@;3wLnOW%MsKW{XD|Hu=wj^4O|DIru6(SSyp=$byD1;M~G7U4&YrXyK{j)QBBOc zTf%+FXo2W|Bk;vAjs(7`qkZz8ba&&pUEkB~ zEEAs@D>hL>uj-()8NuWc`>|(O6EL6A2ye>$9;PnX#}oK-;Ky&-T#FiJob{q36P^Jpw^JU@+Tu?n%=4vYJoP(^Vi{ea^T{s^b+~k#Q2zW;G=h z9h0xvNz^v@3-$Kkd(>qH?s?%i^`fG&b}LROB4sBUHM>XWQKc33Zq|fc9Hv~P7UNUP zuaBo?+_2J~NG=y$pY^{|aL^yjVmU*u4)BMfhPi5y4{?U^RV8_=t|B^`0G#|yEkM&fw^9dO-Pgf{3Wfo~Gy`e^kjPOAwP2}Ft{3IZW zl3fWJOv5T%$LdY{3zd}GhA#}?BcYg(#Q`dT_#R|PExKx{sy!K3;Pjyxa9gxP5b;7H zujwenOF!}CKVMj-#TQOny^39GvH-Sad~l0xq6OSAHB>lk_acJ^>1HY4e~u zW@9dN>tL*co>lA-!vWOngW`HL&m(T^6ILXa=@0HHfML4HSFFB8uO6>nwNo@6Qpydu zS754_>d79=9Pe>jPV9c7$yIf9nXTEKZ!5THBf~Ts3DrxZEJ@d0XhOC!48P+!5fii- z0F1?m8~XqSi5D)C;G}=Tguw!*?X8?{7hZ+s9{MJNJj>SIHT?0lnVQ3HUG3gZ8Edl7 zN{teH6%UVwq202$u8BnIk6}9Q7;cT(TB1CQdy2mC{Wc&-d`Ukddg&)t(BQSFZ zFWM60ygVX=*-Lda7|-^!yU}37QD9H3nA^Pg88>t|07nY}2~P^{1z7?iOd~9-t_ovC z%0aPadKGw1;@(F1XfX4B+R0_8&27}7Qi%qp=0C*w2wdf02#q*J37P!359R)}(Bn{G z{}8gNKmwtvD@IhSW!O0FH4K5FyDAHI>?OhXc zilRx&q}-_|)9l6f0T6JMO+B2sxI7;6-MH-FrNi_EZrEzA9j}FvR=;6yu5O9eVr6yxH~yG2YcMdJ=R!>CD6v;vq?- zHQ$XWD_K`-x)R2N$Vl^)mEb+vA@Q%r>u;s%4CDtSS^*N`Hg$76$Y781vO^JEMzRvo zoq|Qz`8cw56!E7kUu%S?N_${;Q)M{CD zY_1KlL`=(55Y8HSFu`I5NX@0BWhPi~TEP((0jVF{@*=`;lwk8+$86@|4{EjVO*+ot zI-K0L_Qi)@X*x-fXK6Ln<(aOwuUoj)h^dF5uc0BP7!pcC$g0hDA-Fz(uoVH#6bmrw z6-s0nw3(*}Yt^3H?Y$*Fbp!0yJZo#!t5anPD?z+3^#G{UiWdVSvt*0hh0$kfD(@fL z4~^SKIgOP?hE^C`rd{SuNyn0u*qRAos^dH~~0V$A@ zXG16L?#h%NvI91(@|7eRoEoe_3aFt7vO1rdxfzGSTDx?**nCle!H*FM%@yvAjG`(Z z!}T|AL`#drYkthmeI)+jdr!~SUbiWz)L-a`_TW+fCNJy5^w-8aN*IN}N(LJmgHD6` zmpK5ww7wX8W~FYRtFXS*eZOK-HUAL#?BAR1K^!B?Y{LKEY$s`kOOXC<^*M6+61hQ& z<-hY|epXl({R*Qe*17Le_E|IBy0GhCTW;@#0&kC@u98aVp}?(^7q(u0evC+lwPN+Z z98T+eY)iDiQ9MLMgg5TziUzoBN;U?|n$SKiHA4@t9e>K=%vOM?4Fxl0CZ>P*6&$DV?@%fl(gdQ zO$ese7RPAXsxGmj#l=t%H6p?1LNXd* zq^K{YFCMW02k4?>HWbajIi&SzAHi>yP7=gIy`5Vp5*XCtj{$cE8j|6o&2Y7wX#@l| zBSm?-ytR&vfT2RA*F9*TlPU8)^u-U+di=hJUvp2_Ympp_)_s=Gh7! zc;-Ri!G!>aR+!)1C8BjWfofxT-1Ny0f!@yr(+5FJe(qMDQS=hB#LtKv2q(+UtIy!C zYINrCvBQ}ow0w*@@@f+@=>w3dCV-?hrB81cqb6L-2E_l#A~+CS1~JB5+aJ zORC(4<1oi9vCf2C0ijHI{k77M6!TFi6r^zo`nBnjiwpMp{@kl*9E}jFy8&rHK!A^7 zHAyu()RjOD1&@*@i;iwWN*YD_t=qx2cdRVJYj7VFxP;RGAw5DCk@>oNt`MC6L&$#M z*}z>oGG*!0IG;u7N3TO=ZSuK?0J;Icu#(2Fi^WSEe$7=A)#-SH&QY55GexphS~_b< z>Goo|nF|scxE9h3q50(nO?ai>$AR~|E6JIN0ZvdjI{vf|>-Q{~B8w0G#CbIk*M#bT z?ODzgnrbHJDx&+f)$+zV)Zkw8$B2t4FI;9lwT!>m{zX(+&G`Y&MJ-Ol1isVS(#P8z z2$Hzl%$XeCT|TB;i=H?f8>6@dE*V1rmy99eRzUUo$S>zm0UOp0g^sL2BlHJ-h!7(S zD@N@uxjZ^6l0dclr)nPSVxgyMyk(n&5s4u#6n*E{_S;V~v-68<>tX)E!NIpBQms^_ zIVHjz8(IX6V}6CHhguGicz^fLbyUc53@7VSl+F;9HhB_CCKF{+7b;B=XNgaduJ#xg z&&&atG$lAVXZ37^-Nwz;z9%i;&Skjvu%tr&DP%c)t`gj<<$E-}Oo0oSQ}b|xeQhvC0jYXsFCt0wzjp(%F)2II*lVHpX99+ zE}e`$PESrB`M^knfJ74`h=jZGncK<$OHxd}ASA8T!}c=MQ3y-ytBL{S7V)R_hI4z7 zk+E;)=+d5RY*DuX12@Bagh1VZC}NZ0ew=}OV*+<_^pv(&rH(~VjYYo07^8I9UM(Jd zS7NharV(a}8j(p62X{(|BS^BL$VPrZUqQ9HyJ6oeXgku-7(&(WmV(E5fM*(Oj#QA? z^T90Q>Y&T+<2F^=l53XjP5)HEK+$>hyM|@O6SC!`NohP{QU`hZp0cf+ z?APcBrQs+07yohW*qrSS~`RghMwnr2fRi*X0jsNPS=n8 z(@3TKRMj0mAO!yd0>9@W&sBUmh-ZY1R_BI#+SqRzF z$|JpCh>#T!xJEnyq3xtN}A&u|Ag&m@VEVnL!aGM!~R6M%>*}}AOo~UgL@MCI5+)3K$2lZ zXvA(b7{VsHzt4{%MC`R0-IK%W>tLQO_=?fI)Mw5t-s*_;Ivds3NopnlGI7#oaXs@P z?`gOz+L3k*8rSa$NANesgUctn-u}|%h}`*StVOv+VQZcu^vCuo2DqZer|vc1^ip%W zJKj|N$>GOw5AQ=(RMXm%`HK3j`t7U&pO>iPHFxaB-Y`K#V%e{E@5b4jp3Tmaz&tRjT$Sm~MLVkKv#UqXFNb#YTozmx z2p2K}5+ehAa8&lkWyX+~7PQ}v4PX|vL%Ju{yIt2k%nVHO(Bs0*xd&R98(zplqgpLB z|m;f+qT0}H3?I$%E*kO2$g+bc_7GAHFh0d+<@xIl$OYtHv#e-XaIH!UzTxXip znx2*?iYUlOLY}b9Phz-g$wx9;wF+F@t?x~c?|;E`jwr=4)9#dnhA;*>|DG-yRs0*X z*mV`Im&tw54mr;uCc`%&P77E4YCOsAI-F&&eUugUYOxXEI5^`*5i+@6MX&ZC0n;{m zavS&r_F~d@e_D#6``wb81?vQ4?!G=t#7r~JIN!R_(j${MCr=F`lxtnX=1yMT7qZlo zJ3n6-WFFjf6M2HaP+WWb)XhNNI;hUBKoPoMp;?~zw9v`>r&07ZmH3-Kt>e<)2!aGg z@I`^aAE9w3CA5bwt0U|M1q-;3;YRg3nBl6Ieqj(vbXT@P3RtU(0ervb9C8Jj$}+Tm5aaLw!X&Ux{q~S|lYpa+MWjF9qxJJ1 z>ue*+L;vTsS4d$*8}{A8R-1j^J^Aap>cn-e@;b4|x_Zf$jRkJJSkz8p_RFC-e_sG= zU$A%<%K2oOate>b&Ih@JX>(~~zmHj;XrUgf6uMtmRC+gf&$jwMeMVG(FDrk>dx6HF z3+?r`z8JIPsm;ZiKBkz_Bd5mQVSxTjm?Y<`C`^O4so``ZF#UZ45mz4HUmDZ(_T|xF z?v%QonB|mmb99iU%G2KUmYTZj5tFEHn|HsL@`oHVQLxf>oWHt- zfIRR70W5SQLC;lkA^j|WJL!)B{9?NDM=5CTV6i3PU+IdYfLT<@?WY|Zw9+ca_}fY* z5?JqY2Uo!QRnDr5@>d&F{eeObS~;T~IZqH4A^jD0o;XmF)klK=EY82R;28~A8lA}~ zL2q1TZkF8}Z^{C2Zco7|U?|G4ER8?bg@8mT5AU`tSa1KIyB*Tc!IZTgwh|)kjENWa zyxuR=M?qQ(xBr*(Bf_FTdI0+9|AP(;9ozGmDT`2gNI`lXl=9#e0a7`-wdC_UUP>hy z);|UZ`j`0y4UD{Ph7Y%2M*7*uUjRVyale~bbFDM1EwR^-T><2+Y-2*P7U zk7uhSy;Gdq)?@h}O%DRgqe^09MIz3!o#Uy;T5#$=z&vxJ@6%W_W;IAq6wI?y8nYaEsjssxagBW1i79rpBSh6yhJWh z9Ch)(Z0;$Z{I};8rFG2>XO4zqucAdi1%$rrOlhFL;;P(jHnX9b=$w3#NBLLz{7`|t zZPZMV{LKY$#eTgyTrhbs7(I?-ObMAvY$$Itw9~J&8$=Fl%l!CTt-#+5QE-pISXujU zy&+bJy0ndbk|9cHZL$M-sSftm!0WQs!cKTQvp-G$QTQMK2iJoDxJ&TeekWEUvbo~D zj$3A+N}U%PA&_$ku67tW5F@5&g01}gmsrARV=Nm*cXO7xmouDIjh_Rj!|1Sn$$7$&Hy;!C z|N4CVbk^tc*1q=1)}(Wz4#?jjmg`>}RnDOjvA$Jue;FBfWAO#$5Y}IPXovvHl5~K@ zHl`wcSm^5iAj^HGekSlaN1rC{1`1Nk;y+r)NQ=;DfWM(fl#g=0l`$D_SM4N-HEzat zc1eFc$hdrY-bKA>bX+6*a&*+-cz<%$(CJEIqM3bEF}*;mC^Mp0B*=Bl%E7g1XE#Lg zrx8c|mA~~rQ}v7;I03HG+E5GZkh#A1)VtTr_g}^ z*qdEx`G_-<<$_A-YU(L}2Xvo=diXzvj7Saa#%w@DT3F?>`{Sd_Eg{Qo?|NzG3?Jo@ zg>Ha+rCAY zo-4v1e|7Tf<9J^JdTyH1vd?){;fjy%uIFaJCKtbqciX@yY(AD^%N zUmAugR_+GWUYw&?qy5L6cA$xmsr%-2kWa@P7glRg_P^|IaHpR@V9%FXoT?P30;abx zUXvVgty1LO{%Z{4GT>0bL9lPr$R3b-&B3{R4DLS3j3Gf!a%_cyp3CHaQG|Eh8T;9- zzf+u<1>AIE*lES{S@+TDFQw}o!q?8{=^MOWjBjLeT~Wy-5E~7c(vbcoLx0@d_wzrq zmDiXI0;aYw;0Snl?dVbRR7BHDY(`cY5%}3CWDeY`X#dOYvmnJDqjIYLxm)6xuJLHzK zm{2XIpD3sExSnD5EB9bpnEd<26cy2nfDKaK-mB`XR@)Psu?WfR_^WeBbh&TWVFw{q zJ$I)~*h<6ub;7JQvy_%z&3DI@Lshh+-U*>IG+Fle{gbqM)#P|0^3tA*1J_NQ!7CSx zdMU~SJKIxkwHs7lbSdLjJ?Hp&da&rS^uj_b70WgkUEMF=sZj3K%1$~P_#YdKngkrA zG-AUnWjitR$SWN&@F4MJQlf`liT0wS2`}|=3OSHHY3LZ_F@YYtB4!Z9fA4?e736v! z_2{MViUhQ+pIc}NPX=oEi78Il-Ip^_<8I>?{(Nc_(So4;q_N0-)|}^~i)-gd0U}j6 zUoM%J;w~HcLukgtZpCl!)4unix=%BHDtJGHg_^_of#|5E6f-I0D2;bh{@t z6IU_K>)#(#dylZ@t+Ttp0z+MC=#Df?`QpVUq$oZqy|{HGFCs1-y}UO-*VS3jZm(Vl zEnDPzz2KBCw=S^om3MJ%+xm4z_qW?9_kG{)Zk;WfiEND2C8H!3oIkieGAOo3_r1Tq zEbVMsoV8&!BX=ml!TDoUem3^p%8fDLatp_k!>aKCuTN5_-H+jpBd5vUD*Ni0U~TgC zQi<6NebLG5w(7lXLl|;JBz=?az>Ac!g}5m_@g9{)snJ%0&eq^v=p_2*PWqlr4|ccu zj>+acD%T~)&kI-cV&fg5vy)WySF59_7s4A73qjsad(OofKlFwl;#irtA7l7#kdNI9 zm>wCPfJ8G+g8yMEi zii6Pr8iPL6$+9bJiq%TCmzJBFMJ1Uxgq0x}Ros1LXS?z~B4dDezWX8RT`nW)!o5qt zVr#nAR=?XE8#I7GA6W6wU>gl}Z~vt{<#zY*v`-=;Pu?b<$JmjMxZedTp zCIw=&OBoJCsJglb2t>Z#e2xOs?`(R{Y}qUL{b9#wr)O7_B{?C}{9*+Wd3qw|Iv%z@ z!(@O+gMP2v%VTK4titsiE`I$(+V1!3ZO5dQmlgUM#l00yuikXNB6w-qD!@D{ImoDK zOTE~U<+XwAcg=pRO47)l6;WG1j8vhn2opO4P))I&ovNnF9b!^p6V zu-7f$YBj|W3ZEaRyx6gK?HgT%P#n;dP_MA#l>6YWeLFGAH{r1L43IQ_;dz)GkjYz( z9H5m_uGRlFTE@@zb$VenIPNRW?=??!m1JmxbFU61A1jU$UZ-yA%NdFR+WPS-y{{aY z2)oJ=XvY5bD6rWSGFIJ1*b|1TUIpPwqUH@FY;qgwyI*L%`+3~&5a2fxlomKqIs0(a z1jI7WdlJMx8LDDS{%phb(?cqUS%W>cDweYt|LN#=|x(fd_# zhKG{D$KaOsOBdN<;rn_MrrU0(o?~GJJ#tJW1_5bAKWoHnJ=X7{vyMSF zD@V^-p_7$&iaE7rkaw~F%{)X|~)O`aeybbzIZ!_x}x$5(X%tlptXNqDYTM5J{CBOgbDb3}H9~r6d(4Bn2cC zHoC?@N=j+jh=G*E1|vrfesAy3ef$0K;7{IrU)Qt3at?BZwSUNeUGl|LTIm}Ar%v}*HlFde zJM5$z!<9Qj;ruSYZMi%~%{EAT!{{M-L-A);lV%39^Ig8ozs5AUv`UG@-H)9hPfpb_ z#Nzge9mKEEex|P0KE)nEsTte3tq-yb3mexgn|sx}B(s3+-RERXgL&r){+%ANB`bc( z0^dXQwvH>Un6zUH8;cfu-p$cyr@@)W0u>ar^xY4@enL^0U+7)53LW&EanmYx`Y5JF z`Y|80>&}aKQm4dn+HT!#s@Ury z4- zoQGxyRWHNP%-8YqCD6Iz@ocfpLw__gJ8AraB3ySj=6UaK0NyVkkhj-gFQ(XfF7!1Q zNhlJrRro24+(=N<3}+jM&dqX?tW>CFp#+g%o2wkl>b5NhwVRr%26}$;C1JZGr)z#N z9yU^UCq>@gV>nJbZhSJo(HWY7&hY)&K6)t81ixh=sx`Jw+>Kq;*vcza4mD zROH+JhEn%k!JbT0Ol}PGM=u7rWPG3%@U(PYa7!TG-Q{A1}Q^3^X#X^l3Aa8 zMA^#BbkUbt)x`M&7=EnR@%>KuC$n0vg5<`FBdz>3HlQ)ASWE=C zbF?`zAOIq4-+))_uFWK;a0{C#1*4xeHy$id@*u0}1{(0uSW;J(G0W9KyDYJjSv>2s z^&5jvJFkN%ccZhb>`XO1%tLyY@COnCsju8zpi|9H(Nm&2&;3W=w_c3O3_5Ivld%%X z*qPvFZtt;mQ$BBaY%M9@5 zg>BUhgch06mVt+^8`^XQePQ-&)U?b~WO*zB9aQ}_`c~;d*onF`3)#4Os5hqlY*ZO! zHjEG`t)~;?_=}6(e&{tf=o}!A@xVSx?zM^Ft@-;(L$L`7+?bT4x=1c-CQ7!}d(^$S zqw!oI1EjczacKDjhmDRH&kWa1iTCKI*F9bn&ND-~bx8^|dxU*tj$*)kLOs^eGZI*1 zidyG6ZS5GozSV=B6$$hUszIKfTd<8+6b|PTMkao`84B*K-v6O4C@8fT7sX04rq^*f z6Wswp!u2sm@B2ZVk(OS|tQW20BE6duI1vwu8YdZHNBW3QOl+hD=d@Pi6 zY;qb+ux(*9$HDGmvq#7uxt#`zD5iU1r@j3dF)yyawn;WCupJlnV?`HKz5Lx<))jcN zY+HgDakD0#>-vGX!o0Mt9lkYOGtAnzd%hdyDvrl;X4KQvpQQtYK|oGq5%gw+i`Q#j zieQMcJwmm^p@<>Kw+T@O@n160wIiX{h+u0)1B-R>^eqU7ilKPH4ut#0Ucd_cll|g~ zB@K^ya=gtBWbF$sp!CWn51n;WRN-7HUz0Lx<>D}`p)?xb8fWo+qHiFF>}qhm1!q{g zP_a1dsq}mOzGOAaMf3`lC=2g?ekoB3t^C`+F?^~%v((=1{OF*dyA}dpa0Gn)0&<71 za;NcePNJcT7%}#D@Mc7qQuGRvdf==f>l+!tk;ndcRm8~sM)mgj94Rv6S9uQO9}ulJh&Ne^y(mCu6gZV?{&%Xn7THJ&B#H~3%l6rjHlw<_EH@JP?|0wXoC)X{n9Zq zWh~eNQF%DCR>zE#*{o-_22Q>)?guJle^y0t33-7dn)XtnQtD<+)S??lk2pkj-(8>k zd@CxT%xHmuTk?#fWG$!WOi{(*o!n?2@1MwVh1Zvo8FFy`*8Dp>XvD;8xEcMss@PS! zFnajXWy^*-8e>5zQs904p0DWOvBp)7++NRy!~GYWEtFq**~*?`R!Vno2~39Q7}AhJ zTufPs)h_ELHFeAia$Rv!NuO>eAcj1Zs{76#p|Y55!6c}C!OBp1qlT!qamRMhx3O538r7S^o5B__N^fM=Q(Uk0BJ;zp7mK>$>SO}~GWJ{Z_ zdA^`sGcZ@i@9TK?XYuAxr6r*ps``V5hEYp5q87T|e~C8#&>w4))WmfSFAqZ3utEJb zLV>(hR$Agl$_bUAX9KB^<@B1vXdI063}de=H{w^WIvv4fle5KHqb-v2=J}+jujGM| znG@<8?Nyh4X=kU{J9l-_F~F4z=lkE{26IoH^9;=FRf!UV4JZqVTAYo(B%tV_gsriG zj$Brnjhd=r^6E|X*BWS%HfZj(m7gK(ocjnwZXjRj>Gx#G0rdWfdf0kFBo)oKnRP5KUK7l~>0J(E<+uOKmSe(vLP45i&7+-&h~KO|L- zn9$ac=jlZlC0-OoDMhVlW?YX}wX%1%cmUKh94%anhTGCI`_5m(Yb9(Ri=5X$eBWe< z8i)p4kDs#);koF*fi3ykoij?1=l34C*oYTdQm9J2GQ1Lu>b!AwZqk zUa?|%KLi;VrPa!?Xp$39FxE&{sL1es?>NrwwDY01+rUm)Vd650f8(B;*s@gs%?z(% zPaL$J3C^b?p_t+VQi`ZO4;v_YRj@VwL{&yOTxIWu!u$$nm3Bt^mPw+=Czzy4Fi@)y zN>@4Zx+jC0=xg=Rsv`J(qc~x_OmJP_>JGx%yc!iA6`r!k_jqKhhW!=!bhWbqF`07? zuALlIW_}wus4>>EIpr3JrYM80h`|E8jRdPwe z6`|JMrHFDE2!m_B2Mq-;{s@^Wp#dS5&Rl$V_CI|hK_%q%dD7nO=Z5Q}_mQ|^9xHYZ4C}s1 z@v-?#BYF*-pliLqmVCchtyz*YBSQ7T+kKsPDda1zC7-q^5rN~{7!e0S*fr9H^J>_3 zkFG0Aj?8C+7pVL5(o?fOg*U=%$_Tu?u|C3Zy$aHMAiQ7~oCz zqpc|nq2RNhHtwCP+!m%mG*pM$NXCfKAa1H)1i)mjzJuX$>&)(ZrQ@-5q@|{EJb1DA zt&M`?y=^5pu;!37i zWN#Ty2kJI$V+!OlelPqiC@H}jM9=MSb`alLB;#ikg2DcTs5tKz$8)m3*T_)nS=9^jD$Yo zQ*-+oJkz>3ms-%lf_;DOao>bc6cwoWh(7(Je2?1NP;1cw4Ro3a^>;4LW}Bek)bZWv z@;iolna}!*GOXd5^P@vD+dF1YPv6Pn?H1BP&gS4@%`czymyJ_u-S&D56qlz+*ZT|k zDYH)t6gGaL4Dl7(upW)nV{%}X3wVywOav}Oue8vaPFwBj1Rj6|K$v84YFwQE_8oUI+b)n3+sS=?`oyGw9_58WgQ73#I&~%Gh<@-l zvD(-4T)j%1rRD?AQ`~b7iqer!;vE;rw?Z8GZ;U-VES-(Eawh-0DJTW?1JaWn?MbG| zIC&`b2#`DSRxQRSFRgb?Gw-+QTgQQ~N-u8NCr5x$Wqk&jqe+w~cdy@EoiI$h>-{^;_~w6_(>MAE6`l25(2_`L&eOwM1XY!i1-QpPp1 zIzg7r9`Xl-V@g6%c0x)vqTQ2JagG}|r}iu$mW-U$Iqzgys?7(OS0IsS@8(&C#%H$Q zVKPq*DyOrgDcLekomfqb7UMc8kqD}Df_i*^*Cl1x=pKW4uIMC2i{yvHEAa{3dlILt z-R{Vl$t*}t?&zpnUrSHb4`s0A<$RR&7v~x`7mU1` z?alp=PqYPJGEhcr>yuBU3TY7uYeVP_-_)6(k@rh`Y+&Q_94!5#p&6|K#m?cxYiMvQ zJ^Z>|rMbP-NB21njT<04B-P#h8p-Y1JEv|?oc7a1=>Yc?F7=PWOzc_vYJHT-Qyn71 zc^)Wao|q<((OX>MtVS;(62Pq+-U0aB{B3rUQrV!VWwC0tx@pseHTJf_;Q35Do`Be^ zfj7!_vY9Os>UHXGh7!Jb>c}Pb+`nck0Hs35hd4aggz@dsy^iBD2nKod(9>LcN}+vL zvJEaVJ)0qJN-jr?N$q>3P5c;(ZJ;pQ`ZyhDnUdPvO#%Lb+IT=js8M2nZKol>2b~Mc zd<&&%;+%U+7jb}(r>tVW1q4iLagc0RIqLf6OCZ8mKOpBh8MODrdRa}V$WBTt{CJ~N zj84W)#bvwveKhCcY=+bqXhy{|r2!{GXhdZF*0{4~YriB(p@RIiF4q1RH0zP64zL8@h^Kjh}LIygz0)HB7_EPft6$?x7ub0VJ-2pWU94${+_s#P7Fr~GGknl-|RSl_QU8)jyr8d z>FRbZlsjhTDY&#VW^&s7kSIANQuWl57v1%AKLx8%j-S652_)AoCSz^Ko(p{bd@^?UhvTH$m0zDfFH1Z~(}O^q z_BwhNB&|xlNT21&H-0zm@RY$?6Kpk{&f=ZEmI8)@D>~nDPfJFwb>-|m{VlL9Kz_bv z;0k#)2&Uf^e~0Lew7wv01r?3D`qQq*^9`4Z-d2k?B)(SN&Y}73jSYvh$d<2+S8<_w z6ny7b82Fd5J>GE*SJoi&4IE>G12O&RotHmrdBbTuFTgvJCBnE9W-5Mm-u7zKUX+|l z^Q;)YJ=Z~h0h0XaGj@_$UJvSkmNrNC9o07EINNP$Y zxI2K{&Z!n;y%T;zp~|l!I?p=Vpfzq9C}t+AvuJN@z`1~&9*HR==HZMw?d1U#Yb#xH z>Och!{5j@Y)DRs%Mf(2N(Swz}U5_jtnd;B=^=a;6(G-*GrjjYLI~krE@fth$?dC@6 z0n1N2YlcJp&ExIf#Dk{8Z)L8(->+2%v=?r+s&|-g*sRgBt;bzHeDX#GDn%0Fbt}%G z+{909RePy1P3&?kh{;d8Jib0o7w?{B7IdYiV}5V=?6d5-_Aqwb5~IZ%`1L(q>Tosqz@;pamWBK)9u^{lQ+yKaD{chfbN2&`zp;6!biCVKoo47f zae}q{o~GKP>NqP|6(=~POE zVNZb&Em{_N%PR8EBHpwDEAX-L`ErEmb+o+mTV|a;hcRXA{iPhe@Xu#FFn*^^OsL~k znb(U-O2jMj%hogMo-PD$!lIRfzz=8>xB))v>`Am%o-IoM{zZ#LP8k+C@f8Ut#Zb}L z?KgM=00!q>$_aHlUAGrk+MT)HtSS`R_EQvl@%4|VY)wq~Y7kvfa0@+4t&);=uI?gF z#`T6rk~p?{cyK=+mj|}vOtBRX(ORF4mqROon^q>xHT}|Y)V=zAk=)+_8lDN`s!+^O2a z@E}`_S>ucwxWcm!iZLSQFMqG!&I%ahOS|KAWvJSzHQb-;P6=PdlhIqxMxtGL=l(<8 zfT2qP_3HQwF3ye}Iam*3$qw&e?&|cBbv<*Dkk8mHjdy##8k;TcJR_O?m4Vx7a5Sa& zvt;*Q1_r3=cg6$7W8{P`(!{iMhg7#!pDO;#YDxAtyhoheHr&yzoY{`{orwd1YS>N{yNjTo;V zV^Ty}l$ws7h50}YdwXOE+&$J@j~xk7<$Qnw-xOJtZd31mJ!^?mGxMH{R{Gy1{8}V1 zu$Gt~c36LM31u9my;6xJz-sVUzkQKU)f26FV!wG9FDsqZ2w%~?p`f2aurKs*W}1)GD3#jEXsg*92S`{`7?_{kXbqAh4Mfin71K@Ozsm@3|cYtp-Ta}+p75- zCmfCM`~$Fr*!`J~C?RDNS!{-W^O9H{ScuB+=%=jkK6UJDzV}5|W^wiWptAKF!`)#Zuz4!$8yftcgBcoc4;Nh1mG`ld$!aW6kpL-X$#fN9 zH|A9FT}yFEW=8bPxG*$ZxQxmH!Lal;9S@+EW8yRyh+e;p^kmPK|8cz#X;y&AO5n&^ zQv9{fM?8ov$@%P2J;u9H?lBZjD^P5a2F^rw>`#Iy%Om!MV5_L$9wVnAU|)d)+Lq@# zL<{%cKQaMI5FO2i-^UZU%`C;8>Uf_ES7)%s2nszvR6O?3QaILEB5imcHAzHZaStOG zKcqWbbl2%K9A0YnAYR?B9ngCqEYS2&&YgsXkzqf2eM&vGD{U4oPr5_G#u1v*iIJ;4 zQuYIJ{{fH=0OBE5cv)!z+lLy*?F4-GjL#&2$wDRoXI1S;J)HjmtN18!=+BaI!Dt+D zm;51W?on#gNJ%!2f4uWF&NqHm?_bw%o!doy%bFo#E=W)wpkED>h^6&tSZQ z1)33a#Ms#)yv;viL2N5-O3ey=_kD0Q43uo2mS=b@&B1W`i6kRVLWJ+L_#+Gl3DsV~ zF45+eLr+oDZ1X%#@$q#+J_Jycd;aocjDKV%HUOg2eqpklcOmEVlpk5yEus-}T<;&B zd0=K@;@9MlW|xBunoxT5>C+b#Fclxm zGgainU-4+!G$)8-q}k34VL30|l`RJkcNx;G+5dOEr5?=-_c;DEmW z{h=~eqNAs;%CsnSi_2%yzUkXyW&DS=1YO1`2B2WYsVMwSa*j3>OY+oz#QFz=(bYH+ zij|&Vy7pe{;4hFt}R`OFyqYWIT3No&A<~(G;lx=Ukc6LUhDiv7=!sKfkYVUOCJVYmO zQ;Kh&;{ypuPnU&)T?UGNO!;_=EBs{gs47_y49DK~kbL(K2H$l#;bQaA)~>fb22S>~ zCFUm?yO)&sb={_Y!YdZCjIvIvhkr>W)O!D&7z{$uE3GhQil`6+_${o)B-#Z_a$VtxBrwjMa-nqsBG4CzoX#>HvzKCF4 zt<$<)B*mpt@qQ_0rrH|1>Qbfsdsu3R1DdVDIMl z-gGf)pixH7`dRKyJniE}lBv0~KQ7b(EiC_y)|&xP4ppAdFu=4k6w6qoi0v!FuALe7 zW6p*dgT8a8K|2N-i2gJGz68Ko|Dh5$U)kS^fFiZ#&6&i-Bfn;eS{vH)Bb5f8H(okX z3nVfB>s+|>hK5}n3iQQ{F6f%N(o^1k)NTg$=l_0bfnX?k0&10PkhOk30~zKPvre|s z7VtVj051iOeQwM&MOkPR%ukATPBDr%aR+c(7T7^S%}~AiPM-f>kZlei!O>bW>Wx4d zpn`Rv_c#txs$FPz?X7xgK}(Da&A-!p#QTQ>yje$2am78>KKG6PeTnhrw^-k$*u{v@j53xNE}Dp>u6@{H7hU?B2JQj6<; z=`nR%cCnP4J}}t=j$qCWq1NPF`@F$H^6br;3SoTQkr;0KlBfHH>5<6`o4zbQ6wN=Y z3oP&zK421qWuB6wlES~g&&Kxz_NU3ZMb6m@$Or8dM;xKF`Q3LCqF?MVRsfY>vl!Za zgK_uq%m~U@^2zds&%ltlWUOfZ)8UQm{tQXx1Q%HZuT}iCHro2q#q;bH;|a}>wubvD z`Y8$AO*hkSsV13m0E}2m`2TUhx&Y#hZ9CH+N{}>Fim}Ps>cK3Ijgv1?!Zk`gUCjYQ zFOmvR{dF)0;5~2NvR0#;H$DjV1T=flQ8z~`ccRq6q!=A#KPHCF-=qPa#dcD_KVUvnfCfuLGJ`B{9+uJvmZ0@;wKX=qb7ta$`*Y+JEWNA1YiiheHq;`yaaT|2|(E z_;0|KL#^l~h)P4uWV(t9 zgqbg5vcdbZb>OT+J@JipMbpyIgZWaC$uAi-eupcDp4ot_l`kD`+;D_MTgcELq?Uv{ z|L<+UBqTXUQ#O9)K%B4UWE|(yVkS@CB@dx!5k{F+1Wd=HgN^Ppq1Xme$k<$r-iaXCpTPf&ZWCVsl(l`1OJ^H_{o<3jMcTfCKnqNE;KJ zZN<7C7Xy=9{H09wzn~lGYG^4N)6@u*3}y(lmAhe0(jJreQ)T=rc5{D^hyHss4+Qb4 z96%?|$|SL6L|;62%o0+@Se2+?RLA*{Pz8I4^SDz1kwsxH*xsCfA~MYM*9Xpq9D@w* W;!XrUeK&9d__?QLpovnq3H^W8E|Cxb literal 0 HcmV?d00001 diff --git a/docu/docs/information/router.md b/docu/docs/information/router.md new file mode 100644 index 0000000..93a4bd4 --- /dev/null +++ b/docu/docs/information/router.md @@ -0,0 +1,93 @@ +#
      Routing Mechanismus
      + +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. + +
      ![](../img/router.png)
      + +- 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: + [...] +``` diff --git a/docu/mkdocs.yml b/docu/mkdocs.yml index c8432d3..0948dd6 100644 --- a/docu/mkdocs.yml +++ b/docu/mkdocs.yml @@ -15,7 +15,7 @@ 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 From 64fc0ec86873213a5d3d48d39ff026b1d423a36e Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Mon, 28 Oct 2019 09:05:14 +0100 Subject: [PATCH 63/70] fix routerManager --- boswatch/router/routerManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boswatch/router/routerManager.py b/boswatch/router/routerManager.py index 341fad4..cf26d5f 100644 --- a/boswatch/router/routerManager.py +++ b/boswatch/router/routerManager.py @@ -86,7 +86,7 @@ class RouterManager: 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) From e061adab4cb5da5a37c311b8692968891d75a8f5 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Mon, 28 Oct 2019 19:57:53 +0100 Subject: [PATCH 64/70] add basic Dockerfile --- Dockerfile | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..36568ce --- /dev/null +++ b/Dockerfile @@ -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/ \ No newline at end of file From 2f5184742fa7e580601e8b7ad2e086e02e4c58ef Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Mon, 28 Oct 2019 21:20:05 +0100 Subject: [PATCH 65/70] some refactorings --- boswatch/inputSource/inputSource.py | 90 +++++++++++++++++++++++++++++ module/descriptor.py | 4 +- module/filter/modeFilter.py | 4 +- module/filter/regexFilter.py | 4 +- module/{module.py => moduleBase.py} | 4 +- module/template_module.py | 4 +- plugin/{plugin.py => pluginBase.py} | 4 +- plugin/template_plugin.py | 4 +- 8 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 boswatch/inputSource/inputSource.py rename module/{module.py => moduleBase.py} (98%) rename plugin/{plugin.py => pluginBase.py} (99%) diff --git a/boswatch/inputSource/inputSource.py b/boswatch/inputSource/inputSource.py new file mode 100644 index 0000000..a4b8766 --- /dev/null +++ b/boswatch/inputSource/inputSource.py @@ -0,0 +1,90 @@ +#!/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 time +import logging +import threading +from boswatch.utils import paths +from boswatch.processManager import ProcessManager + +logging.debug("- %s loaded", __name__) + + +class SdrInput: + """!Worker class to check internet connection""" + + def __init__(self): + self._isRunning = False + self._mmThread = None + + def start(self, packetQueue, inputConfig, decoderConfig): + self._isRunning = True + self._mmThread = threading.Thread(target=self._handleSDRInput, name="mmReader", + args=(packetQueue, inputConfig, decoderConfig)) + self._mmThread.daemon = True + self._mmThread.start() + + def shutdown(self): + self._isRunning = False + self._mmThread.join() + + def _handleSDRInput(self, 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.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: + dataQueue.put_nowait((line, time.time())) + logging.debug("Add data to queue") + print(line) + logging.debug("stopping thread") + mmProc.stop() + sdrProc.stop() diff --git a/module/descriptor.py b/module/descriptor.py index 7ef926f..164f25e 100644 --- a/module/descriptor.py +++ b/module/descriptor.py @@ -15,7 +15,7 @@ @description: Module to add descriptions to bwPackets """ 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 BoswatchModuleBase(ModuleBase): """!Adds descriptions to bwPackets""" def __init__(self, config): """!Do not change anything here!""" diff --git a/module/filter/modeFilter.py b/module/filter/modeFilter.py index 2e97d1b..7d8b193 100644 --- a/module/filter/modeFilter.py +++ b/module/filter/modeFilter.py @@ -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 BoswatchModuleBase(ModuleBase): """!Filter of specific bwPacket mode""" def __init__(self, config): """!Do not change anything here!""" diff --git a/module/filter/regexFilter.py b/module/filter/regexFilter.py index b0f74bf..622b08f 100644 --- a/module/filter/regexFilter.py +++ b/module/filter/regexFilter.py @@ -15,7 +15,7 @@ @description: Regex filter module """ import logging -from module.module import Module +from module.moduleBase import ModuleBase # ###################### # # Custom plugin includes # @@ -25,7 +25,7 @@ import re logging.debug("- %s loaded", __name__) -class BoswatchModule(Module): +class BoswatchModuleBase(ModuleBase): """!Regex based filter mechanism""" def __init__(self, config): """!Do not change anything here!""" diff --git a/module/module.py b/module/moduleBase.py similarity index 98% rename from module/module.py rename to module/moduleBase.py index 7598ce7..7682db6 100644 --- a/module/module.py +++ b/module/moduleBase.py @@ -9,7 +9,7 @@ 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 @@ -22,7 +22,7 @@ from boswatch import wildcard logging.debug("- %s loaded", __name__) -class Module: +class ModuleBase: """!Main module class""" _modulesActive = [] diff --git a/module/template_module.py b/module/template_module.py index a0d7d15..1066954 100644 --- a/module/template_module.py +++ b/module/template_module.py @@ -15,7 +15,7 @@ @description: Template Module File """ 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 BoswatchModuleBase(ModuleBase): """!Description of the Module""" def __init__(self, config): """!Do not change anything here!""" diff --git a/plugin/plugin.py b/plugin/pluginBase.py similarity index 99% rename from plugin/plugin.py rename to plugin/pluginBase.py index bf60771..12ad6ea 100644 --- a/plugin/plugin.py +++ b/plugin/pluginBase.py @@ -9,7 +9,7 @@ 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 @@ -22,7 +22,7 @@ from boswatch import wildcard logging.debug("- %s loaded", __name__) -class Plugin: +class PluginBase: """!Main plugin class""" _pluginsActive = [] diff --git a/plugin/template_plugin.py b/plugin/template_plugin.py index 2c876b3..987d1df 100644 --- a/plugin/template_plugin.py +++ b/plugin/template_plugin.py @@ -15,7 +15,7 @@ @description: Template Plugin File """ import logging -from plugin.plugin import Plugin +from plugin.pluginBase import PluginBase # ###################### # # Custom plugin includes # @@ -25,7 +25,7 @@ from plugin.plugin import Plugin logging.debug("- %s loaded", __name__) -class BoswatchPlugin(Plugin): +class BoswatchPluginBase(PluginBase): """!Description of the Plugin""" def __init__(self, config): """!Do not change anything here!""" From bf16a5c82f5bbb886e087e3cc21fb4958bd89afe Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Mon, 28 Oct 2019 21:20:27 +0100 Subject: [PATCH 66/70] adding sdrInput and inputBase classes --- boswatch/inputSource/__init__.py | 2 + boswatch/inputSource/inputBase.py | 65 +++++++++++++++++++++ boswatch/inputSource/inputSource.py | 90 ----------------------------- boswatch/inputSource/sdrInput.py | 77 ++++++++++++++++++++++++ boswatch/processManager.py | 3 - bw_client.py | 81 +++++--------------------- 6 files changed, 157 insertions(+), 161 deletions(-) create mode 100644 boswatch/inputSource/__init__.py create mode 100644 boswatch/inputSource/inputBase.py delete mode 100644 boswatch/inputSource/inputSource.py create mode 100644 boswatch/inputSource/sdrInput.py diff --git a/boswatch/inputSource/__init__.py b/boswatch/inputSource/__init__.py new file mode 100644 index 0000000..836e3e8 --- /dev/null +++ b/boswatch/inputSource/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- diff --git a/boswatch/inputSource/inputBase.py b/boswatch/inputSource/inputBase.py new file mode 100644 index 0000000..f8d5588 --- /dev/null +++ b/boswatch/inputSource/inputBase.py @@ -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 + +logging.debug("- %s loaded", __name__) + + +class InputBase: + """!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() + + def _runThread(self, dataQueue, sdrConfig, decoderConfig): + """!Thread routine of the input source has to be inherit""" + logging.fatal("input thread routine not implemented") + exit(1) + + 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) diff --git a/boswatch/inputSource/inputSource.py b/boswatch/inputSource/inputSource.py deleted file mode 100644 index a4b8766..0000000 --- a/boswatch/inputSource/inputSource.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/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 time -import logging -import threading -from boswatch.utils import paths -from boswatch.processManager import ProcessManager - -logging.debug("- %s loaded", __name__) - - -class SdrInput: - """!Worker class to check internet connection""" - - def __init__(self): - self._isRunning = False - self._mmThread = None - - def start(self, packetQueue, inputConfig, decoderConfig): - self._isRunning = True - self._mmThread = threading.Thread(target=self._handleSDRInput, name="mmReader", - args=(packetQueue, inputConfig, decoderConfig)) - self._mmThread.daemon = True - self._mmThread.start() - - def shutdown(self): - self._isRunning = False - self._mmThread.join() - - def _handleSDRInput(self, 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.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: - dataQueue.put_nowait((line, time.time())) - logging.debug("Add data to queue") - print(line) - logging.debug("stopping thread") - mmProc.stop() - sdrProc.stop() diff --git a/boswatch/inputSource/sdrInput.py b/boswatch/inputSource/sdrInput.py new file mode 100644 index 0000000..8f9ce1e --- /dev/null +++ b/boswatch/inputSource/sdrInput.py @@ -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() diff --git a/boswatch/processManager.py b/boswatch/processManager.py index 2e703be..f261fdc 100644 --- a/boswatch/processManager.py +++ b/boswatch/processManager.py @@ -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 diff --git a/bw_client.py b/bw_client.py index 587e22e..14da3af 100644 --- a/bw_client.py +++ b/bw_client.py @@ -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,10 +41,10 @@ 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() @@ -67,8 +65,9 @@ if not bwConfig.loadConfigFile(paths.CONFIG_PATH + args.config): exit(1) # ========== CLIENT CODE ========== -mmThread = None bwClient = None +inputSource = None +inputQueue = queue.Queue() try: ip = bwConfig.get("server", "ip", default="127.0.0.1") @@ -80,68 +79,15 @@ try: ip = broadcastClient.serverIP port = broadcastClient.serverPort - # ========== 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.setStderr(open(paths.LOG_PATH + "rtl_fm.log", "a")) - 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.setStderr(open(paths.LOG_PATH + "multimon-ng.log", "a")) - 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 ========== - - inputQueue = queue.Queue() - if not args.test: - inputThreadRunning = True - mmThread = threading.Thread(target=handleSDRInput, name="mmReader", - args=(inputQueue, bwConfig.get("inputSource", "sdr"), bwConfig.get("decoder"))) - mmThread.daemon = True - mmThread.start() + 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) + + inputSource.start() else: logging.warning("STARTING TESTMODE!") logging.debug("reading testdata from file") @@ -200,9 +146,8 @@ except: # pragma: no cover logging.exception("BOSWatch interrupted by an error") finally: logging.debug("Starting shutdown routine") + if inputSource: + inputSource.shutdown() if bwClient: bwClient.disconnect() - inputThreadRunning = False - if mmThread: - mmThread.join() logging.debug("BOSWatch client has stopped ...") From e61ffb4b5b34c42f5ffb53c0477757c3a4d13545 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Mon, 28 Oct 2019 21:27:15 +0100 Subject: [PATCH 67/70] fix some errors --- module/descriptor.py | 2 +- module/filter/modeFilter.py | 2 +- module/filter/regexFilter.py | 2 +- module/template_module.py | 2 +- plugin/template_plugin.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/module/descriptor.py b/module/descriptor.py index 164f25e..77a28f8 100644 --- a/module/descriptor.py +++ b/module/descriptor.py @@ -25,7 +25,7 @@ from module.moduleBase import ModuleBase logging.debug("- %s loaded", __name__) -class BoswatchModuleBase(ModuleBase): +class BoswatchModule(ModuleBase): """!Adds descriptions to bwPackets""" def __init__(self, config): """!Do not change anything here!""" diff --git a/module/filter/modeFilter.py b/module/filter/modeFilter.py index 7d8b193..a81b688 100644 --- a/module/filter/modeFilter.py +++ b/module/filter/modeFilter.py @@ -25,7 +25,7 @@ from module.moduleBase import ModuleBase logging.debug("- %s loaded", __name__) -class BoswatchModuleBase(ModuleBase): +class BoswatchModule(ModuleBase): """!Filter of specific bwPacket mode""" def __init__(self, config): """!Do not change anything here!""" diff --git a/module/filter/regexFilter.py b/module/filter/regexFilter.py index 622b08f..887a420 100644 --- a/module/filter/regexFilter.py +++ b/module/filter/regexFilter.py @@ -25,7 +25,7 @@ import re logging.debug("- %s loaded", __name__) -class BoswatchModuleBase(ModuleBase): +class BoswatchModule(ModuleBase): """!Regex based filter mechanism""" def __init__(self, config): """!Do not change anything here!""" diff --git a/module/template_module.py b/module/template_module.py index 1066954..7243101 100644 --- a/module/template_module.py +++ b/module/template_module.py @@ -25,7 +25,7 @@ from module.moduleBase import ModuleBase logging.debug("- %s loaded", __name__) -class BoswatchModuleBase(ModuleBase): +class BoswatchModul(ModuleBase): """!Description of the Module""" def __init__(self, config): """!Do not change anything here!""" diff --git a/plugin/template_plugin.py b/plugin/template_plugin.py index 987d1df..d981e9f 100644 --- a/plugin/template_plugin.py +++ b/plugin/template_plugin.py @@ -25,7 +25,7 @@ from plugin.pluginBase import PluginBase logging.debug("- %s loaded", __name__) -class BoswatchPluginBase(PluginBase): +class BoswatchPlugin(PluginBase): """!Description of the Plugin""" def __init__(self, config): """!Do not change anything here!""" From c7d7231959ab815ae08d52329cdcb90d770ad1ea Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 30 Oct 2019 11:08:26 +0100 Subject: [PATCH 68/70] add ABC --- boswatch/inputSource/inputBase.py | 6 +++--- module/moduleBase.py | 3 ++- plugin/pluginBase.py | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/boswatch/inputSource/inputBase.py b/boswatch/inputSource/inputBase.py index f8d5588..0a62410 100644 --- a/boswatch/inputSource/inputBase.py +++ b/boswatch/inputSource/inputBase.py @@ -17,11 +17,12 @@ import time import logging import threading +from abc import ABC, abstractmethod logging.debug("- %s loaded", __name__) -class InputBase: +class InputBase(ABC): """!Base class for handling inout sources""" def __init__(self, inputQueue, inputConfig, decoderConfig): @@ -45,10 +46,9 @@ class InputBase: self._inputThread.daemon = True self._inputThread.start() + @abstractmethod def _runThread(self, dataQueue, sdrConfig, decoderConfig): """!Thread routine of the input source has to be inherit""" - logging.fatal("input thread routine not implemented") - exit(1) def shutdown(self): """!Stop the input source thread""" diff --git a/module/moduleBase.py b/module/moduleBase.py index 7682db6..1d9eafc 100644 --- a/module/moduleBase.py +++ b/module/moduleBase.py @@ -16,13 +16,14 @@ """ import logging import time +from abc import ABC, abstractmethod from boswatch import wildcard logging.debug("- %s loaded", __name__) -class ModuleBase: +class ModuleBase(ABC): """!Main module class""" _modulesActive = [] diff --git a/plugin/pluginBase.py b/plugin/pluginBase.py index 12ad6ea..15042b7 100644 --- a/plugin/pluginBase.py +++ b/plugin/pluginBase.py @@ -16,13 +16,14 @@ """ import logging import time +from abc import ABC, abstractmethod from boswatch import wildcard logging.debug("- %s loaded", __name__) -class PluginBase: +class PluginBase(ABC): """!Main plugin class""" _pluginsActive = [] From ab0af5c61df5e1c7948df429eca7bee70723c038 Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 30 Oct 2019 11:38:38 +0100 Subject: [PATCH 69/70] removed unused import --- module/moduleBase.py | 2 +- plugin/pluginBase.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/moduleBase.py b/module/moduleBase.py index 1d9eafc..db3455f 100644 --- a/module/moduleBase.py +++ b/module/moduleBase.py @@ -16,7 +16,7 @@ """ import logging import time -from abc import ABC, abstractmethod +from abc import ABC from boswatch import wildcard diff --git a/plugin/pluginBase.py b/plugin/pluginBase.py index 15042b7..5785d69 100644 --- a/plugin/pluginBase.py +++ b/plugin/pluginBase.py @@ -16,7 +16,7 @@ """ import logging import time -from abc import ABC, abstractmethod +from abc import ABC from boswatch import wildcard From 2bb481111be81c5995586ba63f2120b0dbc6fa2c Mon Sep 17 00:00:00 2001 From: Bastian Schroll Date: Wed, 19 Feb 2020 14:20:56 +0100 Subject: [PATCH 70/70] edit templates for module and plugin --- module/template_module.py | 6 ++++-- plugin/template_plugin.py | 24 ++++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/module/template_module.py b/module/template_module.py index 7243101..2cb6eb1 100644 --- a/module/template_module.py +++ b/module/template_module.py @@ -32,7 +32,8 @@ class BoswatchModul(ModuleBase): 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): @@ -51,5 +52,6 @@ class BoswatchModul(ModuleBase): return bwPacket def onUnload(self): - """!Called by destruction of the plugin""" + """!Called by destruction of the plugin + Remove if not implemented""" pass diff --git a/plugin/template_plugin.py b/plugin/template_plugin.py index d981e9f..b3a7acc 100644 --- a/plugin/template_plugin.py +++ b/plugin/template_plugin.py @@ -32,41 +32,49 @@ class BoswatchPlugin(PluginBase): 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