From f8dcff788b17424bdab01a3b8ffe7c387c6a0f7b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 23 Sep 2019 22:45:55 +0200 Subject: [PATCH] build valid packets (hopefully) --- owrx/pskreporter.py | 135 +++++++++++++++++++++++++++++++++++++++++++- owrx/wsjt.py | 10 ++-- 2 files changed, 138 insertions(+), 7 deletions(-) diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index b92d5e3..c966025 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -3,6 +3,8 @@ import threading import time import random from sched import scheduler +from owrx.config import PropertyManager +from owrx.version import openwebrx_version logger = logging.getLogger(__name__) @@ -10,7 +12,8 @@ logger = logging.getLogger(__name__) class PskReporter(object): sharedInstance = None creationLock = threading.Lock() - interval = 300 + interval = 60 + supportedModes = ["FT8", "FT4", "JT9", "JT65"] @staticmethod def getSharedInstance(): @@ -22,6 +25,7 @@ class PskReporter(object): def __init__(self): self.spots = [] self.spotLock = threading.Lock() + self.uploader = Uploader() self.scheduler = scheduler(time.time, time.sleep) self.scheduleNextUpload() threading.Thread(target=self.scheduler.run).start() @@ -32,6 +36,8 @@ class PskReporter(object): self.scheduler.enter(delay, 1, self.upload) def spot(self, spot): + if not spot["mode"] in PskReporter.supportedModes: + return with self.spotLock: self.spots.append(spot) @@ -41,6 +47,131 @@ class PskReporter(object): self.spots = [] if spots: - logger.debug("would now upload %i spots", len(spots)) + self.uploader.upload(spots) self.scheduleNextUpload() + + +class Uploader(object): + receieverDelimiter = [0x99, 0x92] + senderDelimiter = [0x99, 0x93] + + def __init__(self): + self.sequence = 0 + + def upload(self, spots): + logger.debug("would now upload %i spots", len(spots)) + for packet in self.getPackets(spots): + l = int.from_bytes(packet[2:4], "big") + logger.debug("packet length: %i; indicated length: %i", len(packet), l) + logger.debug(packet) + # TODO actually send the packet + + def getPackets(self, spots): + encoded = [self.encodeSpot(spot) for spot in spots] + + def chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i : i + n] + + rHeader = self.getReceiverInformationHeader() + rInfo = self.getReceiverInformation() + sHeader = self.getSenderInformationHeader() + + packets = [] + # 50 seems to be a safe bet + for chunk in chunks(encoded, 50): + sInfoLength = sum(map(len, chunk)) + length = sInfoLength + 16 + len(rHeader) + len(sHeader) + len(rInfo) + 4 + header = self.getHeader(length) + packets.append( + header + + rHeader + + sHeader + + rInfo + + bytes(Uploader.senderDelimiter) + + sInfoLength.to_bytes(2, "big") + + b"".join(chunk) + ) + + return packets + + def getHeader(self, length): + self.sequence += 1 + return bytes( + # protocol version + [0x00, 0x0A] + + list(length.to_bytes(2, "big")) + + list(int(time.time()).to_bytes(4, "big")) + + list(self.sequence.to_bytes(4, "big")) + + list((id(self) & 0xFFFFFFFF).to_bytes(4, "big")) + ) + + def encodeString(self, s): + return [len(s)] + list(s.encode("utf-8")) + + def encodeSpot(self, spot): + return bytes( + self.encodeString(spot["callsign"]) + + list(spot["freq"].to_bytes(4, "big")) + + list(int(spot["db"]).to_bytes(1, "big", signed=True)) + + self.encodeString(spot["mode"]) + + self.encodeString(spot["locator"]) + # informationsource. 1 means "automatically extracted + + [0x01] + + list(int(spot["timestamp"] / 1000).to_bytes(4, "big")) + ) + + def getReceiverInformationHeader(self): + return bytes( + # id, length + [0x00, 0x03, 0x00, 0x24] + + Uploader.receieverDelimiter + # number of fields + + [0x00, 0x03, 0x00, 0x00] + # receiverCallsign + + [0x80, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # receiverLocator + + [0x80, 0x04, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # decodingSoftware + + [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # padding + + [0x00, 0x00] + ) + + def getReceiverInformation(self): + pm = PropertyManager.getSharedInstance() + callsign = pm["pskreporter_callsign"] + locator = pm["receiver_qra"] + decodingSoftware = "OpenWebRX " + openwebrx_version + body = [b for s in [callsign, locator, decodingSoftware] for b in self.encodeString(s)] + body = self.pad(body, 4) + body = bytes(Uploader.receieverDelimiter + list((len(body) + 4).to_bytes(2, "big")) + body) + return body + + def getSenderInformationHeader(self): + return bytes( + # id, length + [0x00, 0x02, 0x00, 0x3C] + + Uploader.senderDelimiter + # number of fields + + [0x00, 0x07] + # senderCallsign + + [0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # frequency + + [0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F] + # sNR + + [0x80, 0x05, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] + # mode + + [0x80, 0x0A, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # senderLocator + + [0x80, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # informationSource + + [0x80, 0x0B, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] + # flowStartSeconds + + [0x00, 0x96, 0x00, 0x04] + ) + + def pad(self, bytes, l): + return bytes + [0x00 for _ in range(0, -1 * len(bytes) % l)] diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 2772364..21fefb0 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -282,7 +282,7 @@ class WsjtParser(object): decoder = Jt9Decoder() else: decoder = WsprDecoder() - out = decoder.parse(msg) + out = decoder.parse(msg, self.dial_freq) if "mode" in out: self.pushDecode(out["mode"]) if "callsign" in out and "locator" in out: @@ -328,7 +328,7 @@ class Decoder(object): class Jt9Decoder(Decoder): locator_pattern = re.compile("[A-Z0-9]+\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") - def parse(self, msg): + def parse(self, msg, dial_freq): # ft8 sample # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' # jt65 sample @@ -349,7 +349,7 @@ class Jt9Decoder(Decoder): "timestamp": timestamp, "db": float(msg[0:3]), "dt": float(msg[4:8]), - "freq": int(msg[9:13]), + "freq": dial_freq + int(msg[9:13]), "mode": mode, "msg": wsjt_msg, } @@ -370,7 +370,7 @@ class Jt9Decoder(Decoder): class WsprDecoder(Decoder): wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") - def parse(self, msg): + def parse(self, msg, dial_freq): # wspr sample # '2600 -24 0.4 0.001492 -1 G8AXA JO01 33' # '0052 -29 2.6 0.001486 0 G02CWT IO92 23' @@ -379,7 +379,7 @@ class WsprDecoder(Decoder): "timestamp": self.parse_timestamp(msg[0:4], "%H%M"), "db": float(msg[5:8]), "dt": float(msg[9:13]), - "freq": float(msg[14:24]), + "freq": dial_freq + int(float(msg[14:24]) * 1e6), "drift": int(msg[25:28]), "mode": "WSPR", "msg": wsjt_msg,