From 6014ce89214d41c839817904606b83165aabcd32 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 6 Sep 2021 20:00:14 +0200 Subject: [PATCH] restore pocsag functionality --- csdr/chain/digimodes.py | 19 ++++++++++ csdr/module.py | 57 ++++++++++++++++++++++++++++ htdocs/lib/MessagePanel.js | 6 ++- htdocs/openwebrx.js | 6 +-- owrx/aprs/__init__.py | 78 +++----------------------------------- owrx/aprs/kiss.py | 16 +------- owrx/audio/chopper.py | 14 +------ owrx/connection.py | 3 -- owrx/dsp.py | 7 ++-- owrx/pocsag.py | 41 +++++++++++++++----- 10 files changed, 126 insertions(+), 121 deletions(-) diff --git a/csdr/chain/digimodes.py b/csdr/chain/digimodes.py index 5db5f88..1cadb3e 100644 --- a/csdr/chain/digimodes.py +++ b/csdr/chain/digimodes.py @@ -5,6 +5,8 @@ from owrx.aprs import Ax25Parser, AprsParser from pycsdr.modules import Convert, FmDemod from pycsdr.types import Format from owrx.aprs.module import DirewolfModule +from digiham.modules import FskDemodulator, PocsagDecoder +from owrx.pocsag import PocsagParser class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver): @@ -42,3 +44,20 @@ class PacketDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequency def setDialFrequency(self, frequency: int) -> None: self.parser.setDialFrequency(frequency) + + +class PocsagDemodulator(SecondaryDemodulator, FixedAudioRateChain): + def __init__(self): + workers = [ + FmDemod(), + FskDemodulator(samplesPerSymbol=40, invert=True), + PocsagDecoder(), + PocsagParser(), + ] + super().__init__(workers) + + def supportsSquelch(self) -> bool: + return False + + def getFixedAudioRate(self) -> int: + return 48000 diff --git a/csdr/module.py b/csdr/module.py index 04de7c3..b12d950 100644 --- a/csdr/module.py +++ b/csdr/module.py @@ -2,6 +2,9 @@ from pycsdr.modules import Module as BaseModule from pycsdr.modules import Reader, Writer from pycsdr.types import Format from abc import ABCMeta, abstractmethod +from threading import Thread +from io import BytesIO +import pickle class Module(BaseModule, metaclass=ABCMeta): @@ -23,3 +26,57 @@ class Module(BaseModule, metaclass=ABCMeta): @abstractmethod def getOutputFormat(self) -> Format: pass + + +class ThreadModule(Module, Thread, metaclass=ABCMeta): + def __init__(self): + self.doRun = True + super().__init__() + Thread.__init__(self) + + def _checkStart(self) -> None: + if self.reader is not None and self.writer is not None: + self.start() + + def setReader(self, reader: Reader) -> None: + super().setReader(reader) + self._checkStart() + + def setWriter(self, writer: Writer) -> None: + super().setWriter(writer) + self._checkStart() + + @abstractmethod + def run(self): + pass + + def stop(self): + self.doRun = False + self.reader.stop() + + +class PickleModule(ThreadModule): + def getInputFormat(self) -> Format: + return Format.CHAR + + def getOutputFormat(self) -> Format: + return Format.CHAR + + def run(self): + while self.doRun: + data = self.reader.read() + if data is None: + self.doRun = False + break + io = BytesIO(data.tobytes()) + try: + while True: + output = self.process(pickle.load(io)) + if output is not None: + self.writer.write(pickle.dumps(output)) + except EOFError: + pass + + @abstractmethod + def process(self, input): + pass diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js index cb4e406..1e7a75b 100644 --- a/htdocs/lib/MessagePanel.js +++ b/htdocs/lib/MessagePanel.js @@ -221,7 +221,7 @@ PacketMessagePanel.prototype.pushMessage = function(msg) { $.fn.packetMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new PacketMessagePanel(this)); - }; + } return this.data('panel'); }; @@ -232,6 +232,10 @@ PocsagMessagePanel = function(el) { PocsagMessagePanel.prototype = new MessagePanel(); +PocsagMessagePanel.prototype.supportsMessage = function(message) { + return message['mode'] === 'Pocsag'; +}; + PocsagMessagePanel.prototype.render = function() { $(this.el).append($( '' + diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 33ec9be..057177b 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -848,7 +848,8 @@ function on_ws_recv(evt) { var value = json['value']; var panels = [ $("#openwebrx-panel-wsjt-message").wsjtMessagePanel(), - $('#openwebrx-panel-packet-message').packetMessagePanel() + $('#openwebrx-panel-packet-message').packetMessagePanel(), + $('#openwebrx-panel-pocsag-message').pocsagMessagePanel() ]; if (!panels.some(function(panel) { if (!panel.supportsMessage(value)) return false; @@ -861,9 +862,6 @@ function on_ws_recv(evt) { case 'log_message': divlog(json['value'], true); break; - case 'pocsag_data': - $('#openwebrx-panel-pocsag-message').pocsagMessagePanel().pushMessage(json['value']); - break; case 'backoff': divlog("Server is currently busy: " + json['reason'], true); var $overlay = $('#openwebrx-error-overlay'); diff --git a/owrx/aprs/__init__.py b/owrx/aprs/__init__.py index 576b579..8001b72 100644 --- a/owrx/aprs/__init__.py +++ b/owrx/aprs/__init__.py @@ -2,14 +2,9 @@ from owrx.map import Map, LatLngLocation from owrx.metrics import Metrics, CounterMetric from owrx.bands import Bandplan from datetime import datetime, timezone -from csdr.module import Module -from pycsdr.modules import Reader -from pycsdr.types import Format -from threading import Thread -from io import BytesIO +from csdr.module import PickleModule import re import logging -import pickle logger = logging.getLogger(__name__) @@ -50,41 +45,8 @@ def getSymbolData(symbol, table): return {"symbol": symbol, "table": table, "index": ord(symbol) - 33, "tableindex": ord(table) - 33} -class Ax25Parser(Module, Thread): - def __init__(self): - self.doRun = True - super().__init__() - - def getInputFormat(self) -> Format: - return Format.CHAR - - def getOutputFormat(self) -> Format: - return Format.CHAR - - def setReader(self, reader: Reader) -> None: - super().setReader(reader) - self.start() - - def stop(self): - self.doRun = False - self.reader.stop() - - def run(self): - while self.doRun: - data = self.reader.read() - if data is None: - self.doRun = False - break - io = BytesIO(data.tobytes()) - try: - while True: - frame = self.parse(pickle.load(io)) - if frame is not None: - self.writer.write(pickle.dumps(frame)) - except EOFError: - pass - - def parse(self, ax25frame): +class Ax25Parser(PickleModule): + def process(self, ax25frame): control_pid = ax25frame.find(bytes([0x03, 0xF0])) if control_pid % 7 > 0: logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data") @@ -189,45 +151,15 @@ class AprsLocation(LatLngLocation): return res -class AprsParser(Module, Thread): +class AprsParser(PickleModule): def __init__(self): super().__init__() self.metrics = {} - self.doRun = True self.band = None def setDialFrequency(self, freq): self.band = Bandplan.getSharedInstance().findBand(freq) - def setReader(self, reader: Reader) -> None: - super().setReader(reader) - self.start() - - def getInputFormat(self) -> Format: - return Format.CHAR - - def getOutputFormat(self) -> Format: - return Format.CHAR - - def run(self): - while self.doRun: - data = self.reader.read() - if data is None: - self.doRun = False - break - io = BytesIO(data.tobytes()) - try: - while True: - frame = self.parse(pickle.load(io)) - if frame is not None: - self.writer.write(pickle.dumps(frame)) - except EOFError: - pass - - def stop(self): - self.doRun = False - self.reader.stop() - def getMetric(self, category): if category not in self.metrics: band = "unknown" @@ -250,7 +182,7 @@ class AprsParser(Module, Thread): return False return True - def parse(self, data): + def process(self, data): try: # TODO how can we tell if this is an APRS frame at all? aprsData = self.parseAprsData(data) diff --git a/owrx/aprs/kiss.py b/owrx/aprs/kiss.py index 415231a..e1b1ea6 100644 --- a/owrx/aprs/kiss.py +++ b/owrx/aprs/kiss.py @@ -1,9 +1,6 @@ from pycsdr.modules import Reader from pycsdr.types import Format -from csdr.module import Module -from threading import Thread -import socket -import time +from csdr.module import ThreadModule import pickle import logging @@ -16,11 +13,10 @@ TFEND = 0xDC TFESC = 0xDD -class KissDeframer(Module, Thread): +class KissDeframer(ThreadModule): def __init__(self): self.escaped = False self.buf = bytearray() - self.doRun = True super().__init__() def getInputFormat(self) -> Format: @@ -29,10 +25,6 @@ class KissDeframer(Module, Thread): def getOutputFormat(self) -> Format: return Format.CHAR - def setReader(self, reader: Reader) -> None: - super().setReader(reader) - self.start() - def run(self): while self.doRun: data = self.reader.read() @@ -42,10 +34,6 @@ class KissDeframer(Module, Thread): for frame in self.parse(data): self.writer.write(pickle.dumps(frame)) - def stop(self): - self.doRun = False - self.reader.stop() - def parse(self, input): for b in input: if b == FESC: diff --git a/owrx/audio/chopper.py b/owrx/audio/chopper.py index 41d20d2..bd0cbd4 100644 --- a/owrx/audio/chopper.py +++ b/owrx/audio/chopper.py @@ -1,10 +1,9 @@ from owrx.modes import Modes, AudioChopperMode from itertools import groupby -import threading from owrx.audio import ProfileSourceSubscriber from owrx.audio.wav import AudioWriter from owrx.audio.queue import QueueJob -from csdr.module import Module +from csdr.module import ThreadModule from pycsdr.types import Format import pickle @@ -14,7 +13,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -class AudioChopper(threading.Thread, Module, ProfileSourceSubscriber): +class AudioChopper(ThreadModule, ProfileSourceSubscriber): # TODO parser typing def __init__(self, mode_str: str, parser): self.parser = parser @@ -26,7 +25,6 @@ class AudioChopper(threading.Thread, Module, ProfileSourceSubscriber): raise ValueError("Mode {} is not an audio chopper mode".format(mode_str)) self.profile_source = mode.get_profile_source() super().__init__() - Module.__init__(self) def getInputFormat(self) -> Format: return Format.SHORT @@ -49,14 +47,6 @@ class AudioChopper(threading.Thread, Module, ProfileSourceSubscriber): w.start() self.writers = writers - def setReader(self, reader): - super().setReader(reader) - self.start() - - def stop(self): - self.reader.stop() - super().stop() - def run(self) -> None: logger.debug("Audio chopper starting up") self.setup_writers() diff --git a/owrx/connection.py b/owrx/connection.py index be7c38f..47d8aa5 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -434,9 +434,6 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient): def write_sdr_error(self, message): self.send({"type": "sdr_error", "value": message}) - def write_pocsag_data(self, data): - self.send({"type": "pocsag_data", "value": data}) - def write_backoff_message(self, reason): self.send({"type": "backoff", "reason": reason}) diff --git a/owrx/dsp.py b/owrx/dsp.py index 5c6a332..eb46a1a 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -1,8 +1,6 @@ from owrx.meta import MetaParser from owrx.wsjt import WsjtParser from owrx.js8 import Js8Parser -from owrx.aprs import AprsParser -from owrx.pocsag import PocsagParser from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass from owrx.property import PropertyStack, PropertyLayer, PropertyValidator from owrx.property.validators import OrValidator, RegexValidator, BoolValidator @@ -15,7 +13,7 @@ from csdr.chain.clientaudio import ClientAudioChain from csdr.chain.analog import NFm, WFm, Am, Ssb from csdr.chain.digiham import DigihamChain, Dmr, Dstar, Nxdn, Ysf from csdr.chain.fft import FftChain -from csdr.chain.digimodes import AudioChopperDemodulator, PacketDemodulator +from csdr.chain.digimodes import AudioChopperDemodulator, PacketDemodulator, PocsagDemodulator from pycsdr.modules import Buffer, Writer from pycsdr.types import Format from typing import Union @@ -283,7 +281,6 @@ class DspManager(Output, SdrSourceEventClient): self.sdrSource = sdrSource self.parsers = { "meta": MetaParser(self.handler), - "pocsag_demod": PocsagParser(self.handler), "js8_demod": Js8Parser(self.handler), } @@ -494,6 +491,8 @@ class DspManager(Output, SdrSourceEventClient): return AudioChopperDemodulator(mod, WsjtParser()) elif mod == "packet": return PacketDemodulator() + elif mod == "pocsag": + return PocsagDemodulator() return None def setSecondaryDemodulator(self, mod): diff --git a/owrx/pocsag.py b/owrx/pocsag.py index c265146..0317fff 100644 --- a/owrx/pocsag.py +++ b/owrx/pocsag.py @@ -1,17 +1,38 @@ -from owrx.parser import Parser +from csdr.module import ThreadModule +from pycsdr.types import Format +import pickle import logging logger = logging.getLogger(__name__) -class PocsagParser(Parser): +class PocsagParser(ThreadModule): + def getInputFormat(self) -> Format: + return Format.CHAR + + def getOutputFormat(self) -> Format: + return Format.CHAR + + def run(self): + while self.doRun: + data = self.reader.read() + if data is None: + self.doRun = False + else: + for frame in self.parse(data.tobytes()): + self.writer.write(pickle.dumps(frame)) + def parse(self, raw): - try: - fields = raw.decode("ascii", "replace").rstrip("\n").split(";") - meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""} - if "address" in meta: - meta["address"] = int(meta["address"]) - self.handler.write_pocsag_data(meta) - except Exception: - logger.exception("Exception while parsing Pocsag message") + for line in raw.split(b"\n"): + if not len(line): + continue + try: + fields = line.decode("ascii", "replace").split(";") + meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""} + if "address" in meta: + meta["address"] = int(meta["address"]) + meta["mode"] = "Pocsag" + yield meta + except Exception: + logger.exception("Exception while parsing Pocsag message")