Merge branch 'develop' into m17
This commit is contained in:
@ -56,7 +56,8 @@ Support and info: https://groups.io/g/openwebrx
|
||||
return
|
||||
|
||||
# Get error messages about unknown / unavailable features as soon as possible
|
||||
SdrService.loadProps()
|
||||
# start up "always-on" sources right away
|
||||
SdrService.getSources()
|
||||
|
||||
Services.start()
|
||||
|
||||
|
@ -186,8 +186,8 @@ class AudioWriter(object):
|
||||
)
|
||||
try:
|
||||
for line in decoder.stdout:
|
||||
self.outputWriter.send((job.freq, line))
|
||||
except OSError:
|
||||
self.outputWriter.send((self.profile, job.freq, line))
|
||||
except (OSError, AttributeError):
|
||||
decoder.stdout.flush()
|
||||
# TODO uncouple parsing from the output so that decodes can still go to the map and the spotters
|
||||
logger.debug("output has gone away while decoding job.")
|
||||
|
@ -124,6 +124,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
|
||||
"initial_squelch_level",
|
||||
"profile_id",
|
||||
"squelch_auto_margin",
|
||||
"frequency_display_precision",
|
||||
]
|
||||
|
||||
def __init__(self, conn):
|
||||
@ -425,7 +426,12 @@ class MapConnection(OpenWebRxClient):
|
||||
super().__init__(conn)
|
||||
|
||||
pm = Config.get()
|
||||
self.write_config(pm.filter("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__())
|
||||
self.write_config(pm.filter(
|
||||
"google_maps_api_key",
|
||||
"receiver_gps",
|
||||
"map_position_retention_time",
|
||||
"receiver_name",
|
||||
).__dict__())
|
||||
|
||||
Map.getSharedInstance().addClient(self)
|
||||
|
||||
@ -453,7 +459,7 @@ class WebSocketMessageHandler(object):
|
||||
self.handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)}
|
||||
|
||||
conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version))
|
||||
logger.debug("client connection intitialized")
|
||||
logger.debug("client connection initialized")
|
||||
|
||||
if "type" in self.handshake:
|
||||
if self.handshake["type"] == "receiver":
|
||||
|
@ -126,6 +126,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
|
||||
"lib/ProgressBar.js",
|
||||
"lib/Measurement.js",
|
||||
"lib/FrequencyDisplay.js",
|
||||
"lib/MessagePanel.js",
|
||||
"lib/Js8Threads.js",
|
||||
"lib/Modes.js",
|
||||
],
|
||||
|
@ -28,7 +28,7 @@ class Js8Profiles(object):
|
||||
|
||||
|
||||
class Js8Profile(AudioChopperProfile, metaclass=ABCMeta):
|
||||
def decoding_depth(self, mode):
|
||||
def decoding_depth(self):
|
||||
pm = Config.get()
|
||||
# return global default
|
||||
if "js8_decoding_depth" in pm:
|
||||
@ -40,7 +40,7 @@ class Js8Profile(AudioChopperProfile, metaclass=ABCMeta):
|
||||
return "%y%m%d_%H%M%S"
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["js8", "--js8", "-b", self.get_sub_mode(), "-d", str(self.decoding_depth("js8")), file]
|
||||
return ["js8", "--js8", "-b", self.get_sub_mode(), "-d", str(self.decoding_depth()), file]
|
||||
|
||||
@abstractmethod
|
||||
def get_sub_mode(self):
|
||||
@ -85,7 +85,7 @@ class Js8Parser(Parser):
|
||||
def parse(self, messages):
|
||||
for raw in messages:
|
||||
try:
|
||||
freq, raw_msg = raw
|
||||
profile, freq, raw_msg = raw
|
||||
self.setDialFrequency(freq)
|
||||
msg = raw_msg.decode().rstrip()
|
||||
if Js8Parser.decoderRegex.match(msg):
|
||||
|
@ -81,11 +81,12 @@ class PskReporter(object):
|
||||
else:
|
||||
self.spotCounter.inc()
|
||||
self.spots.append(spot)
|
||||
self.scheduleNextUpload()
|
||||
self.scheduleNextUpload()
|
||||
|
||||
def upload(self):
|
||||
try:
|
||||
with self.spotLock:
|
||||
self.timer = None
|
||||
spots = self.spots
|
||||
self.spots = []
|
||||
|
||||
@ -94,9 +95,6 @@ class PskReporter(object):
|
||||
except Exception:
|
||||
logger.exception("Failed to upload spots")
|
||||
|
||||
self.timer = None
|
||||
self.scheduleNextUpload()
|
||||
|
||||
def cancelTimer(self):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
@ -117,6 +115,8 @@ class Uploader(object):
|
||||
|
||||
def getPackets(self, spots):
|
||||
encoded = [self.encodeSpot(spot) for spot in spots]
|
||||
# filter out any erroneous encodes
|
||||
encoded = [e for e in encoded if e is not None]
|
||||
|
||||
def chunks(l, n):
|
||||
"""Yield successive n-sized chunks from l."""
|
||||
@ -152,40 +152,63 @@ class Uploader(object):
|
||||
return [len(s)] + list(s.encode("utf-8"))
|
||||
|
||||
def encodeSpot(self, spot):
|
||||
return bytes(
|
||||
self.encodeString(spot["callsign"])
|
||||
+ list(int(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"))
|
||||
)
|
||||
try:
|
||||
return bytes(
|
||||
self.encodeString(spot["callsign"])
|
||||
+ list(int(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"))
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error while encoding spot for pskreporter")
|
||||
return None
|
||||
|
||||
def getReceiverInformationHeader(self):
|
||||
pm = Config.get()
|
||||
with_antenna = "pskreporter_antenna_information" in pm and pm["pskreporter_antenna_information"] is not None
|
||||
num_fields = 4 if with_antenna else 3
|
||||
length = 12 + num_fields * 8
|
||||
return bytes(
|
||||
# id, length
|
||||
[0x00, 0x03, 0x00, 0x24]
|
||||
# id
|
||||
[0x00, 0x03]
|
||||
# length
|
||||
+ list(length.to_bytes(2, 'big'))
|
||||
+ Uploader.receieverDelimiter
|
||||
# number of fields
|
||||
+ [0x00, 0x03, 0x00, 0x00]
|
||||
+ list(num_fields.to_bytes(2, 'big'))
|
||||
# padding
|
||||
+ [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]
|
||||
# antennaInformation
|
||||
+ (
|
||||
[0x80, 0x09, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] if with_antenna else []
|
||||
)
|
||||
# padding
|
||||
+ [0x00, 0x00]
|
||||
)
|
||||
|
||||
def getReceiverInformation(self):
|
||||
pm = Config.get()
|
||||
callsign = pm["pskreporter_callsign"]
|
||||
locator = Locator.fromCoordinates(pm["receiver_gps"])
|
||||
decodingSoftware = "OpenWebRX " + openwebrx_version
|
||||
body = [b for s in [callsign, locator, decodingSoftware] for b in self.encodeString(s)]
|
||||
bodyFields = [
|
||||
# callsign
|
||||
pm["pskreporter_callsign"],
|
||||
# locator
|
||||
Locator.fromCoordinates(pm["receiver_gps"]),
|
||||
# decodingSoftware
|
||||
"OpenWebRX " + openwebrx_version,
|
||||
]
|
||||
if "pskreporter_antenna_information" in pm and pm["pskreporter_antenna_information"] is not None:
|
||||
bodyFields += [pm["pskreporter_antenna_information"]]
|
||||
body = [b for s in bodyFields 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
|
||||
|
@ -13,7 +13,7 @@ class SdrService(object):
|
||||
lastPort = None
|
||||
|
||||
@staticmethod
|
||||
def loadProps():
|
||||
def _loadProps():
|
||||
if SdrService.sdrProps is None:
|
||||
pm = Config.get()
|
||||
featureDetector = FeatureDetector()
|
||||
@ -60,7 +60,6 @@ class SdrService(object):
|
||||
|
||||
@staticmethod
|
||||
def getSource(id):
|
||||
SdrService.loadProps()
|
||||
sources = SdrService.getSources()
|
||||
if not sources:
|
||||
return None
|
||||
@ -70,7 +69,7 @@ class SdrService(object):
|
||||
|
||||
@staticmethod
|
||||
def getSources():
|
||||
SdrService.loadProps()
|
||||
SdrService._loadProps()
|
||||
for id in SdrService.sdrProps.keys():
|
||||
if not id in SdrService.sources:
|
||||
props = SdrService.sdrProps[id]
|
||||
|
152
owrx/wsjt.py
152
owrx/wsjt.py
@ -14,8 +14,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta):
|
||||
def decoding_depth(self, mode):
|
||||
def decoding_depth(self):
|
||||
pm = Config.get()
|
||||
mode = self.getMode().lower()
|
||||
# mode-specific setting?
|
||||
if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]:
|
||||
return pm["wsjt_decoding_depths"][mode]
|
||||
@ -25,64 +26,76 @@ class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta):
|
||||
# default when no setting is provided
|
||||
return 3
|
||||
|
||||
def getTimestampFormat(self):
|
||||
if self.getInterval() < 60:
|
||||
return "%H%M%S"
|
||||
return "%H%M"
|
||||
|
||||
def getFileTimestampFormat(self):
|
||||
return "%y%m%d_" + self.getTimestampFormat()
|
||||
|
||||
@abstractmethod
|
||||
def getMode(self):
|
||||
pass
|
||||
|
||||
|
||||
class Ft8Profile(WsjtProfile):
|
||||
def getInterval(self):
|
||||
return 15
|
||||
|
||||
def getFileTimestampFormat(self):
|
||||
return "%y%m%d_%H%M%S"
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["jt9", "--ft8", "-d", str(self.decoding_depth("ft8")), file]
|
||||
return ["jt9", "--ft8", "-d", str(self.decoding_depth()), file]
|
||||
|
||||
def getMode(self):
|
||||
return "FT8"
|
||||
|
||||
|
||||
class WsprProfile(WsjtProfile):
|
||||
def getInterval(self):
|
||||
return 120
|
||||
|
||||
def getFileTimestampFormat(self):
|
||||
return "%y%m%d_%H%M"
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
cmd = ["wsprd"]
|
||||
if self.decoding_depth("wspr") > 1:
|
||||
if self.decoding_depth() > 1:
|
||||
cmd += ["-d"]
|
||||
cmd += [file]
|
||||
return cmd
|
||||
|
||||
def getMode(self):
|
||||
return "WSPR"
|
||||
|
||||
|
||||
class Jt65Profile(WsjtProfile):
|
||||
def getInterval(self):
|
||||
return 60
|
||||
|
||||
def getFileTimestampFormat(self):
|
||||
return "%y%m%d_%H%M"
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["jt9", "--jt65", "-d", str(self.decoding_depth("jt65")), file]
|
||||
return ["jt9", "--jt65", "-d", str(self.decoding_depth()), file]
|
||||
|
||||
def getMode(self):
|
||||
return "JT65"
|
||||
|
||||
|
||||
class Jt9Profile(WsjtProfile):
|
||||
def getInterval(self):
|
||||
return 60
|
||||
|
||||
def getFileTimestampFormat(self):
|
||||
return "%y%m%d_%H%M"
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["jt9", "--jt9", "-d", str(self.decoding_depth("jt9")), file]
|
||||
return ["jt9", "--jt9", "-d", str(self.decoding_depth()), file]
|
||||
|
||||
def getMode(self):
|
||||
return "JT9"
|
||||
|
||||
|
||||
class Ft4Profile(WsjtProfile):
|
||||
def getInterval(self):
|
||||
return 7.5
|
||||
|
||||
def getFileTimestampFormat(self):
|
||||
return "%y%m%d_%H%M%S"
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["jt9", "--ft4", "-d", str(self.decoding_depth("ft4")), file]
|
||||
return ["jt9", "--ft4", "-d", str(self.decoding_depth()), file]
|
||||
|
||||
def getMode(self):
|
||||
return "FT4"
|
||||
|
||||
|
||||
class Fst4Profile(WsjtProfile):
|
||||
@ -94,13 +107,11 @@ class Fst4Profile(WsjtProfile):
|
||||
def getInterval(self):
|
||||
return self.interval
|
||||
|
||||
def getFileTimestampFormat(self):
|
||||
if self.interval < 60:
|
||||
return "%y%m%d_%H%M%S"
|
||||
return "%y%m%d_%H%M"
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["jt9", "--fst4", "-p", str(self.interval), "-d", str(self.decoding_depth("fst4")), file]
|
||||
return ["jt9", "--fst4", "-p", str(self.interval), "-d", str(self.decoding_depth()), file]
|
||||
|
||||
def getMode(self):
|
||||
return "FST4"
|
||||
|
||||
@staticmethod
|
||||
def getEnabledProfiles():
|
||||
@ -118,13 +129,11 @@ class Fst4wProfile(WsjtProfile):
|
||||
def getInterval(self):
|
||||
return self.interval
|
||||
|
||||
def getFileTimestampFormat(self):
|
||||
if self.interval < 60:
|
||||
return "%y%m%d_%H%M%S"
|
||||
return "%y%m%d_%H%M"
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["jt9", "--fst4w", "-p", str(self.interval), "-d", str(self.decoding_depth("fst4w")), file]
|
||||
return ["jt9", "--fst4w", "-p", str(self.interval), "-d", str(self.decoding_depth()), file]
|
||||
|
||||
def getMode(self):
|
||||
return "FST4W"
|
||||
|
||||
@staticmethod
|
||||
def getEnabledProfiles():
|
||||
@ -134,12 +143,10 @@ class Fst4wProfile(WsjtProfile):
|
||||
|
||||
|
||||
class WsjtParser(Parser):
|
||||
modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4", "`": "FST4"}
|
||||
|
||||
def parse(self, messages):
|
||||
for data in messages:
|
||||
try:
|
||||
freq, raw_msg = data
|
||||
profile, freq, raw_msg = data
|
||||
self.setDialFrequency(freq)
|
||||
msg = raw_msg.decode().rstrip()
|
||||
# known debug messages we know to skip
|
||||
@ -148,19 +155,20 @@ class WsjtParser(Parser):
|
||||
if msg.startswith(" EOF on input file"):
|
||||
return
|
||||
|
||||
modes = list(WsjtParser.modes.keys())
|
||||
if msg[21] in modes or msg[19] in modes:
|
||||
decoder = Jt9Decoder()
|
||||
mode = profile.getMode()
|
||||
if mode == "WSPR":
|
||||
decoder = WsprDecoder(profile)
|
||||
else:
|
||||
decoder = WsprDecoder()
|
||||
decoder = Jt9Decoder(profile)
|
||||
out = decoder.parse(msg, freq)
|
||||
if "mode" in out:
|
||||
self.pushDecode(out["mode"])
|
||||
if "callsign" in out and "locator" in out:
|
||||
Map.getSharedInstance().updateLocation(
|
||||
out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band
|
||||
)
|
||||
PskReporter.getSharedInstance().spot(out)
|
||||
out["mode"] = mode
|
||||
|
||||
self.pushDecode(mode)
|
||||
if "callsign" in out and "locator" in out:
|
||||
Map.getSharedInstance().updateLocation(
|
||||
out["callsign"], LocatorLocation(out["locator"]), mode, self.band
|
||||
)
|
||||
PskReporter.getSharedInstance().spot(out)
|
||||
|
||||
self.handler.write_wsjt_message(out)
|
||||
except (ValueError, IndexError):
|
||||
@ -187,13 +195,21 @@ class WsjtParser(Parser):
|
||||
|
||||
|
||||
class Decoder(ABC):
|
||||
locator_pattern = re.compile(".*\\s([A-Z0-9]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$")
|
||||
locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$")
|
||||
|
||||
def parse_timestamp(self, instring, dateformat):
|
||||
ts = datetime.strptime(instring, dateformat)
|
||||
return int(
|
||||
datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000
|
||||
)
|
||||
def __init__(self, profile):
|
||||
self.profile = profile
|
||||
|
||||
def parse_timestamp(self, instring):
|
||||
dateformat = self.profile.getTimestampFormat()
|
||||
remain = instring[len(dateformat) + 1:]
|
||||
try:
|
||||
ts = datetime.strptime(instring[0:len(dateformat)], dateformat)
|
||||
return remain, int(
|
||||
datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000
|
||||
)
|
||||
except ValueError:
|
||||
return remain, None
|
||||
|
||||
@abstractmethod
|
||||
def parse(self, msg, dial_freq):
|
||||
@ -219,18 +235,7 @@ class Jt9Decoder(Decoder):
|
||||
# '0003 -4 0.4 1762 # CQ R2ABM KO85'
|
||||
# fst4 sample
|
||||
# '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV'
|
||||
modes = list(WsjtParser.modes.keys())
|
||||
if msg[19] in modes:
|
||||
dateformat = "%H%M"
|
||||
else:
|
||||
dateformat = "%H%M%S"
|
||||
try:
|
||||
timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat)
|
||||
except ValueError:
|
||||
timestamp = None
|
||||
msg = msg[len(dateformat) + 1:]
|
||||
modeChar = msg[14:15]
|
||||
mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown"
|
||||
msg, timestamp = self.parse_timestamp(msg)
|
||||
wsjt_msg = msg[17:53].strip()
|
||||
|
||||
result = {
|
||||
@ -238,7 +243,6 @@ class Jt9Decoder(Decoder):
|
||||
"db": float(msg[0:3]),
|
||||
"dt": float(msg[4:8]),
|
||||
"freq": dial_freq + int(msg[9:13]),
|
||||
"mode": mode,
|
||||
"msg": wsjt_msg,
|
||||
}
|
||||
result.update(self.parseMessage(wsjt_msg))
|
||||
@ -246,20 +250,20 @@ class Jt9Decoder(Decoder):
|
||||
|
||||
|
||||
class WsprDecoder(Decoder):
|
||||
wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")
|
||||
wspr_splitter_pattern = re.compile("([A-Z0-9/]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")
|
||||
|
||||
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'
|
||||
wsjt_msg = msg[29:].strip()
|
||||
msg, timestamp = self.parse_timestamp(msg)
|
||||
wsjt_msg = msg[24:].strip()
|
||||
result = {
|
||||
"timestamp": self.parse_timestamp(msg[0:4], "%H%M"),
|
||||
"db": float(msg[5:8]),
|
||||
"dt": float(msg[9:13]),
|
||||
"freq": dial_freq + int(float(msg[14:24]) * 1e6),
|
||||
"drift": int(msg[25:28]),
|
||||
"mode": "WSPR",
|
||||
"timestamp": timestamp,
|
||||
"db": float(msg[0:3]),
|
||||
"dt": float(msg[4:8]),
|
||||
"freq": dial_freq + int(float(msg[10:20]) * 1e6),
|
||||
"drift": int(msg[20:23]),
|
||||
"msg": wsjt_msg,
|
||||
}
|
||||
result.update(self.parseMessage(wsjt_msg))
|
||||
|
Reference in New Issue
Block a user