From b26960e1abb989577c4818a09bf5d5321834ea1e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 16 Feb 2024 22:47:25 +0100 Subject: [PATCH] add a tap for image data into MQTT --- csdr/chain/sstv.py | 24 +++++++++++- owrx/feature.py | 11 ++++++ owrx/{sstv.py => sstv/__init__.py} | 63 ++++++++++++++++++++---------- owrx/sstv/png.py | 48 +++++++++++++++++++++++ 4 files changed, 125 insertions(+), 21 deletions(-) rename owrx/{sstv.py => sstv/__init__.py} (51%) create mode 100644 owrx/sstv/png.py diff --git a/csdr/chain/sstv.py b/csdr/chain/sstv.py index b559bf79..2f62dd1c 100644 --- a/csdr/chain/sstv.py +++ b/csdr/chain/sstv.py @@ -1,16 +1,38 @@ from owrx.sstv import SstvParser from csdr.chain.demodulator import SecondaryDemodulator, FixedAudioRateChain -from pycsdr.modules import FmDemod +from pycsdr.modules import FmDemod, Buffer +from pycsdr.types import Format from csdrsstv.modules import SstvDecoder +from owrx.feature import FeatureDetector +from typing import Optional class Sstv(SecondaryDemodulator, FixedAudioRateChain): def __init__(self): + self.imageBuffer = Buffer(Format.CHAR) super().__init__([ FmDemod(), SstvDecoder(), SstvParser(), ]) + self.pngAdapter = None + # tap into the pipeline to be able to send images off to MQTT + if FeatureDetector().is_available("png"): + # local import due to optional features + from owrx.sstv.png import PngAdapter + self.pngAdapter = PngAdapter() + self.pngAdapter.setReader(self.imageBuffer.getReader()) + + def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None: + if isinstance(w1, SstvDecoder): + buffer = self.imageBuffer + super()._connect(w1, w2, buffer) + + def stop(self): + if self.pngAdapter is not None: + self.pngAdapter.stop() + self.pngAdapter = None + super().stop() def getFixedAudioRate(self) -> int: return 12000 diff --git a/owrx/feature.py b/owrx/feature.py index f074159c..2ecee052 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -93,6 +93,7 @@ class FeatureDetector(object): "dab": ["csdreti", "dablin"], "mqtt": ["paho_mqtt"], "sstv": ["csdrsstv"], + "png": ["pypng"], } def feature_availability(self): @@ -706,3 +707,13 @@ class FeatureDetector(object): ) except ImportError: return False + + def has_pypng(self): + """ + TODO + """ + try: + import png + return True + except ImportError: + return False diff --git a/owrx/sstv.py b/owrx/sstv/__init__.py similarity index 51% rename from owrx/sstv.py rename to owrx/sstv/__init__.py index 8175aec9..086f1d6b 100644 --- a/owrx/sstv.py +++ b/owrx/sstv/__init__.py @@ -2,16 +2,15 @@ from pycsdr.types import Format from csdr.module import ThreadModule import pickle import struct +from abc import ABCMeta, abstractmethod +from typing import List import logging logger = logging.getLogger(__name__) -class SstvParser(ThreadModule): - def __init__(self): - super().__init__() - +class ImageParser(ThreadModule, metaclass=ABCMeta): def run(self): stash = bytes() lines = 0 @@ -28,34 +27,58 @@ class SstvParser(ThreadModule): 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)) + self.startImage(vis, pixels, lines) 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)) + self.processLine(line) lines -= 1 if lines == 0: + self.finishImage() synced = False def getInputFormat(self) -> Format: return Format.CHAR + @abstractmethod + def startImage(self, vis: int, pixels: int, lines: int) -> None: + pass + + @abstractmethod + def processLine(self, line: List[int]) -> None: + pass + + @abstractmethod + def finishImage(self): + pass + + +class SstvParser(ImageParser): def getOutputFormat(self) -> Format: return Format.CHAR + + def startImage(self, vis: int, pixels: int, lines: int) -> None: + 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)) + + def processLine(self, line: List[int]) -> None: + message = { + "mode": "SSTV", + "line": line, + } + self.writer.write(pickle.dumps(message)) + + def finishImage(self): + pass + diff --git a/owrx/sstv/png.py b/owrx/sstv/png.py new file mode 100644 index 00000000..0aa657b1 --- /dev/null +++ b/owrx/sstv/png.py @@ -0,0 +1,48 @@ +from pycsdr.types import Format +from owrx.sstv import ImageParser +from typing import List +from io import BytesIO +from owrx.reporting import ReportingEngine +import png +import base64 + + +class PngAdapter(ImageParser): + def __init__(self): + self.vis = 0 + self.pixels = 0 + self.lines = 0 + self.image_data = [] + super().__init__() + + def getOutputFormat(self) -> Format: + return Format.CHAR + + def startImage(self, vis: int, pixels: int, lines: int) -> None: + self.image_data = [] + self.vis = vis + self.pixels = pixels + self.lines = lines + + def processLine(self, line: List[int]) -> None: + self.image_data += [line] + + def finishImage(self): + f = BytesIO() + writer = png.Writer(self.pixels, self.lines, greyscale=False) + writer.write(f, self.image_data) + image_binary = bytes(f.getbuffer()) + if self.writer is not None: + self.writer.write(image_binary) + spot = { + "mode": "SSTV", + "vis": self.vis, + "image": base64.b64encode(image_binary).decode('ascii'), + } + ReportingEngine.getSharedInstance().spot(spot) + f.close() + + def _checkStart(self) -> None: + # we don't need a writer + if self.reader is not None: + self.start()