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 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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
workers = [self.shift, self.decimation, self.bandpass]
|
||||||
|
|
||||||
|
if withSquelch:
|
||||||
self.readings_per_second = 4
|
self.readings_per_second = 4
|
||||||
# s-meter readings are available every 1024 samples
|
# s-meter readings are available every 1024 samples
|
||||||
# the reporting interval is measured in those 1024-sample blocks
|
# the reporting interval is measured in those 1024-sample blocks
|
||||||
self.squelch = Squelch(5, int(outputRate / (self.readings_per_second * 1024)))
|
self.squelch = Squelch(5, int(outputRate / (self.readings_per_second * 1024)))
|
||||||
|
workers += [self.squelch]
|
||||||
workers = [self.shift, self.decimation, self.bandpass, self.squelch]
|
|
||||||
|
|
||||||
super().__init__(workers)
|
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):
|
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
|
||||||
|
@ -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');
|
||||||
};
|
};
|
@ -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);
|
||||||
|
@ -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,11 +250,8 @@ 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:
|
||||||
data = self.ax25parser.parse(frame)
|
|
||||||
|
|
||||||
# TODO how can we tell if this is an APRS frame at all?
|
# TODO how can we tell if this is an APRS frame at all?
|
||||||
aprsData = self.parseAprsData(data)
|
aprsData = self.parseAprsData(data)
|
||||||
|
|
||||||
@ -197,7 +260,10 @@ class AprsParser(Parser):
|
|||||||
self.getMetric("total").inc()
|
self.getMetric("total").inc()
|
||||||
if self.isDirect(aprsData):
|
if self.isDirect(aprsData):
|
||||||
self.getMetric("direct").inc()
|
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:
|
except Exception:
|
||||||
logger.exception("exception while parsing aprs data")
|
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 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
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.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()
|
||||||
|
@ -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})
|
||||||
|
|
||||||
|
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.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,18 +130,17 @@ 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:
|
||||||
|
if self.secondaryDemodulator.getInputFormat() is Format.COMPLEX_FLOAT:
|
||||||
|
self.secondaryDemodulator.setReader(self.selectorBuffer.getReader())
|
||||||
|
else:
|
||||||
self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
|
self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
|
||||||
self.secondaryDemodulator.setWriter(self.secondaryWriter)
|
self.secondaryDemodulator.setWriter(self.secondaryWriter)
|
||||||
|
|
||||||
@ -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
|
||||||
|
@ -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"]
|
||||||
|
@ -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 = {}
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user