first implementation of sstv decoding

This commit is contained in:
Jakob Ketterl 2024-02-16 15:37:17 +01:00
parent 4d1fdf08a0
commit f2532842b5
10 changed files with 177 additions and 3 deletions

16
csdr/chain/sstv.py Normal file
View file

@ -0,0 +1,16 @@
from owrx.sstv import SstvParser
from csdr.chain.demodulator import SecondaryDemodulator, FixedAudioRateChain
from pycsdr.modules import FmDemod
from csdrsstv.modules import SstvDecoder
class Sstv(SecondaryDemodulator, FixedAudioRateChain):
def __init__(self):
super().__init__([
FmDemod(),
SstvDecoder(),
SstvParser(),
])
def getFixedAudioRate(self) -> int:
return 12000

View file

@ -1440,3 +1440,16 @@ img.openwebrx-mirror-img
.under-construction:hover .under-construction-description {
max-height: 500px;
}
.openwebrx-message-panel#openwebrx-panel-sstv-message {
min-height: initial;
}
.sstv-canvas-container {
width: 320px;
height: 256px;
}
.sstv-canvas-container canvas {
width: 100%;
}

View file

@ -81,6 +81,7 @@
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-ism-message" style="display: none; width: 619px;" data-panel-name="ism-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-hfdl-message" style="display: none; width: 619px;" data-panel-name="hfdl-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-vdl2-message" style="display: none; width: 619px;" data-panel-name="vdl2-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-sstv-message" style="display: none" data-panel-name="sstv-message"></div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-m17" style="display: none;" data-panel-name="metadata-m17">
<div class="openwebrx-meta-slot">
<div class="openwebrx-meta-user-image">

View file

@ -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);
});

View file

@ -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($(
'<div class="sstv-container">' +
'<div class="sstv-canvas-container">' +
'<canvas class="sstv-canvas" width="320" height="256"></canvas>' +
'</div>' +
'</div>'
));
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');
};

View file

@ -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();

View file

@ -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)

View file

@ -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

View file

@ -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"),

61
owrx/sstv.py Normal file
View file

@ -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