restore pocsag functionality
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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($( | ||||
|         '<table>' + | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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}) | ||||
|  | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jakob Ketterl
					Jakob Ketterl