Added CWSkimmer decoder using csdr-cwskimmer tool.
This commit is contained in:
parent
3964f9a23c
commit
c91365a612
16
buildall.sh
16
buildall.sh
|
|
@ -18,6 +18,7 @@ GIT_CSDR_ETI=https://github.com/luarvique/csdr-eti.git
|
|||
GIT_PYCSDR_ETI=https://github.com/luarvique/pycsdr-eti.git
|
||||
GIT_JS8PY=https://github.com/jketterl/js8py.git
|
||||
GIT_REDSEA=https://github.com/luarvique/redsea.git
|
||||
GIT_CWSKIMMER=https://github.com/luarvique/csdr-cwskimmer.git
|
||||
GIT_SOAPYSDRPLAY3=https://github.com/luarvique/SoapySDRPlay3.git
|
||||
GIT_OPENWEBRX=https://github.com/luarvique/openwebrx.git
|
||||
|
||||
|
|
@ -45,6 +46,8 @@ if [ "${1:-}" == "--ask" ]; then
|
|||
[[ "$ret" == [Yy]* ]] && BUILD_JS8PY=y || BUILD_JS8PY=n
|
||||
echo;read -n1 -p "Build Redsea? [yN] " ret
|
||||
[[ "$ret" == [Yy]* ]] && BUILD_REDSEA=y || BUILD_REDSEA=n
|
||||
echo;read -n1 -p "Build csdr-cwskimmer? [yN] " ret
|
||||
[[ "$ret" == [Yy]* ]] && BUILD_CWSKIMMER=y || BUILD_CWSKIMMER=n
|
||||
echo;read -n1 -p "Build SoapySDRPlay3? [yN] " ret
|
||||
[[ "$ret" == [Yy]* ]] && BUILD_SOAPYSDRPLAY3=y || BUILD_SOAPYSDRPLAY3=n
|
||||
echo;read -n1 -p "Build OpenWebRX+? [Yn] " ret
|
||||
|
|
@ -65,6 +68,7 @@ else
|
|||
BUILD_PYCSDR_ETI=y
|
||||
BUILD_JS8PY=y
|
||||
BUILD_REDSEA=y
|
||||
BUILD_CWSKIMMER=y
|
||||
CLEAN_OUTPUT=y
|
||||
fi
|
||||
|
||||
|
|
@ -100,6 +104,7 @@ echo "csdr-eti: $BUILD_CSDR_ETI"
|
|||
echo "pycsdr-eti: $BUILD_PYCSDR_ETI"
|
||||
echo "js8py: $BUILD_JS8PY"
|
||||
echo "redsea: $BUILD_REDSEA"
|
||||
echo "csdr-cwskimmer: $BUILD_CWSKIMMER"
|
||||
echo "SoapySDRPlay3: $BUILD_SOAPYSDRPLAY3"
|
||||
echo "OpenWebRx: $BUILD_OWRX"
|
||||
echo "Clean OUTPUT folder: $CLEAN_OUTPUT"
|
||||
|
|
@ -226,6 +231,17 @@ if [ "${BUILD_REDSEA:-}" == "y" ]; then
|
|||
#sudo dpkg -i *redsea*.deb
|
||||
fi
|
||||
|
||||
if [ "${BUILD_CWSKIMMER:-}" == "y" ]; then
|
||||
echo "##### Building csdr-cwskimmer... #####"
|
||||
git clone -b master "$GIT_CWSKIMMER"
|
||||
pushd csdr-cwskimmer
|
||||
dpkg-buildpackage -us -uc
|
||||
popd
|
||||
# Not installing csdr-cwskimmer here since there are no further
|
||||
# build steps depending on it
|
||||
#sudo dpkg -i csdr-cwskimmer*.deb
|
||||
fi
|
||||
|
||||
if [ "${BUILD_SOAPYSDRPLAY3:-}" == "y" ]; then
|
||||
echo "##### Building SoapySDRPlay3 ... #####"
|
||||
git clone -b master "$GIT_SOAPYSDRPLAY3"
|
||||
|
|
|
|||
|
|
@ -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,28 @@ class RdsDemodulator(ServiceDemodulator, DialFrequencyReceiver):
|
|||
self.parser.setDialFrequency(frequency)
|
||||
|
||||
|
||||
class CwSkimmerDemodulator(ServiceDemodulator, DialFrequencyReceiver):
|
||||
def __init__(self, sampleRate: int = 48000, charCount: int = 4, service: bool = False):
|
||||
self.sampleRate = sampleRate
|
||||
self.parser = CwSkimmerParser(service)
|
||||
workers = [
|
||||
Convert(Format.FLOAT, Format.SHORT),
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -119,6 +119,12 @@ class AcarsDecModule(WavFileModule):
|
|||
return Format.CHAR
|
||||
|
||||
|
||||
class CwSkimmerModule(ExecModule):
|
||||
def __init__(self, sampleRate: int = 48000, charCount: int = 4):
|
||||
cmd = ["csdr-cwskimmer", "-i", "-r", str(sampleRate), "-n", str(charCount)]
|
||||
super().__init__(Format.SHORT, Format.CHAR, cmd)
|
||||
|
||||
|
||||
class RedseaModule(ExecModule):
|
||||
def __init__(self, sampleRate: int = 171000, rbds: bool = False):
|
||||
cmd = [ "redsea", "--input", "mpx", "--samplerate", str(sampleRate) ]
|
||||
|
|
|
|||
|
|
@ -1376,6 +1376,62 @@ img.openwebrx-mirror-img
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
#openwebrx-panel-cwskimmer-message {
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-cwskimmer-message tbody {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-cwskimmer-message .freq {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-cwskimmer-message td.freq {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#openwebrx-panel-cwskimmer-message .text {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-cwskimmer-message td.text {
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message {
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message tbody {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message .address {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message .device {
|
||||
width: 220px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message .timestamp {
|
||||
width: 246px;
|
||||
max-width: 486px;
|
||||
white-space: pre;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message .attr {
|
||||
width: 283px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-dsc-message {
|
||||
height: 310px;
|
||||
}
|
||||
|
|
@ -1415,37 +1471,6 @@ img.openwebrx-mirror-img
|
|||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message {
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message tbody {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message .address {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message .device {
|
||||
width: 220px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message .timestamp {
|
||||
width: 246px;
|
||||
max-width: 486px;
|
||||
white-space: pre;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#openwebrx-panel-ism-message .attr {
|
||||
width: 283px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-sstv-message {
|
||||
height: 310px;
|
||||
width: 365px;
|
||||
|
|
@ -1588,6 +1613,7 @@ img.openwebrx-mirror-img
|
|||
#openwebrx-panel-digimodes[data-mode="fax"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ism"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="dsc"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="cwskimmer"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
|
||||
|
|
@ -1611,7 +1637,8 @@ img.openwebrx-mirror-img
|
|||
#openwebrx-panel-digimodes[data-mode="ism"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="selcall"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="zvei"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="eas"] #openwebrx-digimode-select-channel
|
||||
#openwebrx-panel-digimodes[data-mode="eas"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="cwskimmer"] #openwebrx-digimode-select-channel
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -1637,7 +1664,8 @@ img.openwebrx-mirror-img
|
|||
#openwebrx-panel-digimodes[data-mode="sstv"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="fax"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ism"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="dsc"] #openwebrx-digimode-canvas-container
|
||||
#openwebrx-panel-digimodes[data-mode="dsc"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="cwskimmer"] #openwebrx-digimode-canvas-container
|
||||
{
|
||||
height: 200px;
|
||||
margin: -10px;
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@
|
|||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-adsb-message" style="display: none; width: 619px;" data-panel-name="adsb-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-ism-message" style="display: none; width: 619px;" data-panel-name="ism-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-dsc-message" style="display: none; width: 619px;" data-panel-name="dsc-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-cwskimmer-message" style="display: none; width: 619px;" data-panel-name="cwskimmer-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-m17" style="display: none;" data-panel-name="metadata-m17">
|
||||
<div class="openwebrx-meta-slot">
|
||||
<div class="openwebrx-meta-user-image">
|
||||
|
|
|
|||
|
|
@ -173,12 +173,10 @@ DemodulatorPanel.prototype.updatePanels = function() {
|
|||
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65", "msk144"].indexOf(modulation) >= 0);
|
||||
// Aeronautic modes share the same panel
|
||||
toggle_panel("openwebrx-panel-hfdl-message", ['hfdl', 'vdl2', 'acars'].indexOf(modulation) >= 0);
|
||||
// But ADSB has its own panel
|
||||
toggle_panel("openwebrx-panel-adsb-message", modulation === 'adsb');
|
||||
// Packet modes share the same panel
|
||||
toggle_panel("openwebrx-panel-packet-message", ['packet', 'ais'].indexOf(modulation) >= 0);
|
||||
// these modes come with their own
|
||||
['js8', 'page', 'pocsag', 'sstv', 'fax', 'ism', 'dsc'].forEach(function(m) {
|
||||
// These modes come with their own panels
|
||||
['js8', 'page', 'pocsag', 'sstv', 'fax', 'ism', 'dsc', 'adsb', 'cwskimmer'].forEach(function(m) {
|
||||
toggle_panel('openwebrx-panel-' + m + '-message', modulation === m);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -877,3 +877,78 @@ $.fn.faxMessagePanel = function() {
|
|||
}
|
||||
return this.data('panel');
|
||||
};
|
||||
|
||||
CwSkimmerMessagePanel = function(el) {
|
||||
MessagePanel.call(this, el);
|
||||
this.initClearTimer();
|
||||
this.texts = [];
|
||||
}
|
||||
|
||||
CwSkimmerMessagePanel.prototype = Object.create(MessagePanel.prototype);
|
||||
|
||||
CwSkimmerMessagePanel.prototype.supportsMessage = function(message) {
|
||||
return message['mode'] === 'CW';
|
||||
};
|
||||
|
||||
CwSkimmerMessagePanel.prototype.render = function() {
|
||||
$(this.el).append($(
|
||||
'<table width="100%">' +
|
||||
'<thead><tr>' +
|
||||
'<th class="freq">Freq</th>' +
|
||||
'<th class="text">Text</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>'
|
||||
));
|
||||
};
|
||||
|
||||
CwSkimmerMessagePanel.prototype.pushMessage = function(msg) {
|
||||
// Must have some text
|
||||
if (!msg.text) return;
|
||||
|
||||
// Clear cache if requested
|
||||
if (msg.changed) this.texts = [];
|
||||
|
||||
// Current time
|
||||
var now = Date.now();
|
||||
|
||||
// Modify or add a new entry
|
||||
var j = this.texts.findIndex(function(x) { return x.freq >= msg.freq });
|
||||
if (j < 0) {
|
||||
// Append a new entry
|
||||
this.texts.push({ freq: msg.freq, text: msg.text, ts: now });
|
||||
} else if (this.texts[j].freq == msg.freq) {
|
||||
// Update existing entry
|
||||
this.texts[j].text = (this.texts[j].text + msg.text).slice(-64);
|
||||
this.texts[j].ts = now;
|
||||
} else {
|
||||
// Insert a new entry
|
||||
this.texts.splice(j, 0, { freq: msg.freq, text: msg.text, ts: now });
|
||||
}
|
||||
|
||||
// Generate table body
|
||||
var body = '';
|
||||
for (var j = 0 ; j < this.texts.length ; j++) {
|
||||
// Limit the lifetime of entries depending on their length
|
||||
var cutoff = 5000 * this.texts[j].text.length;
|
||||
if (now - this.texts[j].ts >= cutoff) {
|
||||
this.texts.splice(j--, 1);
|
||||
} else {
|
||||
var f = Math.floor(this.texts[j].freq / 100.0) / 10.0;
|
||||
body +=
|
||||
'<tr style="color:black;background-color:' + (j&1? '#E0FFE0':'#FFFFFF') +
|
||||
';"><td class="freq">' + f.toFixed(1) +
|
||||
'</td><td class="text">' + this.texts[j].text + '</td></tr>\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Assign new table body
|
||||
$(this.el).find('tbody').html(body);
|
||||
};
|
||||
|
||||
$.fn.cwskimmerMessagePanel = function() {
|
||||
if (!this.data('panel')) {
|
||||
this.data('panel', new CwSkimmerMessagePanel(this));
|
||||
}
|
||||
return this.data('panel');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1056,7 +1056,7 @@ function on_ws_recv(evt) {
|
|||
break;
|
||||
case 'secondary_demod':
|
||||
var value = json['value'];
|
||||
var panels = ['wsjt', 'packet', 'pocsag', 'page', 'sstv', 'fax', 'ism', 'hfdl', 'adsb', 'dsc'].map(function(id) {
|
||||
var panels = ['wsjt', 'packet', 'pocsag', 'page', 'sstv', 'fax', 'ism', 'hfdl', 'adsb', 'dsc', 'cwskimmer'].map(function(id) {
|
||||
return $('#openwebrx-panel-' + id + '-message')[id + 'MessagePanel']();
|
||||
});
|
||||
panels.push($('#openwebrx-panel-js8-message').js8());
|
||||
|
|
|
|||
|
|
@ -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.toolbox import CwSkimmerDemodulator
|
||||
return CwSkimmerDemodulator()
|
||||
elif mod == "mfrtty170":
|
||||
from csdr.chain.digimodes import MFRttyDemodulator
|
||||
return MFRttyDemodulator(170.0, 45.45, reverse = False)
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ class FeatureDetector(object):
|
|||
"mqtt": ["paho_mqtt"],
|
||||
"hdradio": ["nrsc5"],
|
||||
"rigcontrol": ["hamlib"],
|
||||
"cwskimmer": ["csdr_cwskimmer"],
|
||||
}
|
||||
|
||||
def feature_availability(self):
|
||||
|
|
@ -775,3 +776,11 @@ class FeatureDetector(object):
|
|||
The `hamlib` package is available in most Linux distributions.
|
||||
"""
|
||||
return self.command_is_runnable("rigctl -V")
|
||||
|
||||
def has_csdr_cwskimmer(self):
|
||||
"""
|
||||
OpenWebRX uses the [CSDR CWSkimmer](https://github.com/luarvique/csdr-cwskimmer)
|
||||
to decode multiple CW signals at once. You can install the
|
||||
`csdr-cwskimmer` package from the OpenWebRX+ repositories.
|
||||
"""
|
||||
return self.command_is_runnable("csdr-cwskimmer -h")
|
||||
|
|
|
|||
|
|
@ -174,6 +174,9 @@ class Modes(object):
|
|||
service=True,
|
||||
squelch=False,
|
||||
),
|
||||
# Replaced by Jakob's RTTY decoder
|
||||
# DigitalMode("mfrtty170", "RTTY-170", underlying=["usb"]),
|
||||
# DigitalMode("mfrtty450", "RTTY-450", underlying=["usb"]),
|
||||
# Replaced by the general paging decoder (both POCSAG and FLEX)
|
||||
# DigitalMode(
|
||||
# "pocsag",
|
||||
|
|
@ -194,9 +197,14 @@ class Modes(object):
|
|||
squelch=False,
|
||||
),
|
||||
DigitalMode("cwdecoder", "CW Decoder", underlying=["usb", "lsb"]),
|
||||
# Replaced by Jakob's RTTY decoder
|
||||
# DigitalMode("mfrtty170", "RTTY-170", underlying=["usb"]),
|
||||
# DigitalMode("mfrtty450", "RTTY-450", underlying=["usb"]),
|
||||
DigitalMode(
|
||||
"cwskimmer",
|
||||
"CW Skimmer",
|
||||
underlying=["usbd"],
|
||||
requirements=["cwskimmer"],
|
||||
service=False,
|
||||
squelch=False,
|
||||
),
|
||||
DigitalMode(
|
||||
"sstv",
|
||||
"SSTV",
|
||||
|
|
|
|||
|
|
@ -414,3 +414,40 @@ class EasParser(TextParser):
|
|||
|
||||
# Return received message as text
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
class CwSkimmerParser(TextParser):
|
||||
def __init__(self, service: bool = False):
|
||||
self.reLine = re.compile("^(\d+):(.+)$")
|
||||
self.freqChanged = False
|
||||
# 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)
|
||||
if len(text) > 0:
|
||||
# Compose output
|
||||
out = { "mode": "CW", "text": text }
|
||||
# Add frequency, if known
|
||||
if self.frequency:
|
||||
out["freq"] = self.frequency + freq
|
||||
# Report frequency changes
|
||||
if self.freqChanged:
|
||||
self.freqChanged = False
|
||||
out["changed"] = True
|
||||
# Done
|
||||
return out
|
||||
# No result
|
||||
return None
|
||||
|
||||
def setDialFrequency(self, frequency: int) -> None:
|
||||
self.freqChanged = frequency != self.frequency
|
||||
super().setDialFrequency(frequency)
|
||||
|
|
|
|||
Loading…
Reference in New Issue