diff --git a/csdr/__init__.py b/csdr/__init__.py
index 1737c11..5e05ad9 100644
--- a/csdr/__init__.py
+++ b/csdr/__init__.py
@@ -30,7 +30,6 @@ from functools import partial
from csdr.output import Output
-from owrx.aprs.kiss import KissClient
from owrx.aprs.direwolf import DirewolfConfig, DirewolfConfigSubscriber
from owrx.audio.chopper import AudioChopper
diff --git a/csdr/chain/__init__.py b/csdr/chain/__init__.py
index 21b69c9..f4c14e8 100644
--- a/csdr/chain/__init__.py
+++ b/csdr/chain/__init__.py
@@ -1,5 +1,6 @@
from csdr.module import Module
from pycsdr.modules import Buffer
+from pycsdr.types import Format
from typing import Union, Callable
@@ -133,7 +134,13 @@ class Chain(Module):
self.clientReader.stop()
self.clientReader = None
- def getOutputFormat(self):
+ def getInputFormat(self) -> Format:
+ if self.workers:
+ return self.workers[0].getInputFormat()
+ else:
+ raise BufferError("getInputFormat on empty chain")
+
+ def getOutputFormat(self) -> Format:
if self.workers:
return self.workers[-1].getOutputFormat()
else:
diff --git a/csdr/chain/analog.py b/csdr/chain/analog.py
index c890343..c065193 100644
--- a/csdr/chain/analog.py
+++ b/csdr/chain/analog.py
@@ -19,6 +19,7 @@ class Am(BaseDemodulatorChain):
class NFm(BaseDemodulatorChain):
def __init__(self, sampleRate: int):
+ self.sampleRate = sampleRate
agc = Agc(Format.FLOAT)
agc.setProfile(AgcProfile.SLOW)
agc.setMaxGain(3)
@@ -30,6 +31,12 @@ class NFm(BaseDemodulatorChain):
]
super().__init__(workers)
+ def setSampleRate(self, sampleRate: int) -> None:
+ if sampleRate == self.sampleRate:
+ return
+ self.sampleRate = sampleRate
+ self.replace(2, NfmDeemphasis(sampleRate))
+
class WFm(BaseDemodulatorChain, FixedIfSampleRateChain, HdAudio):
def __init__(self, sampleRate: int, tau: float):
diff --git a/csdr/chain/demodulator.py b/csdr/chain/demodulator.py
index 0a8ab16..ceb6bdb 100644
--- a/csdr/chain/demodulator.py
+++ b/csdr/chain/demodulator.py
@@ -6,9 +6,13 @@ class BaseDemodulatorChain(Chain):
def supportsSquelch(self) -> bool:
return True
+ def setSampleRate(self, sampleRate: int) -> None:
+ pass
+
class SecondaryDemodulator(Chain):
- pass
+ def supportsSquelch(self) -> bool:
+ return True
class FixedAudioRateChain(ABC):
diff --git a/csdr/chain/digimodes.py b/csdr/chain/digimodes.py
index b3f4b3c..5db5f88 100644
--- a/csdr/chain/digimodes.py
+++ b/csdr/chain/digimodes.py
@@ -1,7 +1,10 @@
from csdr.chain.demodulator import SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver
from owrx.audio.chopper import AudioChopper
-from pycsdr.modules import Agc, Convert
+from owrx.aprs.kiss import KissDeframer
+from owrx.aprs import Ax25Parser, AprsParser
+from pycsdr.modules import Convert, FmDemod
from pycsdr.types import Format
+from owrx.aprs.module import DirewolfModule
class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver):
@@ -16,3 +19,26 @@ class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFre
def setDialFrequency(self, frequency: int) -> None:
self.chopper.setDialFrequency(frequency)
+
+
+class PacketDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver):
+ def __init__(self, service: bool = False):
+ self.parser = AprsParser()
+ workers = [
+ FmDemod(),
+ Convert(Format.FLOAT, Format.SHORT),
+ DirewolfModule(service=service),
+ KissDeframer(),
+ Ax25Parser(),
+ self.parser,
+ ]
+ super().__init__(workers)
+
+ def supportsSquelch(self) -> bool:
+ return False
+
+ def getFixedAudioRate(self) -> int:
+ return 48000
+
+ def setDialFrequency(self, frequency: int) -> None:
+ self.parser.setDialFrequency(frequency)
diff --git a/csdr/chain/selector.py b/csdr/chain/selector.py
index 2882603..ac651d5 100644
--- a/csdr/chain/selector.py
+++ b/csdr/chain/selector.py
@@ -62,7 +62,7 @@ class Decimator(Chain):
class Selector(Chain):
- def __init__(self, inputRate: int, outputRate: int, shiftRate: float):
+ def __init__(self, inputRate: int, outputRate: int, shiftRate: float, withSquelch: bool = True):
self.outputRate = outputRate
self.shift = Shift(shiftRate)
@@ -73,12 +73,14 @@ class Selector(Chain):
self.bandpassCutoffs = None
self.setBandpass(-4000, 4000)
- self.readings_per_second = 4
- # s-meter readings are available every 1024 samples
- # the reporting interval is measured in those 1024-sample blocks
- self.squelch = Squelch(5, int(outputRate / (self.readings_per_second * 1024)))
+ workers = [self.shift, self.decimation, self.bandpass]
- workers = [self.shift, self.decimation, self.bandpass, self.squelch]
+ if withSquelch:
+ self.readings_per_second = 4
+ # s-meter readings are available every 1024 samples
+ # the reporting interval is measured in those 1024-sample blocks
+ self.squelch = Squelch(5, int(outputRate / (self.readings_per_second * 1024)))
+ workers += [self.squelch]
super().__init__(workers)
diff --git a/csdr/module.py b/csdr/module.py
index 5783cce..04de7c3 100644
--- a/csdr/module.py
+++ b/csdr/module.py
@@ -1,14 +1,25 @@
-import pycsdr.modules
+from pycsdr.modules import Module as BaseModule
+from pycsdr.modules import Reader, Writer
+from pycsdr.types import Format
+from abc import ABCMeta, abstractmethod
-class Module(pycsdr.modules.Module):
+class Module(BaseModule, metaclass=ABCMeta):
def __init__(self):
self.reader = None
self.writer = None
super().__init__()
- def setReader(self, reader: pycsdr.modules.Reader) -> None:
+ def setReader(self, reader: Reader) -> None:
self.reader = reader
- def setWriter(self, writer: pycsdr.modules.Writer) -> None:
+ def setWriter(self, writer: Writer) -> None:
self.writer = writer
+
+ @abstractmethod
+ def getInputFormat(self) -> Format:
+ pass
+
+ @abstractmethod
+ def getOutputFormat(self) -> Format:
+ pass
diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js
index 2c1a868..cb4e406 100644
--- a/htdocs/lib/MessagePanel.js
+++ b/htdocs/lib/MessagePanel.js
@@ -4,6 +4,10 @@ function MessagePanel(el) {
this.initClearButton();
}
+MessagePanel.prototype.supportsMessage = function(message) {
+ return false;
+};
+
MessagePanel.prototype.render = function() {
};
@@ -46,10 +50,17 @@ MessagePanel.prototype.initClearButton = function() {
function WsjtMessagePanel(el) {
MessagePanel.call(this, el);
this.initClearTimer();
+ this.qsoModes = ['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65'];
+ this.beaconModes = ['WSPR', 'FST4W'];
+ this.modes = [].concat(this.qsoModes, this.beaconModes);
}
WsjtMessagePanel.prototype = new MessagePanel();
+WsjtMessagePanel.prototype.supportsMessage = function(message) {
+ return this.modes.indexOf(message['mode']) >= 0;
+};
+
WsjtMessagePanel.prototype.render = function() {
$(this.el).append($(
'
' +
@@ -78,14 +89,14 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) {
return $('').text(input).html()
};
- if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65'].indexOf(msg['mode']) >= 0) {
+ if (this.qsoModes.indexOf(msg['mode']) >= 0) {
matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/);
if (matches && matches[2] !== 'RR73') {
linkedmsg = html_escape(matches[1]) + '' + matches[2] + '';
} else {
linkedmsg = html_escape(linkedmsg);
}
- } else if (['WSPR', 'FST4W'].indexOf(msg['mode']) >= 0) {
+ } else if (this.beaconModes.indexOf(msg['mode']) >= 0) {
matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/);
if (matches) {
linkedmsg = html_escape(matches[1]) + '' + matches[2] + '' + html_escape(matches[3]);
@@ -108,7 +119,7 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) {
$.fn.wsjtMessagePanel = function(){
if (!this.data('panel')) {
this.data('panel', new WsjtMessagePanel(this));
- };
+ }
return this.data('panel');
};
@@ -119,6 +130,10 @@ function PacketMessagePanel(el) {
PacketMessagePanel.prototype = new MessagePanel();
+PacketMessagePanel.prototype.supportsMessage = function(message) {
+ return message['mode'] === 'APRS';
+};
+
PacketMessagePanel.prototype.render = function() {
$(this.el).append($(
'' +
@@ -243,6 +258,6 @@ PocsagMessagePanel.prototype.pushMessage = function(msg) {
$.fn.pocsagMessagePanel = function() {
if (!this.data('panel')) {
this.data('panel', new PocsagMessagePanel(this));
- };
+ }
return this.data('panel');
};
\ No newline at end of file
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js
index 39f48d4..33ec9be 100644
--- a/htdocs/openwebrx.js
+++ b/htdocs/openwebrx.js
@@ -824,9 +824,6 @@ function on_ws_recv(evt) {
case "js8_message":
$("#openwebrx-panel-js8-message").js8().pushMessage(json['value']);
break;
- case "wsjt_message":
- $("#openwebrx-panel-wsjt-message").wsjtMessagePanel().pushMessage(json['value']);
- break;
case "dial_frequencies":
var as_bookmarks = json['value'].map(function (d) {
return {
@@ -837,9 +834,6 @@ function on_ws_recv(evt) {
});
bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies');
break;
- case "aprs_data":
- $('#openwebrx-panel-packet-message').packetMessagePanel().pushMessage(json['value']);
- break;
case "bookmarks":
bookmarks.replace_bookmarks(json['value'], "server");
break;
@@ -851,7 +845,18 @@ function on_ws_recv(evt) {
$("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator();
break;
case 'secondary_demod':
- secondary_demod_push_data(json['value']);
+ var value = json['value'];
+ var panels = [
+ $("#openwebrx-panel-wsjt-message").wsjtMessagePanel(),
+ $('#openwebrx-panel-packet-message').packetMessagePanel()
+ ];
+ if (!panels.some(function(panel) {
+ if (!panel.supportsMessage(value)) return false;
+ panel.pushMessage(value);
+ return true;
+ })) {
+ secondary_demod_push_data(value);
+ }
break;
case 'log_message':
divlog(json['value'], true);
diff --git a/owrx/aprs/__init__.py b/owrx/aprs/__init__.py
index a4c55ae..576b579 100644
--- a/owrx/aprs/__init__.py
+++ b/owrx/aprs/__init__.py
@@ -1,10 +1,15 @@
-from owrx.aprs.kiss import KissDeframer
from owrx.map import Map, LatLngLocation
from owrx.metrics import Metrics, CounterMetric
-from owrx.parser import Parser
+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
import re
import logging
+import pickle
logger = logging.getLogger(__name__)
@@ -45,7 +50,40 @@ def getSymbolData(symbol, table):
return {"symbol": symbol, "table": table, "index": ord(symbol) - 33, "tableindex": ord(table) - 33}
-class Ax25Parser(object):
+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):
control_pid = ax25frame.find(bytes([0x03, 0xF0]))
if control_pid % 7 > 0:
@@ -54,7 +92,7 @@ class Ax25Parser(object):
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
- yield l[i : i + n]
+ yield l[i:i + n]
return {
"destination": self.extractCallsign(ax25frame[0:7]),
@@ -117,9 +155,9 @@ class WeatherParser(object):
WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4),
]
- def __init__(self, data, weather={}):
+ def __init__(self, data, weather=None):
self.data = data
- self.weather = weather
+ self.weather = {} if weather is None else weather
def getWeather(self):
doWork = True
@@ -151,16 +189,44 @@ class AprsLocation(LatLngLocation):
return res
-class AprsParser(Parser):
- def __init__(self, handler):
- super().__init__(handler)
- self.ax25parser = Ax25Parser()
- self.deframer = KissDeframer()
+class AprsParser(Module, Thread):
+ def __init__(self):
+ super().__init__()
self.metrics = {}
+ self.doRun = True
+ self.band = None
def setDialFrequency(self, freq):
- super().setDialFrequency(freq)
- self.metrics = {}
+ 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:
@@ -184,22 +250,22 @@ class AprsParser(Parser):
return False
return True
- def parse(self, raw):
- for frame in self.deframer.parse(raw):
- try:
- data = self.ax25parser.parse(frame)
+ def parse(self, data):
+ try:
+ # TODO how can we tell if this is an APRS frame at all?
+ aprsData = self.parseAprsData(data)
- # TODO how can we tell if this is an APRS frame at all?
- aprsData = self.parseAprsData(data)
+ logger.debug("decoded APRS data: %s", aprsData)
+ self.updateMap(aprsData)
+ self.getMetric("total").inc()
+ if self.isDirect(aprsData):
+ self.getMetric("direct").inc()
- logger.debug("decoded APRS data: %s", aprsData)
- self.updateMap(aprsData)
- self.getMetric("total").inc()
- if self.isDirect(aprsData):
- self.getMetric("direct").inc()
- self.handler.write_aprs_data(aprsData)
- except Exception:
- logger.exception("exception while parsing aprs data")
+ # the frontend uses this to distinguis hessages from the different parsers
+ aprsData["mode"] = "APRS"
+ return aprsData
+ except Exception:
+ logger.exception("exception while parsing aprs data")
def updateMap(self, mapData):
if "type" in mapData and mapData["type"] == "thirdparty" and "data" in mapData:
diff --git a/owrx/aprs/kiss.py b/owrx/aprs/kiss.py
index b26c1fb..415231a 100644
--- a/owrx/aprs/kiss.py
+++ b/owrx/aprs/kiss.py
@@ -1,5 +1,10 @@
+from pycsdr.modules import Reader
+from pycsdr.types import Format
+from csdr.module import Module
+from threading import Thread
import socket
import time
+import pickle
import logging
@@ -11,33 +16,37 @@ TFEND = 0xDC
TFESC = 0xDD
-class KissClient(object):
- def __init__(self, port):
- delay = 0.5
- retries = 0
- while True:
- try:
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.socket.connect(("localhost", port))
- break
- except ConnectionError:
- if retries > 20:
- logger.error("maximum number of connection attempts reached. did direwolf start up correctly?")
- raise
- retries += 1
- time.sleep(delay)
-
- def read(self):
- return self.socket.recv(1)
-
-
-class KissDeframer(object):
+class KissDeframer(Module, Thread):
def __init__(self):
self.escaped = False
self.buf = bytearray()
+ 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 run(self):
+ while self.doRun:
+ data = self.reader.read()
+ if data is None:
+ self.doRun = False
+ else:
+ 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):
- frames = []
for b in input:
if b == FESC:
self.escaped = True
@@ -49,11 +58,10 @@ class KissDeframer(object):
else:
logger.warning("invalid escape char: %s", str(input[0]))
self.escaped = False
- elif input[0] == FEND:
+ elif b == FEND:
# data frames start with 0x00
if len(self.buf) > 1 and self.buf[0] == 0x00:
- frames += [self.buf[1:]]
+ yield self.buf[1:]
self.buf = bytearray()
else:
self.buf.append(b)
- return frames
diff --git a/owrx/aprs/module.py b/owrx/aprs/module.py
new file mode 100644
index 0000000..5e559b0
--- /dev/null
+++ b/owrx/aprs/module.py
@@ -0,0 +1,94 @@
+from csdr.module import Module
+from pycsdr.types import Format
+from pycsdr.modules import Reader, Writer, TcpSource
+from subprocess import Popen, PIPE
+from owrx.aprs.direwolf import DirewolfConfig
+from owrx.config.core import CoreConfig
+import threading
+import time
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class DirewolfModule(Module):
+ def __init__(self, service: bool = False):
+ self.process = None
+ self.inputReader = None
+ self.tcpSource = None
+ self.service = service
+ super().__init__()
+
+ def setReader(self, reader: Reader) -> None:
+ super().setReader(reader)
+ self.start()
+
+ def setWriter(self, writer: Writer) -> None:
+ super().setWriter(writer)
+ if self.tcpSource is not None:
+ self.tcpSource.setWriter(writer)
+
+ def getInputFormat(self) -> Format:
+ return Format.SHORT
+
+ def getOutputFormat(self) -> Format:
+ return Format.CHAR
+
+ def start(self):
+ temporary_directory = CoreConfig().get_temporary_directory()
+ direwolf_config_path = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
+ tmp_dir=temporary_directory, myid=id(self)
+ )
+ direwolf_config = DirewolfConfig()
+ # TODO
+ # direwolf_config.wire(self)
+
+ file = open(direwolf_config_path, "w")
+ file.write(direwolf_config.getConfig(self.service))
+ file.close()
+
+ # direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h 1>&2
+ self.process = Popen(
+ ["direwolf", "-c", direwolf_config_path, "-r", "48000", "-t", "0", "-q", "d", "-q", "h"],
+ start_new_session=True,
+ stdin=PIPE,
+ )
+
+ threading.Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start()
+
+ delay = 0.5
+ retries = 0
+ while True:
+ try:
+ self.tcpSource = TcpSource(direwolf_config.getPort(), Format.CHAR)
+ if self.writer:
+ self.tcpSource.setWriter(self.writer)
+ break
+ except ConnectionError:
+ if retries > 20:
+ logger.error("maximum number of connection attempts reached. did direwolf start up correctly?")
+ raise
+ retries += 1
+ time.sleep(delay)
+
+ def stop(self):
+ if self.process is not None:
+ self.process.terminate()
+ self.process.wait()
+ self.process = None
+ self.reader.stop()
+
+ def pump(self, read, write):
+ def copy():
+ while True:
+ data = None
+ try:
+ data = read()
+ except ValueError:
+ pass
+ if data is None or isinstance(data, bytes) and len(data) == 0:
+ break
+ write(data)
+
+ return copy
diff --git a/owrx/audio/chopper.py b/owrx/audio/chopper.py
index d9b7508..41d20d2 100644
--- a/owrx/audio/chopper.py
+++ b/owrx/audio/chopper.py
@@ -5,6 +5,7 @@ from owrx.audio import ProfileSourceSubscriber
from owrx.audio.wav import AudioWriter
from owrx.audio.queue import QueueJob
from csdr.module import Module
+from pycsdr.types import Format
import pickle
import logging
@@ -27,6 +28,12 @@ class AudioChopper(threading.Thread, Module, ProfileSourceSubscriber):
super().__init__()
Module.__init__(self)
+ def getInputFormat(self) -> Format:
+ return Format.SHORT
+
+ def getOutputFormat(self) -> Format:
+ return Format.CHAR
+
def stop_writers(self):
while self.writers:
self.writers.pop().stop()
diff --git a/owrx/connection.py b/owrx/connection.py
index 41b0e08..be7c38f 100644
--- a/owrx/connection.py
+++ b/owrx/connection.py
@@ -399,9 +399,13 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def write_secondary_fft(self, data):
self.send(bytes([0x03]) + data)
- def write_secondary_demod(self, data):
- message = data.decode("ascii", "replace")
- self.send({"type": "secondary_demod", "value": message})
+ def write_secondary_demod(self, message):
+ io = BytesIO(message.tobytes())
+ try:
+ while True:
+ self.send({"type": "secondary_demod", "value": pickle.load(io)})
+ except EOFError:
+ pass
def write_secondary_dsp_config(self, cfg):
self.send({"type": "secondary_config", "value": cfg})
@@ -418,23 +422,12 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def write_metadata(self, metadata):
self.send({"type": "metadata", "value": metadata})
- def write_wsjt_message(self, message):
- io = BytesIO(message.tobytes())
- try:
- while True:
- self.send({"type": "wsjt_message", "value": pickle.load(io)})
- except EOFError:
- pass
-
def write_dial_frequencies(self, frequencies):
self.send({"type": "dial_frequencies", "value": frequencies})
def write_bookmarks(self, bookmarks):
self.send({"type": "bookmarks", "value": bookmarks})
- def write_aprs_data(self, data):
- self.send({"type": "aprs_data", "value": data})
-
def write_log_message(self, message):
self.send({"type": "log_message", "value": message})
diff --git a/owrx/dsp.py b/owrx/dsp.py
index f6d835a..5c6a332 100644
--- a/owrx/dsp.py
+++ b/owrx/dsp.py
@@ -15,7 +15,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
+from csdr.chain.digimodes import AudioChopperDemodulator, PacketDemodulator
from pycsdr.modules import Buffer, Writer
from pycsdr.types import Format
from typing import Union
@@ -66,7 +66,7 @@ class ClientDemodulatorChain(Chain):
format = w1.getOutputFormat()
if self.audioBuffer is None or self.audioBuffer.getFormat() != format:
self.audioBuffer = Buffer(format)
- if self.secondaryDemodulator is not None:
+ if self.secondaryDemodulator is not None and self.secondaryDemodulator.getInputFormat() is not Format.COMPLEX_FLOAT:
self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
super()._connect(w1, w2, self.audioBuffer)
else:
@@ -94,28 +94,33 @@ class ClientDemodulatorChain(Chain):
if isinstance(self.demodulator, FixedIfSampleRateChain):
self.selector.setOutputRate(self.demodulator.getFixedIfSampleRate())
- elif self.secondaryDemodulator is not None and isinstance(self.secondaryDemodulator, FixedAudioRateChain):
+ elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
self.selector.setOutputRate(self.secondaryDemodulator.getFixedAudioRate())
else:
self.selector.setOutputRate(outputRate)
+ self.demodulator.setSampleRate(outputRate)
if isinstance(self.demodulator, FixedAudioRateChain):
self.clientAudioChain.setInputRate(self.demodulator.getFixedAudioRate())
- elif self.secondaryDemodulator is not None and isinstance(self.secondaryDemodulator, FixedAudioRateChain):
+ elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
self.clientAudioChain.setInputRate(self.secondaryDemodulator.getFixedAudioRate())
else:
self.clientAudioChain.setInputRate(outputRate)
- if not demodulator.supportsSquelch():
- self.selector.setSquelchLevel(-150)
- else:
- self.selector.setSquelchLevel(self.squelchLevel)
+ self._syncSquelch()
self.clientAudioChain.setClientRate(outputRate)
if self.metaWriter is not None and isinstance(demodulator, DigihamChain):
demodulator.setMetaWriter(self.metaWriter)
+ def _getSelectorOutputRate(self):
+ if isinstance(self.secondaryDemodulator, FixedAudioRateChain):
+ if isinstance(self.demodulator, FixedAudioRateChain) and self.demodulator.getFixedAudioRate() != self.secondaryDemodulator.getFixedAudioRate():
+ raise ValueError("secondary and primary demodulator chain audio rates do not match!")
+ return self.secondaryDemodulator.getFixedAudioRate()
+ return self.outputRate
+
def setSecondaryDemodulator(self, demod: Union[SecondaryDemodulator, None]):
if demod is self.secondaryDemodulator:
return
@@ -125,19 +130,18 @@ class ClientDemodulatorChain(Chain):
self.secondaryDemodulator = demod
- if self.secondaryDemodulator is not None and isinstance(self.secondaryDemodulator, FixedAudioRateChain):
- if isinstance(self.demodulator, FixedAudioRateChain) and self.demodulator.getFixedAudioRate() != self.secondaryDemodulator.getFixedAudioRate():
- raise ValueError("secondary and primary demodulator chain audio rates do not match!")
- else:
- rate = self.secondaryDemodulator.getFixedAudioRate()
- else:
- rate = self.outputRate
+ rate = self._getSelectorOutputRate()
self.selector.setOutputRate(rate)
self.clientAudioChain.setInputRate(rate)
+ self.demodulator.setSampleRate(rate)
self._updateDialFrequency()
+ self._syncSquelch()
if self.secondaryDemodulator is not None:
- self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
+ if self.secondaryDemodulator.getInputFormat() is Format.COMPLEX_FLOAT:
+ self.secondaryDemodulator.setReader(self.selectorBuffer.getReader())
+ else:
+ self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
self.secondaryDemodulator.setWriter(self.secondaryWriter)
if self.secondaryDemodulator is None and self.secondaryFftChain is not None:
@@ -146,10 +150,19 @@ class ClientDemodulatorChain(Chain):
if self.secondaryDemodulator is not None and self.secondaryFftChain is None:
# TODO eliminate constants
- self.secondaryFftChain = FftChain(self.outputRate, 2048, 0.3, 9, "adpcm")
+ self.secondaryFftChain = FftChain(self._getSelectorOutputRate(), 2048, 0.3, 9, "adpcm")
self.secondaryFftChain.setReader(self.selectorBuffer.getReader())
self.secondaryFftChain.setWriter(self.secondaryFftWriter)
+ if self.secondaryFftChain is not None:
+ self.secondaryFftChain.setSampleRate(rate)
+
+ def _syncSquelch(self):
+ if not self.demodulator.supportsSquelch() or (self.secondaryDemodulator is not None and not self.secondaryDemodulator.supportsSquelch()):
+ self.selector.setSquelchLevel(-150)
+ else:
+ self.selector.setSquelchLevel(self.squelchLevel)
+
def setLowCut(self, lowCut):
self.selector.setLowCut(lowCut)
@@ -189,9 +202,7 @@ class ClientDemodulatorChain(Chain):
if level == self.squelchLevel:
return
self.squelchLevel = level
- if not self.demodulator.supportsSquelch():
- return
- self.selector.setSquelchLevel(level)
+ self._syncSquelch()
def setOutputRate(self, outputRate) -> None:
if outputRate == self.outputRate:
@@ -203,6 +214,7 @@ class ClientDemodulatorChain(Chain):
return
if not isinstance(self.demodulator, FixedIfSampleRateChain):
self.selector.setOutputRate(outputRate)
+ self.demodulator.setSampleRate(outputRate)
if not isinstance(self.demodulator, FixedAudioRateChain):
self.clientAudioChain.setClientRate(outputRate)
@@ -271,7 +283,6 @@ class DspManager(Output, SdrSourceEventClient):
self.sdrSource = sdrSource
self.parsers = {
"meta": MetaParser(self.handler),
- "packet_demod": AprsParser(self.handler),
"pocsag_demod": PocsagParser(self.handler),
"js8_demod": Js8Parser(self.handler),
}
@@ -354,7 +365,7 @@ class DspManager(Output, SdrSourceEventClient):
buffer = Buffer(Format.CHAR)
self.chain.setSecondaryWriter(buffer)
# TODO there's multiple outputs depending on the modulation right now
- self.wireOutput("wsjt_demod", buffer)
+ self.wireOutput("secondary_demod", buffer)
def set_dial_freq(changes):
if (
@@ -481,6 +492,8 @@ class DspManager(Output, SdrSourceEventClient):
# TODO add remaining modes
if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]:
return AudioChopperDemodulator(mod, WsjtParser())
+ elif mod == "packet":
+ return PacketDemodulator()
return None
def setSecondaryDemodulator(self, mod):
@@ -520,7 +533,6 @@ class DspManager(Output, SdrSourceEventClient):
"smeter": self.handler.write_s_meter_level,
"secondary_fft": self.handler.write_secondary_fft,
"secondary_demod": self.handler.write_secondary_demod,
- "wsjt_demod": self.handler.write_wsjt_message,
}
for demod, parser in self.parsers.items():
writers[demod] = parser.parse
diff --git a/owrx/meta.py b/owrx/meta.py
index d747e28..f8f43bc 100644
--- a/owrx/meta.py
+++ b/owrx/meta.py
@@ -140,8 +140,7 @@ class DStarEnricher(Enricher):
try:
# we can send the DPRS stuff through our APRS parser to extract the information
# TODO: only third-party parsing accepts this format right now
- # TODO: we also need to pass a handler, which is not needed
- parser = AprsParser(None)
+ parser = AprsParser()
dprsData = parser.parseThirdpartyAprsData(meta["dprs"])
if "data" in dprsData:
data = dprsData["data"]
diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py
index 0b589f3..930dc39 100644
--- a/owrx/service/__init__.py
+++ b/owrx/service/__init__.py
@@ -2,68 +2,25 @@ import threading
from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
from owrx.sdr import SdrService
from owrx.bands import Bandplan
-from csdr.output import Output
from owrx.wsjt import WsjtParser
-from owrx.aprs import AprsParser
-from owrx.js8 import Js8Parser
from owrx.config import Config
from owrx.source.resampler import Resampler
from owrx.property import PropertyLayer, PropertyDeleted
from js8py import Js8Frame
-from abc import ABCMeta, abstractmethod
from owrx.service.schedule import ServiceScheduler
from owrx.service.chain import ServiceDemodulatorChain
from owrx.modes import Modes, DigitalMode
from typing import Union
from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, DialFrequencyReceiver
from csdr.chain.analog import NFm, Ssb
-from csdr.chain.digimodes import AudioChopperDemodulator
+from csdr.chain.digimodes import AudioChopperDemodulator, PacketDemodulator
+from pycsdr.modules import Buffer
import logging
logger = logging.getLogger(__name__)
-class ServiceOutput(Output, metaclass=ABCMeta):
- def __init__(self, frequency):
- self.frequency = frequency
-
- @abstractmethod
- def getParser(self):
- # abstract method; implement in subclasses
- pass
-
- def receive_output(self, t, read_fn):
- parser = self.getParser()
- parser.setDialFrequency(self.frequency)
- target = self.pump(read_fn, parser.parse)
- threading.Thread(target=target, name="service_output_receive").start()
-
-
-class WsjtServiceOutput(ServiceOutput):
- def getParser(self):
- return WsjtParser(WsjtHandler())
-
- def supports_type(self, t):
- return t == "wsjt_demod"
-
-
-class AprsServiceOutput(ServiceOutput):
- def getParser(self):
- return AprsParser(AprsHandler())
-
- def supports_type(self, t):
- return t == "packet_demod"
-
-
-class Js8ServiceOutput(ServiceOutput):
- def getParser(self):
- return Js8Parser(Js8Handler())
-
- def supports_type(self, t):
- return t == "js8_demod"
-
-
class ServiceHandler(SdrSourceEventClient):
def __init__(self, source):
self.lock = threading.RLock()
@@ -287,13 +244,6 @@ class ServiceHandler(SdrSourceEventClient):
def setupService(self, mode, frequency, source):
logger.debug("setting up service {0} on frequency {1}".format(mode, frequency))
- # TODO selecting outputs will need some more intelligence here
- if mode == "packet":
- output = AprsServiceOutput(frequency)
- elif mode == "js8":
- output = Js8ServiceOutput(frequency)
- else:
- output = WsjtServiceOutput(frequency)
modeObject = Modes.findByModulation(mode)
if not isinstance(modeObject, DigitalMode):
@@ -312,6 +262,10 @@ class ServiceHandler(SdrSourceEventClient):
chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, shift)
chain.setBandPass(bandpass.low_cut, bandpass.high_cut)
chain.setReader(source.getBuffer().getReader())
+
+ # dummy buffer, we don't use the output right now
+ buffer = Buffer(chain.getOutputFormat())
+ chain.setWriter(buffer)
return chain
# TODO move this elsewhere
@@ -321,7 +275,7 @@ class ServiceHandler(SdrSourceEventClient):
# TODO: move this to Modes
demodChain = None
if demod == "nfm":
- demodChain = NFm(props["output_rate"])
+ demodChain = NFm(48000)
elif demod in ["usb", "lsb", "cw"]:
demodChain = Ssb()
@@ -334,24 +288,11 @@ class ServiceHandler(SdrSourceEventClient):
# TODO add remaining modes
if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]:
return AudioChopperDemodulator(mod, WsjtParser())
+ elif mod == "packet":
+ return PacketDemodulator(service=True)
return None
-class WsjtHandler(object):
- def write_wsjt_message(self, msg):
- pass
-
-
-class AprsHandler(object):
- def write_aprs_data(self, data):
- pass
-
-
-class Js8Handler(object):
- def write_js8_message(self, frame: Js8Frame, freq: int):
- pass
-
-
class Services(object):
handlers = {}
schedulers = {}
diff --git a/owrx/service/chain.py b/owrx/service/chain.py
index c275ae9..549066a 100644
--- a/owrx/service/chain.py
+++ b/owrx/service/chain.py
@@ -1,6 +1,7 @@
from csdr.chain import Chain
from csdr.chain.selector import Selector
from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, FixedAudioRateChain
+from pycsdr.types import Format
class ServiceDemodulatorChain(Chain):
@@ -8,13 +9,16 @@ class ServiceDemodulatorChain(Chain):
# TODO magic number... check if this edge case even exsists and change the api if possible
rate = secondaryDemod.getFixedAudioRate() if isinstance(secondaryDemod, FixedAudioRateChain) else 1200
- self.selector = Selector(sampleRate, rate, shiftRate)
+ self.selector = Selector(sampleRate, rate, shiftRate, withSquelch=False)
+
+ workers = [self.selector]
+
+ # primary demodulator is only necessary if the secondary does not accept IQ input
+ if secondaryDemod.getInputFormat() is not Format.COMPLEX_FLOAT:
+ workers += [demod]
+
+ workers += [secondaryDemod]
- workers = [
- self.selector,
- demod,
- secondaryDemod
- ]
super().__init__(workers)
def setBandPass(self, lowCut, highCut):