restore aprs functionality

This commit is contained in:
Jakob Ketterl 2021-09-06 15:05:33 +02:00
parent 7c43c78c4b
commit b9f43654cd
18 changed files with 390 additions and 190 deletions

View File

@ -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

View File

@ -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:

View File

@ -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):

View File

@ -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):

View File

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

View File

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

View File

@ -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

View File

@ -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($(
'<table>' +
@ -78,14 +89,14 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) {
return $('<div/>').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]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
} 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]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + 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($(
'<table>' +
@ -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');
};

View File

@ -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);

View File

@ -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:

View File

@ -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

94
owrx/aprs/module.py Normal file
View File

@ -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

View File

@ -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()

View File

@ -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})

View File

@ -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

View File

@ -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"]

View File

@ -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 = {}

View File

@ -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):