diff --git a/csdr/chain/toolbox.py b/csdr/chain/toolbox.py
index f5c089ee..8530f847 100644
--- a/csdr/chain/toolbox.py
+++ b/csdr/chain/toolbox.py
@@ -224,7 +224,7 @@ class RdsDemodulator(ServiceDemodulator, DialFrequencyReceiver):
class CwSkimmerDemodulator(ServiceDemodulator, DialFrequencyReceiver):
- def __init__(self, sampleRate: int = 12000, charCount: int = 4, service: bool = False):
+ def __init__(self, sampleRate: int = 48000, charCount: int = 4, service: bool = False):
self.sampleRate = sampleRate
self.parser = CwSkimmerParser(service)
workers = [
diff --git a/csdr/module/toolbox.py b/csdr/module/toolbox.py
index 76a6bf5a..86682d82 100644
--- a/csdr/module/toolbox.py
+++ b/csdr/module/toolbox.py
@@ -120,7 +120,7 @@ class AcarsDecModule(WavFileModule):
class CwSkimmerModule(ExecModule):
- def __init__(self, sampleRate: int = 12000, charCount: int = 4):
+ 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)
diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css
index 0bd0443d..aa56c74f 100644
--- a/htdocs/css/openwebrx.css
+++ b/htdocs/css/openwebrx.css
@@ -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;
diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js
index 1f8144ff..77c1fcb2 100644
--- a/htdocs/lib/MessagePanel.js
+++ b/htdocs/lib/MessagePanel.js
@@ -881,7 +881,6 @@ $.fn.faxMessagePanel = function() {
CwSkimmerMessagePanel = function(el) {
MessagePanel.call(this, el);
this.initClearTimer();
- this.freqs = [];
this.texts = [];
}
@@ -895,8 +894,8 @@ CwSkimmerMessagePanel.prototype.render = function() {
$(this.el).append($(
'
' +
'' +
- '| Freq | ' +
- 'Message | ' +
+ 'Freq | ' +
+ 'Text | ' +
'
' +
'' +
'
'
@@ -907,30 +906,40 @@ CwSkimmerMessagePanel.prototype.pushMessage = function(msg) {
// Must have some text
if (!msg.text) return;
- // Find existing frequency
- var j = this.freqs.indexOf(msg.freq);
- if (j >= 0) {
+ // 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] = (this.texts[j] + msg.text).slice(-64);
+ this.texts[j].text = (this.texts[j].text + msg.text).slice(-64);
+ this.texts[j].ts = now;
} else {
- // Add a new entry
- this.freqs.push(msg.freq);
- this.texts.push(msg.text);
- // Limit the number of active frequencies
-// if (this.freqs.length > 16) {
-// this.freqs.shift();
-// this.texts.shift();
-// }
+ // Insert a new entry
+ this.texts.splice(j, 0, { freq: msg.freq, text: msg.text, ts: now });
}
// Generate table body
var body = '';
- for (j = 0 ; j < this.freqs.length ; j++) {
- body +=
- '| ' + Math.round(this.freqs[j]/10)/100 +
- ' | ' +
- this.texts[j] + ' |
\n';
+ 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 +=
+ '| ' + f.toFixed(1) +
+ ' | ' + this.texts[j].text + ' |
\n';
+ }
}
// Assign new table body
diff --git a/owrx/feature.py b/owrx/feature.py
index b3f9c254..8514e5ec 100644
--- a/owrx/feature.py
+++ b/owrx/feature.py
@@ -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")
diff --git a/owrx/modes.py b/owrx/modes.py
index 21d804c3..fda42e65 100644
--- a/owrx/modes.py
+++ b/owrx/modes.py
@@ -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,10 +197,14 @@ 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"]),
+ DigitalMode(
+ "cwskimmer",
+ "CW Skimmer",
+ underlying=["usbd"],
+ requirements=["cwskimmer"],
+ service=False,
+ squelch=False,
+ ),
DigitalMode(
"sstv",
"SSTV",
diff --git a/owrx/toolbox.py b/owrx/toolbox.py
index d5553b3c..935405d4 100644
--- a/owrx/toolbox.py
+++ b/owrx/toolbox.py
@@ -419,6 +419,7 @@ class EasParser(TextParser):
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)
@@ -438,7 +439,15 @@ class CwSkimmerParser(TextParser):
# 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)