diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js
index 6f35e6b7..06eb8f47 100644
--- a/htdocs/lib/DemodulatorPanel.js
+++ b/htdocs/lib/DemodulatorPanel.js
@@ -169,7 +169,7 @@ DemodulatorPanel.prototype.updatePanels = function() {
// WSJT-X modes share the same panel
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65", "msk144"].indexOf(modulation) >= 0);
// these modes come with their own
- ['js8', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl', 'vdl2'].forEach(function(m) {
+ ['js8', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl', 'vdl2', 'sstv'].forEach(function(m) {
toggle_panel('openwebrx-panel-' + m + '-message', modulation === m);
});
diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js
index 0bb2d5ac..bbe366af 100644
--- a/htdocs/lib/MessagePanel.js
+++ b/htdocs/lib/MessagePanel.js
@@ -809,4 +809,65 @@ $.fn.vdl2MessagePanel = function() {
this.data('panel', new Vdl2MessagePanel(this));
}
return this.data('panel');
+};
+
+SstvMessagePanel = function(el) {
+ MessagePanel.call(this, el);
+ this.currentLine = 0;
+}
+
+SstvMessagePanel.prototype = Object.create(MessagePanel.prototype);
+
+SstvMessagePanel.prototype.render = function() {
+ $(this.el).append($(
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '
'
+ ));
+ this.canvas = $(this.el).find('canvas').get(0)
+ this.context = this.canvas.getContext('2d');
+};
+
+SstvMessagePanel.prototype.supportsMessage = function(message) {
+ return message['mode'] === 'SSTV';
+};
+
+SstvMessagePanel.prototype.pushMessage = function(message) {
+ if ('vis' in message) {
+ console.info("got vis: " + message.vis);
+ }
+ if ('resolution' in message) {
+ console.info("got resolution: ", message.resolution);
+ this.pixels = message.resolution.width;
+ this.lines = message.resolution.height;
+ this.canvas.width = this.pixels;
+ this.canvas.height = this.lines;
+ this.currentLine = 0;
+ }
+ if ('line' in message) {
+ var line = this.context.createImageData(this.pixels, 1);
+ for (var i = 0; i < this.pixels; i++) {
+ line.data[i * 4] = message.line[i * 3];
+ line.data[i * 4 + 1] = message.line[i * 3 + 1];
+ line.data[i * 4 + 2] = message.line[i * 3 + 2];
+ // alpha
+ line.data[i * 4 + 3] = 255;
+ }
+ this.context.putImageData(line, 0, this.currentLine);
+ this.currentLine = (this.currentLine + 1) % this.lines;
+ }
+};
+
+SstvMessagePanel.prototype.clearMessages = function() {
+ this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ this.currentLine = 0;
+};
+
+$.fn.sstvMessagePanel = function() {
+ if (!this.data('panel')) {
+ this.data('panel', new SstvMessagePanel(this));
+ }
+ return this.data('panel');
};
\ No newline at end of file
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js
index 1ab27d3f..4ba10904 100644
--- a/htdocs/openwebrx.js
+++ b/htdocs/openwebrx.js
@@ -865,7 +865,7 @@ function on_ws_recv(evt) {
break;
case 'secondary_demod':
var value = json['value'];
- var panels = ['wsjt', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl', 'vdl2'].map(function(id) {
+ var panels = ['wsjt', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl', 'vdl2', 'sstv'].map(function(id) {
return $('#openwebrx-panel-' + id + '-message')[id + 'MessagePanel']();
});
panels.push($('#openwebrx-panel-js8-message').js8());
@@ -1470,7 +1470,7 @@ function secondary_demod_init() {
.mousedown(secondary_demod_canvas_container_mousedown)
.mouseenter(secondary_demod_canvas_container_mousein)
.mouseleave(secondary_demod_canvas_container_mouseleave);
- ['wsjt', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl'].forEach(function(id){
+ ['wsjt', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl', 'vdl2', 'sstv'].forEach(function(id){
$('#openwebrx-panel-' + id + '-message')[id + 'MessagePanel']();
})
$('#openwebrx-panel-js8-message').js8();
diff --git a/owrx/dsp.py b/owrx/dsp.py
index 625254ea..ba11884c 100644
--- a/owrx/dsp.py
+++ b/owrx/dsp.py
@@ -655,6 +655,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
elif mod == "vdl2":
from csdr.chain.dumpvdl2 import DumpVDL2
return DumpVDL2()
+ elif mod == "sstv":
+ from csdr.chain.sstv import Sstv
+ return Sstv()
def setSecondaryDemodulator(self, mod):
demodulator = self._getSecondaryDemodulator(mod)
diff --git a/owrx/feature.py b/owrx/feature.py
index 93fad5da..f074159c 100644
--- a/owrx/feature.py
+++ b/owrx/feature.py
@@ -92,6 +92,7 @@ class FeatureDetector(object):
"redsea": ["redsea"],
"dab": ["csdreti", "dablin"],
"mqtt": ["paho_mqtt"],
+ "sstv": ["csdrsstv"],
}
def feature_availability(self):
@@ -688,3 +689,20 @@ class FeatureDetector(object):
return True
except ImportError:
return False
+
+ def has_csdrsstv(self):
+ """
+ TODO
+ """
+ required_version = LooseVersion("0.1")
+
+ try:
+ from csdrsstv.modules import csdrsstv_version
+ from csdrsstv.modules import version as pycsdrsstv_version
+
+ return (
+ LooseVersion(csdrsstv_version) >= required_version
+ and LooseVersion(pycsdrsstv_version) >= required_version
+ )
+ except ImportError:
+ return False
diff --git a/owrx/modes.py b/owrx/modes.py
index f5ea047e..30af0511 100644
--- a/owrx/modes.py
+++ b/owrx/modes.py
@@ -140,6 +140,7 @@ class Modes(object):
DigitalMode("rtty170", "RTTY 45/170", underlying=["usb", "lsb"]),
DigitalMode("rtty450", "RTTY 50N/450", underlying=["lsb", "usb"]),
DigitalMode("rtty85", "RTTY 50N/85", underlying=["lsb", "usb"]),
+ DigitalMode("sstv", "SSTV", underlying=["usb", "lsb"], bandpass=Bandpass(1100, 2400)),
WsjtMode("ft8", "FT8"),
WsjtMode("ft4", "FT4"),
WsjtMode("jt65", "JT65"),
diff --git a/owrx/sstv.py b/owrx/sstv.py
new file mode 100644
index 00000000..8175aec9
--- /dev/null
+++ b/owrx/sstv.py
@@ -0,0 +1,61 @@
+from pycsdr.types import Format
+from csdr.module import ThreadModule
+import pickle
+import struct
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class SstvParser(ThreadModule):
+ def __init__(self):
+ super().__init__()
+
+ def run(self):
+ stash = bytes()
+ lines = 0
+ pixels = 0
+ synced = False
+ while self.doRun:
+ data = self.reader.read()
+ if data is None:
+ self.doRun = False
+ else:
+ stash += data
+ while not synced and len(stash) >= 10:
+ synced = stash[:4] == bytes(b"SYNC")
+ if synced:
+ (vis, pixels, lines) = struct.unpack("hhh", stash[4:10])
+ stash = stash[10:]
+ logger.debug("got image data: VIS = %i resolution: %i x %i", vis, pixels, lines)
+ message = {
+ "mode": "SSTV",
+ "vis": vis,
+ "resolution": {
+ "width": pixels,
+ "height": lines
+ }
+ }
+ self.writer.write(pickle.dumps(message))
+ else:
+ logger.debug("search for sync...")
+ # go search for sync... byte by byte.
+ stash = stash[1:]
+ while synced and len(stash) >= pixels * 3:
+ line = [x for x in stash[:pixels * 3]]
+ stash = stash[pixels * 3:]
+ message = {
+ "mode": "SSTV",
+ "line": line,
+ }
+ self.writer.write(pickle.dumps(message))
+ lines -= 1
+ if lines == 0:
+ synced = False
+
+ def getInputFormat(self) -> Format:
+ return Format.CHAR
+
+ def getOutputFormat(self) -> Format:
+ return Format.CHAR