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 csdr.output import Output
from owrx.aprs.kiss import KissClient
from owrx.aprs.direwolf import DirewolfConfig, DirewolfConfigSubscriber from owrx.aprs.direwolf import DirewolfConfig, DirewolfConfigSubscriber
from owrx.audio.chopper import AudioChopper from owrx.audio.chopper import AudioChopper

View File

@ -1,5 +1,6 @@
from csdr.module import Module from csdr.module import Module
from pycsdr.modules import Buffer from pycsdr.modules import Buffer
from pycsdr.types import Format
from typing import Union, Callable from typing import Union, Callable
@ -133,7 +134,13 @@ class Chain(Module):
self.clientReader.stop() self.clientReader.stop()
self.clientReader = None 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: if self.workers:
return self.workers[-1].getOutputFormat() return self.workers[-1].getOutputFormat()
else: else:

View File

@ -19,6 +19,7 @@ class Am(BaseDemodulatorChain):
class NFm(BaseDemodulatorChain): class NFm(BaseDemodulatorChain):
def __init__(self, sampleRate: int): def __init__(self, sampleRate: int):
self.sampleRate = sampleRate
agc = Agc(Format.FLOAT) agc = Agc(Format.FLOAT)
agc.setProfile(AgcProfile.SLOW) agc.setProfile(AgcProfile.SLOW)
agc.setMaxGain(3) agc.setMaxGain(3)
@ -30,6 +31,12 @@ class NFm(BaseDemodulatorChain):
] ]
super().__init__(workers) 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): class WFm(BaseDemodulatorChain, FixedIfSampleRateChain, HdAudio):
def __init__(self, sampleRate: int, tau: float): def __init__(self, sampleRate: int, tau: float):

View File

@ -6,9 +6,13 @@ class BaseDemodulatorChain(Chain):
def supportsSquelch(self) -> bool: def supportsSquelch(self) -> bool:
return True return True
def setSampleRate(self, sampleRate: int) -> None:
pass
class SecondaryDemodulator(Chain): class SecondaryDemodulator(Chain):
pass def supportsSquelch(self) -> bool:
return True
class FixedAudioRateChain(ABC): class FixedAudioRateChain(ABC):

View File

