restore pocsag functionality

This commit is contained in:
Jakob Ketterl 2021-09-06 20:00:14 +02:00
parent b9f43654cd
commit 6014ce8921
10 changed files with 126 additions and 121 deletions

View File

@ -5,6 +5,8 @@ from owrx.aprs import Ax25Parser, AprsParser
from pycsdr.modules import Convert, FmDemod from pycsdr.modules import Convert, FmDemod
from pycsdr.types import Format from pycsdr.types import Format
from owrx.aprs.module import DirewolfModule from owrx.aprs.module import DirewolfModule
from digiham.modules import FskDemodulator, PocsagDecoder
from owrx.pocsag import PocsagParser
class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver): class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver):
@ -42,3 +44,20 @@ class PacketDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequency
def setDialFrequency(self, frequency: int) -> None: def setDialFrequency(self, frequency: int) -> None:
self.parser.setDialFrequency(frequency) 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

View File

@ -2,6 +2,9 @@ from pycsdr.modules import Module as BaseModule
from pycsdr.modules import Reader, Writer from pycsdr.modules import Reader, Writer
from pycsdr.types import Format from pycsdr.types import Format
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from threading import Thread
from io import BytesIO
import pickle
class Module(BaseModule, metaclass=ABCMeta): class Module(BaseModule, metaclass=ABCMeta):
@ -23,3 +26,57 @@ class Module(BaseModule, metaclass=ABCMeta):
@abstractmethod @abstractmethod
def getOutputFormat(self) -> Format: def getOutputFormat(self) -> Format:
pass 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

View File

