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($( '' + '' + - '' + - '' + + '' + + '' + '' + '' + '
FreqMessageFreqText
' @@ -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)