diff --git a/csdr/chain/digimodes.py b/csdr/chain/digimodes.py
index 8b08f37a..0629b401 100644
--- a/csdr/chain/digimodes.py
+++ b/csdr/chain/digimodes.py
@@ -118,7 +118,6 @@ class SstvDemodulator(ServiceDemodulator):
def __init__(self):
self.sampleRate = 48000
workers = [
- Shift(1500.0 / self.sampleRate),
Agc(Format.COMPLEX_FLOAT),
SstvDecoder(self.sampleRate),
SstvParser()
diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css
index 7d529fab..cd44f7d7 100644
--- a/htdocs/css/openwebrx.css
+++ b/htdocs/css/openwebrx.css
@@ -1205,7 +1205,11 @@ img.openwebrx-mirror-img
white-space: pre;
}
-.aprs-symbol {
+#openwebrx-panel-sstv-message .frame {
+ border: 2px dotted white;
+}
+
+ .aprs-symbol {
display: inline-block;
width: 15px;
height: 15px;
diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js
index 68fb3af8..c1be6b89 100644
--- a/htdocs/lib/MessagePanel.js
+++ b/htdocs/lib/MessagePanel.js
@@ -281,7 +281,7 @@ SstvMessagePanel.prototype.render = function() {
$(this.el).append($(
'
' +
'' +
- '| Message | ' +
+ 'TV | ' +
'
' +
'' +
'
'
@@ -290,15 +290,30 @@ SstvMessagePanel.prototype.render = function() {
SstvMessagePanel.prototype.pushMessage = function(msg) {
var $b = $(this.el).find('tbody');
- if(msg.hasOwnProperty('message'))
+ if(msg.hasOwnProperty('message')) {
$b.append($('| ' + msg.message + ' |
'));
- if(msg.width>0 && msg.height>0 && !msg.hasOwnProperty('line')) {
- var $h = 'SCREEN ' + msg.width + "x" + msg.height + '
';
- var $c = '';
- $b.append($('| ' + $h + $c + ' |
'));
+ $b.scrollTop($b[0].scrollHeight);
+ }
+ else if(msg.width>0 && msg.height>0 && !msg.hasOwnProperty('line')) {
+ var h = 'SCREEN ' + msg.width + "x" + msg.height + '
';
+ var c = '';
+ $b.append($('| ' + h + c + ' |
'));
+ $b.scrollTop($b[0].scrollHeight);
+ }
+ else if(msg.width>0 && msg.height>0 && msg.line>=0 && msg.hasOwnProperty('pixels')) {
+ var pixels = atob(msg.pixels);
+ var canvas = $(this.el).find('canvas');
+ var ctx = canvas.getContext("2d");
+ var img = $ctx.createImageData(msg.width, 1);
+ for (var x = 0; x < msg.width; x++) {
+ img.data[x*4 + 0] = pixels.charCodeAt(x*3 + 2);
+ img.data[x*4 + 1] = pixels.charCodeAt(x*3 + 1);
+ img.data[x*4 + 2] = pixels.charCodeAt(x*3 + 0);
+ img.data[x*4 + 3] = 0xFF;
+ }
+ ctx.putImageData(img, 0, msg.line);
}
- $b.scrollTop($b[0].scrollHeight);
};
$.fn.sstvMessagePanel = function() {
diff --git a/owrx/sstv.py b/owrx/sstv.py
index 6fb3d970..f35d3698 100644
--- a/owrx/sstv.py
+++ b/owrx/sstv.py
@@ -1,6 +1,5 @@
from csdr.module import ThreadModule
from pycsdr.types import Format
-from io import BytesIO
import base64
import pickle
@@ -10,9 +9,10 @@ logger = logging.getLogger(__name__)
class SstvParser(ThreadModule):
def __init__(self):
- self.width = 0
+ self.data = bytearray(b'')
+ self.width = 0
self.height = 0
- self.line = 0
+ self.line = 0
super().__init__()
def getInputFormat(self) -> Format:
@@ -22,32 +22,83 @@ class SstvParser(ThreadModule):
return Format.CHAR
def run(self):
+ # Run while there is input data
while self.doRun:
- data = self.reader.read()
- if data is None:
+ # Read input data
+ inp = self.reader.read()
+ # Terminate if no input data
+ if inp is None:
self.doRun = False
break
- out = self.process(data.tobytes())
- self.writer.write(pickle.dumps(out))
+ # Add read data to the buffer
+ self.data = self.data + inp.tobytes()
+ # Process buffer contents
+ out = self.process()
+ # Keep processing while there is input to parse
+ while out is not None:
+ self.writer.write(pickle.dumps(out))
+ out = self.process()
- def process(self, data):
+ def process(self):
try:
- out = { "mode": "SSTV" }
-
- if len(data)==54 and data[0]==ord(b'B') and data[1]==ord(b'M'):
- self.width = data[18] + (data[19]<<8) + (data[20]<<16) + (data[21]<<24)
- self.height = data[22] + (data[23]<<8) + (data[24]<<16) + (data[25]<<24)
+ # Parse bitmap (BMP) file header starting with 'BM'
+ if len(self.data)>=54 and self.data[0]==ord(b'B') and self.data[1]==ord(b'M'):
+ # BMP height value is negative
+ self.width = self.data[18] + (self.data[19]<<8) + (self.data[20]<<16) + (self.data[21]<<24)
+ self.height = -self.data[22] - (self.data[23]<<8) - (self.data[24]<<16) - (self.data[25]<<24)
self.line = 0
- elif self.width>0 and len(data)==self.width*3:
- out["pixels"] = base64.b64encode(data).decode('utf-8')
- out["line"] = self.line
- self.line = self.line + 1
- elif data[0]==ord(b' ') and data[1]==ord(b'['):
- out["message"] = data.decode()
+ logger.warning("@@@ IMAGE %d x %d" % (self.width, self.height))
+ # Remove parsed data
+ del self.data[0:54]
+ # Return parsed values
+ return {
+ "mode": "SSTV",
+ "width": self.width,
+ "height": self.height
+ }
- out["width"] = self.width
- out["height"] = self.height
- return out
+ # Parse debug messages enclosed in ' [...]'
+ elif len(self.data)>=2 and self.data[0]==ord(b' ') and self.data[1]==ord(b'['):
+ # Wait until we find the closing bracket
+ w = self.data.find(b']')
+ if w>=0:
+ logger.warning("@@@ MESSAGE = '%s'" % str(self.data[0:w+1]))
+ # Compose result
+ return {
+ "mode": "SSTV",
+ "message": self.data[0:w+1].decode()
+ }
+ # Remove parsed data
+ del self.data[0:w+1]
+ # Return parsed values
+ return out
+
+ # Parse bitmap file data (scanlines)
+ elif self.width>0 and len(self.data)>=self.width*3:
+ logger.warning("@@@ LINE %d/%d..." % (self.line, self.height))
+ w = self.width * 3
+ # Compose result
+ out = {
+ "mode": "SSTV",
+ "pixels": base64.b64encode(self.data[0:w]).decode()
+ "line": self.line
+ "width": self.width
+ "height": self.height
+ }
+ # Advance scanline
+ self.line = self.line + 1
+ # If we reached the end of frame, finish scan
+ if self.line>=self.height:
+ self.width = 0
+ self.height = 0
+ self.line = 0
+ # Remove parsed data
+ del self.data[0:w]
+ # Return parsed values
+ return out
+
+ # Could not parse input data (yet)
+ return None
except Exception:
logger.exception("Exception while parsing SSTV data")