@ -221,7 +221,7 @@ PacketMessagePanel.prototype.pushMessage = function(msg) {
$.fn.packetMessagePanel = function() { $.fn.packetMessagePanel = function() {
if (!this.data('panel')) { if (!this.data('panel')) {
this.data('panel', new PacketMessagePanel(this)); this.data('panel', new PacketMessagePanel(this));
}; }
return this.data('panel'); return this.data('panel');
}; };
@ -232,6 +232,10 @@ PocsagMessagePanel = function(el) {
PocsagMessagePanel.prototype = new MessagePanel(); PocsagMessagePanel.prototype = new MessagePanel();
PocsagMessagePanel.prototype.supportsMessage = function(message) {
return message['mode'] === 'Pocsag';
};
PocsagMessagePanel.prototype.render = function() { PocsagMessagePanel.prototype.render = function() {
$(this.el).append($( $(this.el).append($(
'<table>' + '<table>' +

View File

@ -848,7 +848,8 @@ function on_ws_recv(evt) {
var value = json['value']; var value = json['value'];
var panels = [ var panels = [
$("#openwebrx-panel-wsjt-message").wsjtMessagePanel(), $("#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 (!panels.some(function(panel) {
if (!panel.supportsMessage(value)) return false; if (!panel.supportsMessage(value)) return false;
@ -861,9 +862,6 @@ function on_ws_recv(evt) {
case 'log_message': case 'log_message':
divlog(json['value'], true); divlog(json['value'], true);
break; break;
case 'pocsag_data':
$('#openwebrx-panel-pocsag-message').pocsagMessagePanel().pushMessage(json['value']);
break;
case 'backoff': case 'backoff':
divlog("Server is currently busy: " + json['reason'], true); divlog("Server is currently busy: " + json['reason'], true);
var $overlay = $('#openwebrx-error-overlay'); var $overlay = $('#openwebrx-error-overlay');

View File

@ -2,14 +2,9 @@ from owrx.map import Map, LatLngLocation
from owrx.metrics import Metrics, CounterMetric from owrx.metrics import Metrics, CounterMetric
from owrx.bands import Bandplan from owrx.bands import Bandplan
from datetime import datetime, timezone from datetime import datetime, timezone
from csdr.module import Module from csdr.module import PickleModule
from pycsdr.modules import Reader
from pycsdr.types import Format
from threading import Thread
from io import BytesIO
import re import re
import logging import logging
import pickle
logger = logging.getLogger(__name__) 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} return {"symbol": symbol, "table": table, "index": ord(symbol) - 33, "tableindex": ord(table) - 33}
class Ax25Parser(Module, Thread): class Ax25Parser(PickleModule):
def __init__(self): def process(self, ax25frame):
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):
control_pid = ax25frame.find(bytes([0x03, 0xF0])) control_pid = ax25frame.find(bytes([0x03, 0xF0]))
if control_pid % 7 > 0: if control_pid % 7 > 0:
logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data") 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 return res
class AprsParser(Module, Thread): class AprsParser(PickleModule):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.metrics = {} self.metrics = {}
self.doRun = True
self.band = None self.band = None
def setDialFrequency(self, freq): def setDialFrequency(self, freq):
self.band = Bandplan.getSharedInstance().findBand(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): def getMetric(self, category):
if category not in self.metrics: if category not in self.metrics:
band = "unknown" band = "unknown"
@ -250,7 +182,7 @@ class AprsParser(Module, Thread):
return False return False
return True return True
def parse(self, data): def process(self, data):
try: try:
# TODO how can we tell if this is an APRS frame at all? # TODO how can we tell if this is an APRS frame at all?
aprsData = self.parseAprsData(data) aprsData = self.parseAprsData(data)

View File

@ -1,9 +1,6 @@
from pycsdr.modules import Reader from pycsdr.modules import Reader
from pycsdr.types import Format from pycsdr.types import Format
from csdr.module import Module from csdr.module import ThreadModule
from threading import Thread
import socket
import time
import pickle import pickle
import logging import logging
@ -16,11 +13,10 @@ TFEND = 0xDC
TFESC = 0xDD TFESC = 0xDD
class KissDeframer(Module, Thread): class KissDeframer(ThreadModule):
def __init__(self): def __init__(self):
self.escaped = False self.escaped = False
self.buf = bytearray() self.buf = bytearray()
self.doRun = True
super().__init__() super().__init__()
def getInputFormat(self) -> Format: def getInputFormat(self) -> Format:
@ -29,10 +25,6 @@ class KissDeframer(Module, Thread):
def getOutputFormat(self) -> Format: def getOutputFormat(self) -> Format:
return Format.CHAR return Format.CHAR
def setReader(self, reader: Reader) -> None:
super().setReader(reader)
self.start()
def run(self): def run(self):
while self.doRun: while self.doRun:
data = self.reader.read() data = self.reader.read()
@ -42,10 +34,6 @@ class KissDeframer(Module, Thread):
for frame in self.parse(data): for frame in self.parse(data):
self.writer.write(pickle.dumps(frame)) self.writer.write(pickle.dumps(frame))
def stop(self):
self.doRun = False
self.reader.stop()
def parse(self, input): def parse(self, input):
for b in input: for b in input:
if b == FESC: if b == FESC:

View File

@ -1,10 +1,9 @@
from owrx.modes import Modes, AudioChopperMode from owrx.modes import Modes, AudioChopperMode
from itertools import groupby from itertools import groupby
import threading
from owrx.audio import ProfileSourceSubscriber from owrx.audio import ProfileSourceSubscriber
from owrx.audio.wav import AudioWriter from owrx.audio.wav import AudioWriter
from owrx.audio.queue import QueueJob from owrx.audio.queue import QueueJob
from csdr.module import Module from csdr.module import ThreadModule
from pycsdr.types import Format from pycsdr.types import Format
import pickle import pickle
@ -14,7 +13,7 @@ logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
class AudioChopper(threading.Thread, Module, ProfileSourceSubscriber): class AudioChopper(ThreadModule, ProfileSourceSubscriber):
# TODO parser typing # TODO parser typing
def __init__(self, mode_str: str, parser): def __init__(self, mode_str: str, parser):
self.parser = 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)) raise ValueError("Mode {} is not an audio chopper mode".format(mode_str))
self.profile_source = mode.get_profile_source() self.profile_source = mode.get_profile_source()
super().__init__() super().__init__()
Module.__init__(self)
def getInputFormat(self) -> Format: def getInputFormat(self) -> Format:
return Format.SHORT return Format.SHORT
@ -49,14 +47,6 @@ class AudioChopper(threading.Thread, Module, ProfileSourceSubscriber):
w.start() w.start()
self.writers = writers self.writers = writers
def setReader(self, reader):
super().setReader(reader)
self.start()
def stop(self):
self.reader.stop()
super().stop()
def run(self) -> None: def run(self) -> None:
logger.debug("Audio chopper starting up") logger.debug("Audio chopper starting up")
self.setup_writers() self.setup_writers()

View File

@ -434,9 +434,6 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def write_sdr_error(self, message): def write_sdr_error(self, message):
self.send({"type": "sdr_error", "value": 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): def write_backoff_message(self, reason):
self.send({"type": "backoff", "reason": reason}) self.send({"type": "backoff", "reason": reason})

View File

@ -1,8 +1,6 @@
from owrx.meta import MetaParser from owrx.meta import MetaParser
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
from owrx.js8 import Js8Parser from owrx.js8 import Js8Parser
from owrx.aprs import AprsParser
from owrx.pocsag import PocsagParser
from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
from owrx.property import PropertyStack, PropertyLayer, PropertyValidator from owrx.property import PropertyStack, PropertyLayer, PropertyValidator
from owrx.property.validators import OrValidator, RegexValidator, BoolValidator 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.analog import NFm, WFm, Am, Ssb
from csdr.chain.digiham import DigihamChain, Dmr, Dstar, Nxdn, Ysf from csdr.chain.digiham import DigihamChain, Dmr, Dstar, Nxdn, Ysf
from csdr.chain.fft import FftChain 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.modules import Buffer, Writer
from pycsdr.types import Format from pycsdr.types import Format
from typing import Union from typing import Union
@ -283,7 +281,6 @@ class DspManager(Output, SdrSourceEventClient):
self.sdrSource = sdrSource self.sdrSource = sdrSource
self.parsers = { self.parsers = {
"meta": MetaParser(self.handler), "meta": MetaParser(self.handler),
"pocsag_demod": PocsagParser(self.handler),
"js8_demod": Js8Parser(self.handler), "js8_demod": Js8Parser(self.handler),
} }
@ -494,6 +491,8 @@ class DspManager(Output, SdrSourceEventClient):
return AudioChopperDemodulator(mod, WsjtParser()) return AudioChopperDemodulator(mod, WsjtParser())
elif mod == "packet": elif mod == "packet":
return PacketDemodulator() return PacketDemodulator()
elif mod == "pocsag":
return PocsagDemodulator()
return None return None
def setSecondaryDemodulator(self, mod): def setSecondaryDemodulator(self, mod):

View File

@ -1,17 +1,38 @@
from owrx.parser import Parser from csdr.module import ThreadModule
from pycsdr.types import Format
import pickle
import logging import logging
logger = logging.getLogger(__name__) 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): def parse(self, raw):
try: for line in raw.split(b"\n"):
fields = raw.decode("ascii", "replace").rstrip("\n").split(";") if not len(line):
meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""} continue
if "address" in meta: try:
meta["address"] = int(meta["address"]) fields = line.decode("ascii", "replace").split(";")
self.handler.write_pocsag_data(meta) meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""}
except Exception: if "address" in meta:
logger.exception("Exception while parsing Pocsag message") meta["address"] = int(meta["address"])
meta["mode"] = "Pocsag"
yield meta
except Exception:
logger.exception("Exception while parsing Pocsag message")