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")