From 399cc3f267af02193ed4a89037436be507931903 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Tue, 12 Nov 2024 21:44:30 -0500 Subject: [PATCH] Adding CW skimmer. --- csdr/chain/toolbox.py | 25 +++++++++++++++++++++++-- csdr/module/toolbox.py | 6 ++++++ owrx/dsp.py | 3 +++ owrx/modes.py | 1 + owrx/toolbox.py | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/csdr/chain/toolbox.py b/csdr/chain/toolbox.py index 0868d2a7..0e736614 100644 --- a/csdr/chain/toolbox.py +++ b/csdr/chain/toolbox.py @@ -1,8 +1,8 @@ from csdr.chain.demodulator import ServiceDemodulator, DialFrequencyReceiver -from csdr.module.toolbox import Rtl433Module, MultimonModule, DumpHfdlModule, DumpVdl2Module, Dump1090Module, AcarsDecModule, RedseaModule, SatDumpModule +from csdr.module.toolbox import Rtl433Module, MultimonModule, DumpHfdlModule, DumpVdl2Module, Dump1090Module, AcarsDecModule, RedseaModule, SatDumpModule, CwSkimmerModule from pycsdr.modules import FmDemod, AudioResampler, Convert, Agc, Squelch from pycsdr.types import Format -from owrx.toolbox import TextParser, PageParser, SelCallParser, EasParser, IsmParser, RdsParser +from owrx.toolbox import TextParser, PageParser, SelCallParser, EasParser, IsmParser, RdsParser, CwSkimmerParser from owrx.aircraft import HfdlParser, Vdl2Parser, AdsbParser, AcarsParser from datetime import datetime @@ -223,6 +223,27 @@ class RdsDemodulator(ServiceDemodulator, DialFrequencyReceiver): self.parser.setDialFrequency(frequency) +class CwSkimmerDemodulator(ServiceDemodulator, DialFrequencyReceiver): + def __init__(self, sampleRate: int = 12000, charCount: int = 8): + self.sampleRate = sampleRate + self.parser = CwSkimmerParser() + workers = [ + CwSkimmerModule(sampleRate, charCount), + self.parser, + ] + # Connect all the workers + super().__init__(workers) + + def getFixedAudioRate(self) -> int: + return self.sampleRate + + def supportsSquelch(self) -> bool: + return False + + def setDialFrequency(self, frequency: int) -> None: + self.parser.setDialFrequency(frequency) + + class NoaaAptDemodulator(ServiceDemodulator): def __init__(self, satellite: int = 19, service: bool = False): d = datetime.utcnow() diff --git a/csdr/module/toolbox.py b/csdr/module/toolbox.py index 2308a40f..3ca568ed 100644 --- a/csdr/module/toolbox.py +++ b/csdr/module/toolbox.py @@ -119,6 +119,12 @@ class AcarsDecModule(WavFileModule): return Format.CHAR +class CwSkimmerModule(ExecModule): + def __init__(self, sampleRate: int = 12000, charCount: int = 8): + cmd = ["csdr-cwskimmer", "-r", str(sampleRate), "-n", str(charCount)] + super().__init__(Format.FLOAT, Format.CHAR, cmd) + + class RedseaModule(ExecModule): def __init__(self, sampleRate: int = 171000, rbds: bool = False): cmd = [ "redsea", "--input", "mpx", "--samplerate", str(sampleRate) ] diff --git a/owrx/dsp.py b/owrx/dsp.py index 72167b0a..aa922365 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -720,6 +720,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) elif mod == "cwdecoder": from csdr.chain.digimodes import CwDemodulator return CwDemodulator(75.0) + elif mod == "cwskimmer": + from csdr.chain.digimodes import CwSkimmerDemodulator + return CwSkimmerDemodulator() elif mod == "mfrtty170": from csdr.chain.digimodes import MFRttyDemodulator return MFRttyDemodulator(170.0, 45.45, reverse = False) diff --git a/owrx/modes.py b/owrx/modes.py index c34b8aaa..21d804c3 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -194,6 +194,7 @@ class Modes(object): squelch=False, ), DigitalMode("cwdecoder", "CW Decoder", underlying=["usb", "lsb"]), + DigitalMode("cwskimmer", "CW Skimmer", underlying=["usb", "lsb"]), # Replaced by Jakob's RTTY decoder # DigitalMode("mfrtty170", "RTTY-170", underlying=["usb"]), # DigitalMode("mfrtty450", "RTTY-450", underlying=["usb"]), diff --git a/owrx/toolbox.py b/owrx/toolbox.py index 06f4e366..140da2d3 100644 --- a/owrx/toolbox.py +++ b/owrx/toolbox.py @@ -414,3 +414,37 @@ class EasParser(TextParser): # Return received message as text return "\n".join(out) + + +class CwSkimmerParser(TextParser): + def __init__(self, charTotal: int = 32, service: bool = False): + self.reLine = re.compile("^(\d+):\s*(.*)$") + self.data = {} + # Construct parent object + super().__init__(filePrefix="CW", service=service) + + def parse(self, msg: bytes): + # Do not parse in service mode + if self.service: + return None + # Parse CW messages by frequency + msg = msg.decode("utf-8", "replace") + r = self.reLine.match(msg) + if r is not None: + freq = int(r.group(1)) + text = r.group(2) + # Add up newly decoded characters + if freq in self.data: + text = self.data[freq] + text + # Truncate received text to charTotal + text = text if len(text) <= charTotal else text[:charTotal] + self.data[freq] = text + # Compose output + out = { "mode": "CW", "text": text } + # Add frequency, if known + if self.frequency: + out["freq"] = self.frequency + freq + # Done + return out + # No result + return None