restore aprs functionality
This commit is contained in:
parent
7c43c78c4b
commit
b9f43654cd
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
workers = [self.shift, self.decimation, self.bandpass]
|
||||
|
||||
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.shift, self.decimation, self.bandpass, self.squelch]
|
||||
workers += [self.squelch]
|
||||
|
||||
super().__init__(workers)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
};
|
@ -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);
|
||||
|
@ -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:
|
||||
@ -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,11 +250,8 @@ class AprsParser(Parser):
|
||||
return False
|
||||
return True
|
||||
|
||||
def parse(self, raw):
|
||||
for frame in self.deframer.parse(raw):
|
||||
def parse(self, data):
|
||||
try:
|
||||
data = self.ax25parser.parse(frame)
|
||||
|
||||
# TODO how can we tell if this is an APRS frame at all?
|
||||
aprsData = self.parseAprsData(data)
|
||||
|
||||
@ -197,7 +260,10 @@ class AprsParser(Parser):
|
||||
self.getMetric("total").inc()
|
||||
if self.isDirect(aprsData):
|
||||
self.getMetric("direct").inc()
|
||||
self.handler.write_aprs_data(aprsData)
|
||||
|
||||
# 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")
|
||||
|
||||
|
@ -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
94
owrx/aprs/module.py
Normal 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
|
@ -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()
|
||||
|
@ -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})
|
||||
|
||||
|
56
owrx/dsp.py
56
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,18 +130,17 @@ 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:
|
||||
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)
|
||||
|
||||
@ -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
|
||||
|
@ -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"]
|
||||
|
@ -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 = {}
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user