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