2021-04-11 16:46:21 +00:00
|
|
|
from owrx.audio import AudioChopperProfile, ConfigWiredProfileSource
|
2021-09-27 22:27:01 +00:00
|
|
|
from owrx.audio.chopper import AudioChopperParser
|
2020-04-12 11:10:23 +00:00
|
|
|
import re
|
2020-04-13 14:35:31 +00:00
|
|
|
from js8py import Js8
|
2020-04-17 21:50:23 +00:00
|
|
|
from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound
|
2021-04-11 16:46:21 +00:00
|
|
|
from owrx.map import Map, LocatorLocation
|
|
|
|
from owrx.metrics import Metrics, CounterMetric
|
|
|
|
from owrx.config import Config
|
2020-04-23 20:19:07 +00:00
|
|
|
from abc import ABCMeta, abstractmethod
|
2021-01-14 19:52:56 +00:00
|
|
|
from owrx.reporting import ReportingEngine
|
2021-09-06 20:50:57 +00:00
|
|
|
from owrx.bands import Bandplan
|
2021-04-11 16:46:21 +00:00
|
|
|
from typing import List
|
2020-04-12 11:10:23 +00:00
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2020-04-22 22:34:49 +00:00
|
|
|
class Js8Profile(AudioChopperProfile, metaclass=ABCMeta):
|
2020-12-09 10:38:46 +00:00
|
|
|
def decoding_depth(self):
|
2020-04-25 14:22:40 +00:00
|
|
|
pm = Config.get()
|
|
|
|
# return global default
|
|
|
|
if "js8_decoding_depth" in pm:
|
|
|
|
return pm["js8_decoding_depth"]
|
|
|
|
# default when no setting is provided
|
|
|
|
return 3
|
|
|
|
|
2020-04-12 11:10:23 +00:00
|
|
|
def getFileTimestampFormat(self):
|
|
|
|
return "%y%m%d_%H%M%S"
|
|
|
|
|
|
|
|
def decoder_commandline(self, file):
|
2020-12-09 10:38:46 +00:00
|
|
|
return ["js8", "--js8", "-b", self.get_sub_mode(), "-d", str(self.decoding_depth()), file]
|
2020-04-23 20:19:07 +00:00
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def get_sub_mode(self):
|
|
|
|
pass
|
2020-04-12 11:10:23 +00:00
|
|
|
|
|
|
|
|
2021-04-11 16:46:21 +00:00
|
|
|
class Js8ProfileSource(ConfigWiredProfileSource):
|
|
|
|
def getPropertiesToWire(self) -> List[str]:
|
|
|
|
return ["js8_enabled_profiles"]
|
|
|
|
|
|
|
|
def getProfiles(self) -> List[AudioChopperProfile]:
|
|
|
|
config = Config.get()
|
|
|
|
profiles = config["js8_enabled_profiles"] if "js8_enabled_profiles" in config else []
|
|
|
|
return [self._loadProfile(p) for p in profiles]
|
|
|
|
|
|
|
|
def _loadProfile(self, profileName):
|
|
|
|
className = "Js8{0}Profile".format(profileName[0].upper() + profileName[1:].lower())
|
|
|
|
return globals()[className]()
|
|
|
|
|
|
|
|
|
2020-04-22 22:34:49 +00:00
|
|
|
class Js8NormalProfile(Js8Profile):
|
|
|
|
def getInterval(self):
|
|
|
|
return 15
|
|
|
|
|
2020-04-23 20:19:07 +00:00
|
|
|
def get_sub_mode(self):
|
|
|
|
return "A"
|
|
|
|
|
2020-04-22 22:34:49 +00:00
|
|
|
|
|
|
|
class Js8SlowProfile(Js8Profile):
|
|
|
|
def getInterval(self):
|
|
|
|
return 30
|
|
|
|
|
2020-04-23 20:19:07 +00:00
|
|
|
def get_sub_mode(self):
|
|
|
|
return "E"
|
|
|
|
|
2020-04-22 22:34:49 +00:00
|
|
|
|
2020-04-23 20:27:03 +00:00
|
|
|
class Js8FastProfile(Js8Profile):
|
|
|
|
def getInterval(self):
|
|
|
|
return 10
|
|
|
|
|
|
|
|
def get_sub_mode(self):
|
|
|
|
return "B"
|
|
|
|
|
|
|
|
|
|
|
|
class Js8TurboProfile(Js8Profile):
|
|
|
|
def getInterval(self):
|
|
|
|
return 6
|
|
|
|
|
|
|
|
def get_sub_mode(self):
|
|
|
|
return "C"
|
|
|
|
|
|
|
|
|
2021-09-27 22:27:01 +00:00
|
|
|
class Js8Parser(AudioChopperParser):
|
2020-04-12 11:10:23 +00:00
|
|
|
decoderRegex = re.compile(" ?<Decode(Started|Debug|Finished)>")
|
|
|
|
|
2021-09-27 22:27:01 +00:00
|
|
|
def parse(self, profile: AudioChopperProfile, freq: int, raw_msg: bytes):
|
2021-04-11 19:04:13 +00:00
|
|
|
try:
|
2021-09-06 20:50:57 +00:00
|
|
|
band = None
|
|
|
|
if freq is not None:
|
|
|
|
band = Bandplan.getSharedInstance().findBand(freq)
|
|
|
|
|
2021-04-11 19:04:13 +00:00
|
|
|
msg = raw_msg.decode().rstrip()
|
|
|
|
if Js8Parser.decoderRegex.match(msg):
|
|
|
|
return
|
|
|
|
if msg.startswith(" EOF on input file"):
|
|
|
|
return
|
|
|
|
|
|
|
|
frame = Js8().parse_message(msg)
|
|
|
|
|
2021-09-06 20:50:57 +00:00
|
|
|
self.pushDecode(band)
|
2021-04-11 19:04:13 +00:00
|
|
|
|
|
|
|
if (isinstance(frame, Js8FrameHeartbeat) or isinstance(frame, Js8FrameCompound)) and frame.grid:
|
|
|
|
Map.getSharedInstance().updateLocation(
|
2022-11-30 00:07:16 +00:00
|
|
|
frame.source, LocatorLocation(frame.grid), "JS8", band
|
2021-04-11 19:04:13 +00:00
|
|
|
)
|
|
|
|
ReportingEngine.getSharedInstance().spot(
|
|
|
|
{
|
2022-11-30 00:07:16 +00:00
|
|
|
"source": frame.source,
|
2021-04-11 19:04:13 +00:00
|
|
|
"mode": "JS8",
|
|
|
|
"locator": frame.grid,
|
2021-09-06 20:50:57 +00:00
|
|
|
"freq": freq + frame.freq,
|
2021-04-11 19:04:13 +00:00
|
|
|
"db": frame.db,
|
|
|
|
"timestamp": frame.timestamp,
|
|
|
|
"msg": str(frame),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2021-09-06 20:50:57 +00:00
|
|
|
out = {
|
|
|
|
"mode": "JS8",
|
|
|
|
"msg": str(frame),
|
|
|
|
"timestamp": frame.timestamp,
|
|
|
|
"db": frame.db,
|
|
|
|
"dt": frame.dt,
|
|
|
|
"freq": freq + frame.freq,
|
|
|
|
"thread_type": frame.thread_type,
|
|
|
|
"js8mode": frame.mode,
|
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
2021-04-11 19:04:13 +00:00
|
|
|
except Exception:
|
|
|
|
logger.exception("error while parsing js8 message")
|
2020-04-14 20:31:30 +00:00
|
|
|
|
2021-09-06 20:50:57 +00:00
|
|
|
def pushDecode(self, band):
|
2020-04-14 20:31:30 +00:00
|
|
|
metrics = Metrics.getSharedInstance()
|
2021-09-06 20:50:57 +00:00
|
|
|
bandName = "unknown"
|
|
|
|
if band is not None:
|
|
|
|
bandName = band.getName()
|
2020-04-14 20:31:30 +00:00
|
|
|
|
2021-09-06 20:50:57 +00:00
|
|
|
name = "js8call.decodes.{band}.JS8".format(band=bandName)
|
2020-04-14 20:31:30 +00:00
|
|
|
metric = metrics.getMetric(name)
|
|
|
|
if metric is None:
|
|
|
|
metric = CounterMetric()
|
|
|
|
metrics.addMetric(name, metric)
|
|
|
|
|
|
|
|
metric.inc()
|