@ -1,7 +1,10 @@
from csdr.chain.demodulator import SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver from csdr.chain.demodulator import SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver
from owrx.audio.chopper import AudioChopper 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 pycsdr.types import Format
from owrx.aprs.module import DirewolfModule
class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver): class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver):
@ -16,3 +19,26 @@ class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFre
def setDialFrequency(self, frequency: int) -> None: def setDialFrequency(self, frequency: int) -> None:
self.chopper.setDialFrequency(frequency) 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): 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.outputRate = outputRate
self.shift = Shift(shiftRate) self.shift = Shift(shiftRate)
@ -73,12 +73,14 @@ class Selector(Chain):
self.bandpassCutoffs = None self.bandpassCutoffs = None
self.setBandpass(-4000, 4000) self.setBandpass(-4000, 4000)
self.readings_per_second = 4 workers = [self.shift, self.decimation, self.bandpass]
# 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, 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) 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): def __init__(self):
self.reader = None self.reader = None
self.writer = None self.writer = None
super().__init__() super().__init__()
def setReader(self, reader: pycsdr.modules.Reader) -> None: def setReader(self, reader: Reader) -> None:
self.reader = reader self.reader = reader
def setWriter(self, writer: pycsdr.modules.Writer) -> None: def setWriter(self, writer: Writer) -> None:
self.writer = writer 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(); this.initClearButton();
} }
MessagePanel.prototype.supportsMessage = function(message) {
return false;
};
MessagePanel.prototype.render = function() { MessagePanel.prototype.render = function() {
}; };
@ -46,10 +50,17 @@ MessagePanel.prototype.initClearButton = function() {
function WsjtMessagePanel(el) { function WsjtMessagePanel(el) {
MessagePanel.call(this, el); MessagePanel.call(this, el);
this.initClearTimer(); 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 = new MessagePanel();
WsjtMessagePanel.prototype.supportsMessage = function(message) {
return this.modes.indexOf(message['mode']) >= 0;
};
WsjtMessagePanel.prototype.render = function() { WsjtMessagePanel.prototype.render = function() {
$(this.el).append($( $(this.el).append($(
'<table>' + '<table>' +
@ -78,14 +89,14 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) {
return $('<div/>').text(input).html() 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})$/); matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/);
if (matches && matches[2] !== 'RR73') { if (matches && matches[2] !== 'RR73') {
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>'; linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
} else { } else {
linkedmsg = html_escape(linkedmsg); 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]+)/); matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/);
if (matches) { if (matches) {
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + html_escape(matches[3]); 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(){ $.fn.wsjtMessagePanel = function(){
if (!this.data('panel')) { if (!this.data('panel')) {
this.data('panel', new WsjtMessagePanel(this)); this.data('panel', new WsjtMessagePanel(this));
}; }
return this.data('panel'); return this.data('panel');
}; };
@ -119,6 +130,10 @@ function PacketMessagePanel(el) {
PacketMessagePanel.prototype = new MessagePanel(); PacketMessagePanel.prototype = new MessagePanel();
PacketMessagePanel.prototype.supportsMessage = function(message) {
return message['mode'] === 'APRS';
};
PacketMessagePanel.prototype.render = function() { PacketMessagePanel.prototype.render = function() {
$(this.el).append($( $(this.el).append($(
'<table>' + '<table>' +
@ -243,6 +258,6 @@ PocsagMessagePanel.prototype.pushMessage = function(msg) {
$.fn.pocsagMessagePanel = function() { $.fn.pocsagMessagePanel = function() {
if (!this.data('panel')) { if (!this.data('panel')) {
this.data('panel', new PocsagMessagePanel(this)); this.data('panel', new PocsagMessagePanel(this));
}; }
return this.data('panel'); return this.data('panel');
}; };

View File

@ -824,9 +824,6 @@ function on_ws_recv(evt) {
case "js8_message": case "js8_message":
$("#openwebrx-panel-js8-message").js8().pushMessage(json['value']); $("#openwebrx-panel-js8-message").js8().pushMessage(json['value']);
break; break;
case "wsjt_message":
$("#openwebrx-panel-wsjt-message").wsjtMessagePanel().pushMessage(json['value']);
break;
case "dial_frequencies": case "dial_frequencies":
var as_bookmarks = json['value'].map(function (d) { var as_bookmarks = json['value'].map(function (d) {
return { return {
@ -837,9 +834,6 @@ function on_ws_recv(evt) {
}); });
bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies'); bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies');
break; break;
case "aprs_data":
$('#openwebrx-panel-packet-message').packetMessagePanel().pushMessage(json['value']);
break;
case "bookmarks": case "bookmarks":
bookmarks.replace_bookmarks(json['value'], "server"); bookmarks.replace_bookmarks(json['value'], "server");
break; break;
@ -851,7 +845,18 @@ function on_ws_recv(evt) {
$("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator(); $("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator();
break; break;
case 'secondary_demod': 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; break;
case 'log_message': case 'log_message':
divlog(json['value'], true); divlog(json['value'], true);

View File

@ -1,10 +1,15 @@
from owrx.aprs.kiss import KissDeframer
from owrx.map import Map, LatLngLocation from owrx.map import Map, LatLngLocation
from owrx.metrics import Metrics, CounterMetric from owrx.metrics import Metrics, CounterMetric
from owrx.parser import Parser from owrx.bands import Bandplan
from datetime import datetime, timezone 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 re
import logging import logging
import pickle
logger = logging.getLogger(__name__) 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} 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): 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:
@ -54,7 +92,7 @@ class Ax25Parser(object):
def chunks(l, n): def chunks(l, n):
"""Yield successive n-sized chunks from l.""" """Yield successive n-sized chunks from l."""
for i in range(0, len(l), n): for i in range(0, len(l), n):
yield l[i : i + n] yield l[i:i + n]
return { return {
"destination": self.extractCallsign(ax25frame[0:7]), "destination": self.extractCallsign(ax25frame[0:7]),
@ -117,9 +155,9 @@ class WeatherParser(object):
WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4), WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4),
] ]
def __init__(self, data, weather={}): def __init__(self, data, weather=None):
self.data = data self.data = data
self.weather = weather self.weather = {} if weather is None else weather
def getWeather(self): def getWeather(self):
doWork = True doWork = True
@ -151,16 +189,44 @@ class AprsLocation(LatLngLocation):
return res return res
class AprsParser(Parser): class AprsParser(Module, Thread):
def __init__(self, handler): def __init__(self):
super().__init__(handler) super().__init__()
self.ax25parser = Ax25Parser()
self.deframer = KissDeframer()
self.metrics = {} self.metrics = {}
self.doRun = True
self.band = None
def setDialFrequency(self, freq): def setDialFrequency(self, freq):
super().setDialFrequency(freq) self.band = Bandplan.getSharedInstance().findBand(freq)
self.metrics = {}
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:
@ -184,22 +250,22 @@ class AprsParser(Parser):
return False return False
return True return True
def parse(self, raw): def parse(self, data):
for frame in self.deframer.parse(raw): try:
try: # TODO how can we tell if this is an APRS frame at all?
data = self.ax25parser.parse(frame) aprsData = self.parseAprsData(data)
# TODO how can we tell if this is an APRS frame at all? logger.debug("decoded APRS data: %s", aprsData)
aprsData = self.parseAprsData(data) self.updateMap(aprsData)
self.getMetric("total").inc()
if self.isDirect(aprsData):
self.getMetric("direct").inc()
logger.debug("decoded APRS data: %s", aprsData) # the frontend uses this to distinguis hessages from the different parsers
self.updateMap(aprsData) aprsData["mode"] = "APRS"
self.getMetric("total").inc() return aprsData
if self.isDirect(aprsData): except Exception:
self.getMetric("direct").inc() logger.exception("exception while parsing aprs data")
self.handler.write_aprs_data(aprsData)
except Exception:
logger.exception("exception while parsing aprs data")
def updateMap(self, mapData): def updateMap(self, mapData):
if "type" in mapData and mapData["type"] == "thirdparty" and "data" in 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 socket
import time import time
import pickle
import logging import logging
@ -11,33 +16,37 @@ TFEND = 0xDC
TFESC = 0xDD TFESC = 0xDD
class KissClient(object): class KissDeframer(Module, Thread):
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):
def __init__(self): def __init__(self):
self.escaped = False self.escaped = False
self.buf = bytearray() 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): def parse(self, input):
frames = []
for b in input: for b in input:
if b == FESC: if b == FESC:
self.escaped = True self.escaped = True
@ -49,11 +58,10 @@ class KissDeframer(object):
else: else:
logger.warning("invalid escape char: %s", str(input[0])) logger.warning("invalid escape char: %s", str(input[0]))
self.escaped = False self.escaped = False
elif input[0] == FEND: elif b == FEND:
# data frames start with 0x00 # data frames start with 0x00
if len(self.buf) > 1 and self.buf[0] == 0x00: if len(self.buf) > 1 and self.buf[0] == 0x00:
frames += [self.buf[1:]] yield self.buf[1:]
self.buf = bytearray() self.buf = bytearray()
else: else:
self.buf.append(b) 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.wav import AudioWriter
from owrx.audio.queue import QueueJob from owrx.audio.queue import QueueJob
from csdr.module import Module from csdr.module import Module
from pycsdr.types import Format
import pickle import pickle
import logging import logging
@ -27,6 +28,12 @@ class AudioChopper(threading.Thread, Module, ProfileSourceSubscriber):
super().__init__() super().__init__()
Module.__init__(self) Module.__init__(self)
def getInputFormat(self) -> Format:
return Format.SHORT
def getOutputFormat(self) -> Format:
return Format.CHAR
def stop_writers(self): def stop_writers(self):
while self.writers: while self.writers:
self.writers.pop().stop() self.writers.pop().stop()

View File

@ -399,9 +399,13 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def write_secondary_fft(self, data): def write_secondary_fft(self, data):
self.send(bytes([0x03]) + data) self.send(bytes([0x03]) + data)
def write_secondary_demod(self, data): def write_secondary_demod(self, message):
message = data.decode("ascii", "replace") io = BytesIO(message.tobytes())
self.send({"type": "secondary_demod", "value": message}) try:
while True:
self.send({"type": "secondary_demod", "value": pickle.load(io)})
except EOFError:
pass
def write_secondary_dsp_config(self, cfg): def write_secondary_dsp_config(self, cfg):
self.send({"type": "secondary_config", "value": cfg}) self.send({"type": "secondary_config", "value": cfg})
@ -418,23 +422,12 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def write_metadata(self, metadata): def write_metadata(self, metadata):
self.send({"type": "metadata", "value": 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): def write_dial_frequencies(self, frequencies):
self.send({"type": "dial_frequencies", "value": frequencies}) self.send({"type": "dial_frequencies", "value": frequencies})
def write_bookmarks(self, bookmarks): def write_bookmarks(self, bookmarks):
self.send({"type": "bookmarks", "value": 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): def write_log_message(self, message):
self.send({"type": "log_message", "value": 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.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 from csdr.chain.digimodes import AudioChopperDemodulator, PacketDemodulator
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
@ -66,7 +66,7 @@ class ClientDemodulatorChain(Chain):
format = w1.getOutputFormat() format = w1.getOutputFormat()
if self.audioBuffer is None or self.audioBuffer.getFormat() != format: if self.audioBuffer is None or self.audioBuffer.getFormat() != format:
self.audioBuffer = Buffer(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()) self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
super()._connect(w1, w2, self.audioBuffer) super()._connect(w1, w2, self.audioBuffer)
else: else:
@ -94,28 +94,33 @@ class ClientDemodulatorChain(Chain):
if isinstance(self.demodulator, FixedIfSampleRateChain): if isinstance(self.demodulator, FixedIfSampleRateChain):
self.selector.setOutputRate(self.demodulator.getFixedIfSampleRate()) 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()) self.selector.setOutputRate(self.secondaryDemodulator.getFixedAudioRate())
else: else:
self.selector.setOutputRate(outputRate) self.selector.setOutputRate(outputRate)
self.demodulator.setSampleRate(outputRate)
if isinstance(self.demodulator, FixedAudioRateChain): if isinstance(self.demodulator, FixedAudioRateChain):
self.clientAudioChain.setInputRate(self.demodulator.getFixedAudioRate()) 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()) self.clientAudioChain.setInputRate(self.secondaryDemodulator.getFixedAudioRate())
else: else:
self.clientAudioChain.setInputRate(outputRate) self.clientAudioChain.setInputRate(outputRate)
if not demodulator.supportsSquelch(): self._syncSquelch()
self.selector.setSquelchLevel(-150)
else:
self.selector.setSquelchLevel(self.squelchLevel)
self.clientAudioChain.setClientRate(outputRate) self.clientAudioChain.setClientRate(outputRate)
if self.metaWriter is not None and isinstance(demodulator, DigihamChain): if self.metaWriter is not None and isinstance(demodulator, DigihamChain):
demodulator.setMetaWriter(self.metaWriter) 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]): def setSecondaryDemodulator(self, demod: Union[SecondaryDemodulator, None]):
if demod is self.secondaryDemodulator: if demod is self.secondaryDemodulator:
return return
@ -125,19 +130,18 @@ class ClientDemodulatorChain(Chain):
self.secondaryDemodulator = demod self.secondaryDemodulator = demod
if self.secondaryDemodulator is not None and isinstance(self.secondaryDemodulator, FixedAudioRateChain): rate = self._getSelectorOutputRate()
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
self.selector.setOutputRate(rate) self.selector.setOutputRate(rate)
self.clientAudioChain.setInputRate(rate) self.clientAudioChain.setInputRate(rate)
self.demodulator.setSampleRate(rate)
self._updateDialFrequency() self._updateDialFrequency()
self._syncSquelch()
if self.secondaryDemodulator is not None: 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) self.secondaryDemodulator.setWriter(self.secondaryWriter)
if self.secondaryDemodulator is None and self.secondaryFftChain is not None: 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: if self.secondaryDemodulator is not None and self.secondaryFftChain is None:
# TODO eliminate constants # 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.setReader(self.selectorBuffer.getReader())
self.secondaryFftChain.setWriter(self.secondaryFftWriter) 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): def setLowCut(self, lowCut):
self.selector.setLowCut(lowCut) self.selector.setLowCut(lowCut)
@ -189,9 +202,7 @@ class ClientDemodulatorChain(Chain):
if level == self.squelchLevel: if level == self.squelchLevel:
return return
self.squelchLevel = level self.squelchLevel = level
if not self.demodulator.supportsSquelch(): self._syncSquelch()
return
self.selector.setSquelchLevel(level)
def setOutputRate(self, outputRate) -> None: def setOutputRate(self, outputRate) -> None:
if outputRate == self.outputRate: if outputRate == self.outputRate:
@ -203,6 +214,7 @@ class ClientDemodulatorChain(Chain):
return return
if not isinstance(self.demodulator, FixedIfSampleRateChain): if not isinstance(self.demodulator, FixedIfSampleRateChain):
self.selector.setOutputRate(outputRate) self.selector.setOutputRate(outputRate)
self.demodulator.setSampleRate(outputRate)
if not isinstance(self.demodulator, FixedAudioRateChain): if not isinstance(self.demodulator, FixedAudioRateChain):
self.clientAudioChain.setClientRate(outputRate) self.clientAudioChain.setClientRate(outputRate)
@ -271,7 +283,6 @@ class DspManager(Output, SdrSourceEventClient):
self.sdrSource = sdrSource self.sdrSource = sdrSource
self.parsers = { self.parsers = {
"meta": MetaParser(self.handler), "meta": MetaParser(self.handler),
"packet_demod": AprsParser(self.handler),
"pocsag_demod": PocsagParser(self.handler), "pocsag_demod": PocsagParser(self.handler),
"js8_demod": Js8Parser(self.handler), "js8_demod": Js8Parser(self.handler),
} }
@ -354,7 +365,7 @@ class DspManager(Output, SdrSourceEventClient):
buffer = Buffer(Format.CHAR) buffer = Buffer(Format.CHAR)
self.chain.setSecondaryWriter(buffer) self.chain.setSecondaryWriter(buffer)
# TODO there's multiple outputs depending on the modulation right now # 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): def set_dial_freq(changes):
if ( if (
@ -481,6 +492,8 @@ class DspManager(Output, SdrSourceEventClient):
# TODO add remaining modes # TODO add remaining modes
if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]:
return AudioChopperDemodulator(mod, WsjtParser()) return AudioChopperDemodulator(mod, WsjtParser())
elif mod == "packet":
return PacketDemodulator()
return None return None
def setSecondaryDemodulator(self, mod): def setSecondaryDemodulator(self, mod):
@ -520,7 +533,6 @@ class DspManager(Output, SdrSourceEventClient):
"smeter": self.handler.write_s_meter_level, "smeter": self.handler.write_s_meter_level,
"secondary_fft": self.handler.write_secondary_fft, "secondary_fft": self.handler.write_secondary_fft,
"secondary_demod": self.handler.write_secondary_demod, "secondary_demod": self.handler.write_secondary_demod,
"wsjt_demod": self.handler.write_wsjt_message,
} }
for demod, parser in self.parsers.items(): for demod, parser in self.parsers.items():
writers[demod] = parser.parse writers[demod] = parser.parse

View File

@ -140,8 +140,7 @@ class DStarEnricher(Enricher):
try: try:
# we can send the DPRS stuff through our APRS parser to extract the information # 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: only third-party parsing accepts this format right now
# TODO: we also need to pass a handler, which is not needed parser = AprsParser()
parser = AprsParser(None)
dprsData = parser.parseThirdpartyAprsData(meta["dprs"]) dprsData = parser.parseThirdpartyAprsData(meta["dprs"])
if "data" in dprsData: if "data" in dprsData:
data = dprsData["data"] data = dprsData["data"]

View File

@ -2,68 +2,25 @@ import threading
from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
from owrx.sdr import SdrService from owrx.sdr import SdrService
from owrx.bands import Bandplan from owrx.bands import Bandplan
from csdr.output import Output
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
from owrx.aprs import AprsParser
from owrx.js8 import Js8Parser
from owrx.config import Config from owrx.config import Config
from owrx.source.resampler import Resampler from owrx.source.resampler import Resampler
from owrx.property import PropertyLayer, PropertyDeleted from owrx.property import PropertyLayer, PropertyDeleted
from js8py import Js8Frame from js8py import Js8Frame
from abc import ABCMeta, abstractmethod
from owrx.service.schedule import ServiceScheduler from owrx.service.schedule import ServiceScheduler
from owrx.service.chain import ServiceDemodulatorChain from owrx.service.chain import ServiceDemodulatorChain
from owrx.modes import Modes, DigitalMode from owrx.modes import Modes, DigitalMode
from typing import Union from typing import Union
from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, DialFrequencyReceiver from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, DialFrequencyReceiver
from csdr.chain.analog import NFm, Ssb 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 import logging
logger = logging.getLogger(__name__) 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): class ServiceHandler(SdrSourceEventClient):
def __init__(self, source): def __init__(self, source):
self.lock = threading.RLock() self.lock = threading.RLock()
@ -287,13 +244,6 @@ class ServiceHandler(SdrSourceEventClient):
def setupService(self, mode, frequency, source): def setupService(self, mode, frequency, source):
logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) 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) modeObject = Modes.findByModulation(mode)
if not isinstance(modeObject, DigitalMode): if not isinstance(modeObject, DigitalMode):
@ -312,6 +262,10 @@ class ServiceHandler(SdrSourceEventClient):
chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, shift) chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, shift)
chain.setBandPass(bandpass.low_cut, bandpass.high_cut) chain.setBandPass(bandpass.low_cut, bandpass.high_cut)
chain.setReader(source.getBuffer().getReader()) chain.setReader(source.getBuffer().getReader())
# dummy buffer, we don't use the output right now
buffer = Buffer(chain.getOutputFormat())
chain.setWriter(buffer)
return chain return chain
# TODO move this elsewhere # TODO move this elsewhere
@ -321,7 +275,7 @@ class ServiceHandler(SdrSourceEventClient):
# TODO: move this to Modes # TODO: move this to Modes
demodChain = None demodChain = None
if demod == "nfm": if demod == "nfm":
demodChain = NFm(props["output_rate"]) demodChain = NFm(48000)
elif demod in ["usb", "lsb", "cw"]: elif demod in ["usb", "lsb", "cw"]:
demodChain = Ssb() demodChain = Ssb()
@ -334,24 +288,11 @@ class ServiceHandler(SdrSourceEventClient):
# TODO add remaining modes # TODO add remaining modes
if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]:
return AudioChopperDemodulator(mod, WsjtParser()) return AudioChopperDemodulator(mod, WsjtParser())
elif mod == "packet":
return PacketDemodulator(service=True)
return None 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): class Services(object):
handlers = {} handlers = {}
schedulers = {} schedulers = {}

View File

@ -1,6 +1,7 @@
from csdr.chain import Chain from csdr.chain import Chain
from csdr.chain.selector import Selector from csdr.chain.selector import Selector
from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, FixedAudioRateChain from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, FixedAudioRateChain
from pycsdr.types import Format
class ServiceDemodulatorChain(Chain): 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 # 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 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) super().__init__(workers)
def setBandPass(self, lowCut, highCut): def setBandPass(self, lowCut, highCut):