restore aprs functionality
This commit is contained in:
		| @@ -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) | ||||||
|  |  | ||||||
|         self.readings_per_second = 4 |         workers = [self.shift, self.decimation, self.bandpass] | ||||||
|         # s-meter readings are available every 1024 samples |  | ||||||
|         # the reporting interval is measured in those 1024-sample blocks |  | ||||||
|         self.squelch = Squelch(5, int(outputRate / (self.readings_per_second * 1024))) |  | ||||||
|  |  | ||||||
|         workers = [self.shift, self.decimation, self.bandpass, self.squelch] |         if withSquelch: | ||||||
|  |             self.readings_per_second = 4 | ||||||
|  |             # s-meter readings are available every 1024 samples | ||||||
|  |             # the reporting interval is measured in those 1024-sample blocks | ||||||
|  |             self.squelch = Squelch(5, int(outputRate / (self.readings_per_second * 1024))) | ||||||
|  |             workers += [self.squelch] | ||||||
|  |  | ||||||
|         super().__init__(workers) |         super().__init__(workers) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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,22 +250,22 @@ class AprsParser(Parser): | |||||||
|             return False |             return False | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
|     def parse(self, raw): |     def parse(self, data): | ||||||
|         for frame in self.deframer.parse(raw): |         try: | ||||||
|             try: |             # TODO how can we tell if this is an APRS frame at all? | ||||||
|                 data = self.ax25parser.parse(frame) |             aprsData = self.parseAprsData(data) | ||||||
|  |  | ||||||
|                 # TODO how can we tell if this is an APRS frame at all? |             logger.debug("decoded APRS data: %s", aprsData) | ||||||
|                 aprsData = self.parseAprsData(data) |             self.updateMap(aprsData) | ||||||
|  |             self.getMetric("total").inc() | ||||||
|  |             if self.isDirect(aprsData): | ||||||
|  |                 self.getMetric("direct").inc() | ||||||
|  |  | ||||||
|                 logger.debug("decoded APRS data: %s", aprsData) |             # the frontend uses this to distinguis hessages from the different parsers | ||||||
|                 self.updateMap(aprsData) |             aprsData["mode"] = "APRS" | ||||||
|                 self.getMetric("total").inc() |             return aprsData | ||||||
|                 if self.isDirect(aprsData): |         except Exception: | ||||||
|                     self.getMetric("direct").inc() |             logger.exception("exception while parsing aprs data") | ||||||
|                 self.handler.write_aprs_data(aprsData) |  | ||||||
|             except Exception: |  | ||||||
|                 logger.exception("exception while parsing aprs data") |  | ||||||
|  |  | ||||||
|     def updateMap(self, mapData): |     def updateMap(self, mapData): | ||||||
|         if "type" in mapData and mapData["type"] == "thirdparty" and "data" in mapData: |         if "type" in mapData and mapData["type"] == "thirdparty" and "data" in mapData: | ||||||
|   | |||||||
| @@ -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}) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										58
									
								
								owrx/dsp.py
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								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,19 +130,18 @@ class ClientDemodulatorChain(Chain): | |||||||
|  |  | ||||||
|         self.secondaryDemodulator = demod |         self.secondaryDemodulator = demod | ||||||
|  |  | ||||||
|         if self.secondaryDemodulator is not None and isinstance(self.secondaryDemodulator, FixedAudioRateChain): |         rate = self._getSelectorOutputRate() | ||||||
|             if isinstance(self.demodulator, FixedAudioRateChain) and self.demodulator.getFixedAudioRate() != self.secondaryDemodulator.getFixedAudioRate(): |  | ||||||
|                 raise ValueError("secondary and primary demodulator chain audio rates do not match!") |  | ||||||
|             else: |  | ||||||
|                 rate = self.secondaryDemodulator.getFixedAudioRate() |  | ||||||
|         else: |  | ||||||
|             rate = self.outputRate |  | ||||||
|         self.selector.setOutputRate(rate) |         self.selector.setOutputRate(rate) | ||||||
|         self.clientAudioChain.setInputRate(rate) |         self.clientAudioChain.setInputRate(rate) | ||||||
|  |         self.demodulator.setSampleRate(rate) | ||||||
|         self._updateDialFrequency() |         self._updateDialFrequency() | ||||||
|  |         self._syncSquelch() | ||||||
|  |  | ||||||
|         if self.secondaryDemodulator is not None: |         if self.secondaryDemodulator is not None: | ||||||
|             self.secondaryDemodulator.setReader(self.audioBuffer.getReader()) |             if self.secondaryDemodulator.getInputFormat() is Format.COMPLEX_FLOAT: | ||||||
|  |                 self.secondaryDemodulator.setReader(self.selectorBuffer.getReader()) | ||||||
|  |             else: | ||||||
|  |                 self.secondaryDemodulator.setReader(self.audioBuffer.getReader()) | ||||||
|             self.secondaryDemodulator.setWriter(self.secondaryWriter) |             self.secondaryDemodulator.setWriter(self.secondaryWriter) | ||||||
|  |  | ||||||
|         if self.secondaryDemodulator is None and self.secondaryFftChain is not None: |         if self.secondaryDemodulator is None and self.secondaryFftChain is not None: | ||||||
| @@ -146,10 +150,19 @@ class ClientDemodulatorChain(Chain): | |||||||
|  |  | ||||||
|         if self.secondaryDemodulator is not None and self.secondaryFftChain is None: |         if self.secondaryDemodulator is not None and self.secondaryFftChain is None: | ||||||
|             # TODO eliminate constants |             # TODO eliminate constants | ||||||
|             self.secondaryFftChain = FftChain(self.outputRate, 2048, 0.3, 9, "adpcm") |             self.secondaryFftChain = FftChain(self._getSelectorOutputRate(), 2048, 0.3, 9, "adpcm") | ||||||
|             self.secondaryFftChain.setReader(self.selectorBuffer.getReader()) |             self.secondaryFftChain.setReader(self.selectorBuffer.getReader()) | ||||||
|             self.secondaryFftChain.setWriter(self.secondaryFftWriter) |             self.secondaryFftChain.setWriter(self.secondaryFftWriter) | ||||||
|  |  | ||||||
|  |         if self.secondaryFftChain is not None: | ||||||
|  |             self.secondaryFftChain.setSampleRate(rate) | ||||||
|  |  | ||||||
|  |     def _syncSquelch(self): | ||||||
|  |         if not self.demodulator.supportsSquelch() or (self.secondaryDemodulator is not None and not self.secondaryDemodulator.supportsSquelch()): | ||||||
|  |             self.selector.setSquelchLevel(-150) | ||||||
|  |         else: | ||||||
|  |             self.selector.setSquelchLevel(self.squelchLevel) | ||||||
|  |  | ||||||
|     def setLowCut(self, lowCut): |     def setLowCut(self, lowCut): | ||||||
|         self.selector.setLowCut(lowCut) |         self.selector.setLowCut(lowCut) | ||||||
|  |  | ||||||
| @@ -189,9 +202,7 @@ class ClientDemodulatorChain(Chain): | |||||||
|         if level == self.squelchLevel: |         if level == self.squelchLevel: | ||||||
|             return |             return | ||||||
|         self.squelchLevel = level |         self.squelchLevel = level | ||||||
|         if not self.demodulator.supportsSquelch(): |         self._syncSquelch() | ||||||
|             return |  | ||||||
|         self.selector.setSquelchLevel(level) |  | ||||||
|  |  | ||||||
|     def setOutputRate(self, outputRate) -> None: |     def setOutputRate(self, outputRate) -> None: | ||||||
|         if outputRate == self.outputRate: |         if outputRate == self.outputRate: | ||||||
| @@ -203,6 +214,7 @@ class ClientDemodulatorChain(Chain): | |||||||
|             return |             return | ||||||
|         if not isinstance(self.demodulator, FixedIfSampleRateChain): |         if not isinstance(self.demodulator, FixedIfSampleRateChain): | ||||||
|             self.selector.setOutputRate(outputRate) |             self.selector.setOutputRate(outputRate) | ||||||
|  |             self.demodulator.setSampleRate(outputRate) | ||||||
|         if not isinstance(self.demodulator, FixedAudioRateChain): |         if not isinstance(self.demodulator, FixedAudioRateChain): | ||||||
|             self.clientAudioChain.setClientRate(outputRate) |             self.clientAudioChain.setClientRate(outputRate) | ||||||
|  |  | ||||||
| @@ -271,7 +283,6 @@ class DspManager(Output, SdrSourceEventClient): | |||||||
|         self.sdrSource = sdrSource |         self.sdrSource = sdrSource | ||||||
|         self.parsers = { |         self.parsers = { | ||||||
|             "meta": MetaParser(self.handler), |             "meta": MetaParser(self.handler), | ||||||
|             "packet_demod": AprsParser(self.handler), |  | ||||||
|             "pocsag_demod": PocsagParser(self.handler), |             "pocsag_demod": PocsagParser(self.handler), | ||||||
|             "js8_demod": Js8Parser(self.handler), |             "js8_demod": Js8Parser(self.handler), | ||||||
|         } |         } | ||||||
| @@ -354,7 +365,7 @@ class DspManager(Output, SdrSourceEventClient): | |||||||
|         buffer = Buffer(Format.CHAR) |         buffer = Buffer(Format.CHAR) | ||||||
|         self.chain.setSecondaryWriter(buffer) |         self.chain.setSecondaryWriter(buffer) | ||||||
|         # TODO there's multiple outputs depending on the modulation right now |         # TODO there's multiple outputs depending on the modulation right now | ||||||
|         self.wireOutput("wsjt_demod", buffer) |         self.wireOutput("secondary_demod", buffer) | ||||||
|  |  | ||||||
|         def set_dial_freq(changes): |         def set_dial_freq(changes): | ||||||
|             if ( |             if ( | ||||||
| @@ -481,6 +492,8 @@ class DspManager(Output, SdrSourceEventClient): | |||||||
|         # TODO add remaining modes |         # TODO add remaining modes | ||||||
|         if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: |         if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: | ||||||
|             return AudioChopperDemodulator(mod, WsjtParser()) |             return AudioChopperDemodulator(mod, WsjtParser()) | ||||||
|  |         elif mod == "packet": | ||||||
|  |             return PacketDemodulator() | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|     def setSecondaryDemodulator(self, mod): |     def setSecondaryDemodulator(self, mod): | ||||||
| @@ -520,7 +533,6 @@ class DspManager(Output, SdrSourceEventClient): | |||||||
|             "smeter": self.handler.write_s_meter_level, |             "smeter": self.handler.write_s_meter_level, | ||||||
|             "secondary_fft": self.handler.write_secondary_fft, |             "secondary_fft": self.handler.write_secondary_fft, | ||||||
|             "secondary_demod": self.handler.write_secondary_demod, |             "secondary_demod": self.handler.write_secondary_demod, | ||||||
|             "wsjt_demod": self.handler.write_wsjt_message, |  | ||||||
|         } |         } | ||||||
|         for demod, parser in self.parsers.items(): |         for demod, parser in self.parsers.items(): | ||||||
|             writers[demod] = parser.parse |             writers[demod] = parser.parse | ||||||
|   | |||||||
| @@ -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): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Jakob Ketterl
					Jakob Ketterl