mirror of
https://github.com/jketterl/openwebrx.git
synced 2026-01-28 03:14:24 +01:00
first implementation of sstv decoding
This commit is contained in:
parent
4d1fdf08a0
commit
f2532842b5
16
csdr/chain/sstv.py
Normal file
16
csdr/chain/sstv.py
Normal 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
|
||||
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
61
owrx/sstv.py
Normal 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
|
||||
Loading…
Reference in a new issue