restore aprs functionality
This commit is contained in:
		| @@ -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: | ||||
| @@ -54,7 +92,7 @@ class Ax25Parser(object): | ||||
|         def chunks(l, n): | ||||
|             """Yield successive n-sized chunks from l.""" | ||||
|             for i in range(0, len(l), n): | ||||
|                 yield l[i : i + n] | ||||
|                 yield l[i:i + n] | ||||
|  | ||||
|         return { | ||||
|             "destination": self.extractCallsign(ax25frame[0:7]), | ||||
| @@ -117,9 +155,9 @@ class WeatherParser(object): | ||||
|         WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4), | ||||
|     ] | ||||
|  | ||||
|     def __init__(self, data, weather={}): | ||||
|     def __init__(self, data, weather=None): | ||||
|         self.data = data | ||||
|         self.weather = weather | ||||
|         self.weather = {} if weather is None else weather | ||||
|  | ||||
|     def getWeather(self): | ||||
|         doWork = True | ||||
| @@ -151,16 +189,44 @@ class AprsLocation(LatLngLocation): | ||||
|         return res | ||||
|  | ||||
|  | ||||
| class AprsParser(Parser): | ||||
|     def __init__(self, handler): | ||||
|         super().__init__(handler) | ||||
|         self.ax25parser = Ax25Parser() | ||||
|         self.deframer = KissDeframer() | ||||
| class AprsParser(Module, Thread): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.metrics = {} | ||||
|         self.doRun = True | ||||
|         self.band = None | ||||
|  | ||||
|     def setDialFrequency(self, freq): | ||||
|         super().setDialFrequency(freq) | ||||
|         self.metrics = {} | ||||
|         self.band = Bandplan.getSharedInstance().findBand(freq) | ||||
|  | ||||
|     def setReader(self, reader: Reader) -> None: | ||||
|         super().setReader(reader) | ||||
|         self.start() | ||||
|  | ||||
|     def getInputFormat(self) -> Format: | ||||
|         return Format.CHAR | ||||
|  | ||||
|     def getOutputFormat(self) -> Format: | ||||
|         return Format.CHAR | ||||
|  | ||||
|     def run(self): | ||||
|         while self.doRun: | ||||
|             data = self.reader.read() | ||||
|             if data is None: | ||||
|                 self.doRun = False | ||||
|                 break | ||||
|             io = BytesIO(data.tobytes()) | ||||
|             try: | ||||
|                 while True: | ||||
|                     frame = self.parse(pickle.load(io)) | ||||
|                     if frame is not None: | ||||
|                         self.writer.write(pickle.dumps(frame)) | ||||
|             except EOFError: | ||||
|                 pass | ||||
|  | ||||
|     def stop(self): | ||||
|         self.doRun = False | ||||
|         self.reader.stop() | ||||
|  | ||||
|     def getMetric(self, category): | ||||
|         if category not in self.metrics: | ||||
| @@ -184,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): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jakob Ketterl
					Jakob Ketterl