2019-07-21 21:39:11 +00:00
|
|
|
import threading
|
2019-12-21 19:58:28 +00:00
|
|
|
from owrx.source import SdrSource
|
|
|
|
from owrx.sdr import SdrService
|
2019-07-21 21:39:11 +00:00
|
|
|
from owrx.bands import Bandplan
|
2019-12-08 16:15:48 +00:00
|
|
|
from csdr.csdr import dsp, output
|
2019-07-21 21:39:11 +00:00
|
|
|
from owrx.wsjt import WsjtParser
|
2019-08-15 13:45:15 +00:00
|
|
|
from owrx.aprs import AprsParser
|
2020-04-14 19:27:50 +00:00
|
|
|
from owrx.js8 import Js8Parser
|
2020-03-21 21:40:39 +00:00
|
|
|
from owrx.config import Config
|
2019-12-21 19:58:28 +00:00
|
|
|
from owrx.source.resampler import Resampler
|
2020-03-23 22:56:05 +00:00
|
|
|
from owrx.property import PropertyLayer
|
2020-04-14 19:27:50 +00:00
|
|
|
from js8py import Js8Frame
|
2020-01-17 21:46:01 +00:00
|
|
|
from abc import ABCMeta, abstractmethod
|
|
|
|
from .schedule import ServiceScheduler
|
2020-04-26 13:17:03 +00:00
|
|
|
from owrx.modes import Modes
|
2019-07-21 21:39:11 +00:00
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2020-01-17 21:46:01 +00:00
|
|
|
class ServiceOutput(output, metaclass=ABCMeta):
|
2019-07-21 21:39:11 +00:00
|
|
|
def __init__(self, frequency):
|
|
|
|
self.frequency = frequency
|
|
|
|
|
2020-01-17 21:46:01 +00:00
|
|
|
@abstractmethod
|
2019-08-15 13:45:15 +00:00
|
|
|
def getParser(self):
|
|
|
|
# abstract method; implement in subclasses
|
|
|
|
pass
|
|
|
|
|
2019-08-04 12:55:56 +00:00
|
|
|
def receive_output(self, t, read_fn):
|
2019-08-15 13:45:15 +00:00
|
|
|
parser = self.getParser()
|
2019-08-04 12:55:56 +00:00
|
|
|
parser.setDialFrequency(self.frequency)
|
|
|
|
target = self.pump(read_fn, parser.parse)
|
2020-08-14 18:22:25 +00:00
|
|
|
threading.Thread(target=target, name="service_output_receive").start()
|
2019-07-21 21:39:11 +00:00
|
|
|
|
2019-08-15 13:45:15 +00:00
|
|
|
|
|
|
|
class WsjtServiceOutput(ServiceOutput):
|
|
|
|
def getParser(self):
|
|
|
|
return WsjtParser(WsjtHandler())
|
|
|
|
|
2019-08-04 12:55:56 +00:00
|
|
|
def supports_type(self, t):
|
2019-08-11 09:37:45 +00:00
|
|
|
return t == "wsjt_demod"
|
2019-08-04 12:55:56 +00:00
|
|
|
|
2019-07-21 21:39:11 +00:00
|
|
|
|
2019-08-15 13:45:15 +00:00
|
|
|
class AprsServiceOutput(ServiceOutput):
|
|
|
|
def getParser(self):
|
|
|
|
return AprsParser(AprsHandler())
|
|
|
|
|
|
|
|
def supports_type(self, t):
|
|
|
|
return t == "packet_demod"
|
|
|
|
|
|
|
|
|
2020-04-14 19:27:50 +00:00
|
|
|
class Js8ServiceOutput(ServiceOutput):
|
|
|
|
def getParser(self):
|
|
|
|
return Js8Parser(Js8Handler())
|
|
|
|
|
|
|
|
def supports_type(self, t):
|
|
|
|
return t == "js8_demod"
|
|
|
|
|
|
|
|
|
2019-07-21 21:39:11 +00:00
|
|
|
class ServiceHandler(object):
|
|
|
|
def __init__(self, source):
|
2020-07-30 19:35:31 +00:00
|
|
|
self.lock = threading.RLock()
|
2019-07-21 21:39:11 +00:00
|
|
|
self.services = []
|
|
|
|
self.source = source
|
2019-08-03 21:44:56 +00:00
|
|
|
self.startupTimer = None
|
2019-07-21 21:39:11 +00:00
|
|
|
self.source.addClient(self)
|
2019-09-15 22:31:35 +00:00
|
|
|
props = self.source.getProps()
|
2020-03-24 21:16:11 +00:00
|
|
|
props.filter("center_freq", "samp_rate").wire(self.onFrequencyChange)
|
2019-09-15 22:31:35 +00:00
|
|
|
if self.source.isAvailable():
|
|
|
|
self.scheduleServiceStartup()
|
2019-10-31 21:24:31 +00:00
|
|
|
self.scheduler = None
|
2020-01-17 21:46:01 +00:00
|
|
|
if "schedule" in props or "scheduler" in props:
|
|
|
|
self.scheduler = ServiceScheduler(self.source)
|
2019-09-15 22:31:35 +00:00
|
|
|
|
2019-11-15 22:05:52 +00:00
|
|
|
def getClientClass(self):
|
2019-11-16 14:40:12 +00:00
|
|
|
return SdrSource.CLIENT_INACTIVE
|
2019-07-21 21:39:11 +00:00
|
|
|
|
2019-11-15 21:13:00 +00:00
|
|
|
def onStateChange(self, state):
|
|
|
|
if state == SdrSource.STATE_RUNNING:
|
|
|
|
self.scheduleServiceStartup()
|
|
|
|
elif state == SdrSource.STATE_STOPPING:
|
|
|
|
logger.debug("sdr source becoming unavailable; stopping services.")
|
|
|
|
self.stopServices()
|
|
|
|
elif state == SdrSource.STATE_FAILED:
|
|
|
|
logger.debug("sdr source failed; stopping services.")
|
|
|
|
self.stopServices()
|
2019-12-31 17:44:47 +00:00
|
|
|
if self.scheduler:
|
|
|
|
self.scheduler.shutdown()
|
2019-10-12 18:19:34 +00:00
|
|
|
|
2019-11-15 22:05:52 +00:00
|
|
|
def onBusyStateChange(self, state):
|
|
|
|
pass
|
|
|
|
|
2019-07-21 21:39:11 +00:00
|
|
|
def isSupported(self, mode):
|
2020-03-21 21:40:39 +00:00
|
|
|
configured = Config.get()["services_decoders"]
|
2020-04-26 13:17:03 +00:00
|
|
|
available = [m.modulation for m in Modes.getAvailableServices()]
|
2020-03-29 17:50:37 +00:00
|
|
|
return mode in configured and mode in available
|
2019-07-21 21:39:11 +00:00
|
|
|
|
2019-10-31 21:24:31 +00:00
|
|
|
def shutdown(self):
|
|
|
|
self.stopServices()
|
|
|
|
self.source.removeClient(self)
|
|
|
|
if self.scheduler:
|
|
|
|
self.scheduler.shutdown()
|
|
|
|
|
2019-07-21 21:39:11 +00:00
|
|
|
def stopServices(self):
|
2019-09-15 22:31:35 +00:00
|
|
|
with self.lock:
|
|
|
|
services = self.services
|
|
|
|
self.services = []
|
2019-07-21 21:39:11 +00:00
|
|
|
|
2019-09-15 22:31:35 +00:00
|
|
|
for service in services:
|
|
|
|
service.stop()
|
2019-07-21 21:39:11 +00:00
|
|
|
|
|
|
|
def onFrequencyChange(self, key, value):
|
2019-08-03 21:44:56 +00:00
|
|
|
self.stopServices()
|
2019-07-21 21:39:11 +00:00
|
|
|
if not self.source.isAvailable():
|
|
|
|
return
|
2019-08-03 21:44:56 +00:00
|
|
|
self.scheduleServiceStartup()
|
|
|
|
|
|
|
|
def scheduleServiceStartup(self):
|
|
|
|
if self.startupTimer:
|
|
|
|
self.startupTimer.cancel()
|
|
|
|
self.startupTimer = threading.Timer(10, self.updateServices)
|
|
|
|
self.startupTimer.start()
|
2019-07-28 10:11:22 +00:00
|
|
|
|
|
|
|
def updateServices(self):
|
2019-09-15 22:31:35 +00:00
|
|
|
with self.lock:
|
2020-07-30 19:35:31 +00:00
|
|
|
logger.debug("re-scheduling services due to sdr changes")
|
|
|
|
self.stopServices()
|
|
|
|
if not self.source.isAvailable():
|
|
|
|
logger.debug("sdr source is unavailable")
|
|
|
|
return
|
|
|
|
cf = self.source.getProps()["center_freq"]
|
|
|
|
sr = self.source.getProps()["samp_rate"]
|
|
|
|
srh = sr / 2
|
|
|
|
frequency_range = (cf - srh, cf + srh)
|
|
|
|
|
|
|
|
dials = [
|
|
|
|
dial
|
|
|
|
for dial in Bandplan.getSharedInstance().collectDialFrequencies(
|
|
|
|
frequency_range
|
|
|
|
)
|
|
|
|
if self.isSupported(dial["mode"])
|
|
|
|
]
|
|
|
|
|
|
|
|
if not dials:
|
|
|
|
logger.debug("no services available")
|
|
|
|
return
|
2019-09-15 22:31:35 +00:00
|
|
|
|
2019-10-13 16:25:32 +00:00
|
|
|
groups = self.optimizeResampling(dials, sr)
|
|
|
|
if groups is None:
|
|
|
|
for dial in dials:
|
2020-03-29 17:50:37 +00:00
|
|
|
self.services.append(
|
|
|
|
self.setupService(dial["mode"], dial["frequency"], self.source)
|
|
|
|
)
|
2019-10-13 16:25:32 +00:00
|
|
|
else:
|
|
|
|
for group in groups:
|
|
|
|
frequencies = sorted([f["frequency"] for f in group])
|
|
|
|
min = frequencies[0]
|
|
|
|
max = frequencies[-1]
|
|
|
|
cf = (min + max) / 2
|
|
|
|
bw = max - min
|
2020-03-29 17:50:37 +00:00
|
|
|
logger.debug(
|
|
|
|
"group center frequency: {0}, bandwidth: {1}".format(cf, bw)
|
|
|
|
)
|
2020-03-23 22:56:05 +00:00
|
|
|
resampler_props = PropertyLayer()
|
2019-10-13 16:25:32 +00:00
|
|
|
resampler_props["center_freq"] = cf
|
|
|
|
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
|
|
|
|
resampler_props["samp_rate"] = bw + 24000
|
2019-12-31 14:27:33 +00:00
|
|
|
resampler = Resampler(resampler_props, self.source)
|
2019-10-13 16:25:32 +00:00
|
|
|
resampler.start()
|
|
|
|
|
|
|
|
for dial in group:
|
2020-03-29 17:50:37 +00:00
|
|
|
self.services.append(
|
|
|
|
self.setupService(
|
|
|
|
dial["mode"], dial["frequency"], resampler
|
|
|
|
)
|
|
|
|
)
|
2019-09-10 22:30:14 +00:00
|
|
|
|
2019-10-31 18:13:33 +00:00
|
|
|
# resampler goes in after the services since it must not be shutdown as long as the services are still running
|
|
|
|
self.services.append(resampler)
|
|
|
|
|
2019-09-10 22:30:14 +00:00
|
|
|
def optimizeResampling(self, freqs, bandwidth):
|
|
|
|
freqs = sorted(freqs, key=lambda f: f["frequency"])
|
2019-09-13 21:03:05 +00:00
|
|
|
distances = [
|
2020-03-29 17:50:37 +00:00
|
|
|
{
|
|
|
|
"frequency": freqs[i]["frequency"],
|
|
|
|
"distance": freqs[i + 1]["frequency"] - freqs[i]["frequency"],
|
|
|
|
}
|
2019-09-13 21:03:05 +00:00
|
|
|
for i in range(0, len(freqs) - 1)
|
|
|
|
]
|
2019-09-10 22:30:14 +00:00
|
|
|
|
|
|
|
distances = [d for d in distances if d["distance"] > 0]
|
|
|
|
|
|
|
|
distances = sorted(distances, key=lambda f: f["distance"], reverse=True)
|
|
|
|
|
|
|
|
def calculate_usage(num_splits):
|
|
|
|
splits = sorted([f["frequency"] for f in distances[0:num_splits]])
|
|
|
|
previous = 0
|
|
|
|
groups = []
|
|
|
|
for split in splits:
|
|
|
|
groups.append([f for f in freqs if previous < f["frequency"] <= split])
|
|
|
|
previous = split
|
|
|
|
groups.append([f for f in freqs if previous < f["frequency"]])
|
|
|
|
|
|
|
|
def get_bandwitdh(group):
|
|
|
|
freqs = sorted([f["frequency"] for f in group])
|
|
|
|
# the group will process the full BW once, plus the reduced BW once for each group member
|
|
|
|
return bandwidth + len(group) * (freqs[-1] - freqs[0] + 24000)
|
|
|
|
|
|
|
|
total_bandwidth = sum([get_bandwitdh(group) for group in groups])
|
2020-03-29 17:50:37 +00:00
|
|
|
return {
|
|
|
|
"num_splits": num_splits,
|
|
|
|
"total_bandwidth": total_bandwidth,
|
|
|
|
"groups": groups,
|
|
|
|
}
|
2019-09-10 22:30:14 +00:00
|
|
|
|
|
|
|
usages = [calculate_usage(i) for i in range(0, len(freqs))]
|
2019-10-13 16:28:58 +00:00
|
|
|
# another possible outcome might be that it's best not to resample at all. this is a special case.
|
2020-03-29 17:50:37 +00:00
|
|
|
usages += [
|
|
|
|
{
|
|
|
|
"num_splits": None,
|
|
|
|
"total_bandwidth": bandwidth * len(freqs),
|
|
|
|
"groups": [freqs],
|
|
|
|
}
|
|
|
|
]
|
2019-09-10 22:30:14 +00:00
|
|
|
results = sorted(usages, key=lambda f: f["total_bandwidth"])
|
|
|
|
|
|
|
|
for r in results:
|
2020-03-29 17:50:37 +00:00
|
|
|
logger.debug(
|
|
|
|
"splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"])
|
|
|
|
)
|
2019-09-10 22:30:14 +00:00
|
|
|
|
2019-10-13 16:25:32 +00:00
|
|
|
best = results[0]
|
|
|
|
if best["num_splits"] is None:
|
|
|
|
return None
|
|
|
|
return best["groups"]
|
2019-09-10 22:30:14 +00:00
|
|
|
|
|
|
|
def setupService(self, mode, frequency, source):
|
2019-07-21 21:39:11 +00:00
|
|
|
logger.debug("setting up service {0} on frequency {1}".format(mode, frequency))
|
2019-08-15 13:45:15 +00:00
|
|
|
# TODO selecting outputs will need some more intelligence here
|
|
|
|
if mode == "packet":
|
|
|
|
output = AprsServiceOutput(frequency)
|
2020-04-14 19:27:50 +00:00
|
|
|
elif mode == "js8":
|
|
|
|
output = Js8ServiceOutput(frequency)
|
2019-08-15 13:45:15 +00:00
|
|
|
else:
|
|
|
|
output = WsjtServiceOutput(frequency)
|
|
|
|
d = dsp(output)
|
2019-09-10 22:30:14 +00:00
|
|
|
d.nc_port = source.getPort()
|
2020-04-05 14:35:46 +00:00
|
|
|
center_freq = source.getProps()["center_freq"]
|
|
|
|
d.set_offset_freq(frequency - center_freq)
|
|
|
|
d.set_center_freq(center_freq)
|
2019-08-11 22:02:39 +00:00
|
|
|
if mode == "packet":
|
|
|
|
d.set_demodulator("nfm")
|
|
|
|
d.set_bpf(-4000, 4000)
|
2019-09-15 10:23:11 +00:00
|
|
|
elif mode == "wspr":
|
|
|
|
d.set_demodulator("usb")
|
|
|
|
# WSPR only samples between 1400 and 1600 Hz
|
|
|
|
d.set_bpf(1350, 1650)
|
2019-08-11 22:02:39 +00:00
|
|
|
else:
|
|
|
|
d.set_demodulator("usb")
|
|
|
|
d.set_bpf(0, 3000)
|
2019-07-21 21:39:11 +00:00
|
|
|
d.set_secondary_demodulator(mode)
|
|
|
|
d.set_audio_compression("none")
|
2019-09-10 22:30:14 +00:00
|
|
|
d.set_samp_rate(source.getProps()["samp_rate"])
|
2020-05-11 21:45:44 +00:00
|
|
|
d.set_temporary_directory(Config.get()['temporary_directory'])
|
2019-09-13 20:28:17 +00:00
|
|
|
d.set_service()
|
2019-07-21 21:39:11 +00:00
|
|
|
d.start()
|
|
|
|
return d
|
|
|
|
|
|
|
|
|
|
|
|
class WsjtHandler(object):
|
|
|
|
def write_wsjt_message(self, msg):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2019-08-15 13:45:15 +00:00
|
|
|
class AprsHandler(object):
|
|
|
|
def write_aprs_data(self, data):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2020-04-14 19:27:50 +00:00
|
|
|
class Js8Handler(object):
|
|
|
|
def write_js8_message(self, frame: Js8Frame, freq: int):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2019-09-15 19:10:30 +00:00
|
|
|
class Services(object):
|
2019-10-27 11:16:17 +00:00
|
|
|
handlers = []
|
2019-11-23 00:12:21 +00:00
|
|
|
|
2019-07-21 21:39:11 +00:00
|
|
|
@staticmethod
|
2019-09-15 19:10:30 +00:00
|
|
|
def start():
|
2020-03-21 21:40:39 +00:00
|
|
|
if not Config.get()["services_enabled"]:
|
2019-07-28 11:29:45 +00:00
|
|
|
return
|
2019-07-21 21:39:11 +00:00
|
|
|
for source in SdrService.getSources().values():
|
2019-12-31 17:44:47 +00:00
|
|
|
props = source.getProps()
|
2020-01-17 21:46:01 +00:00
|
|
|
if "services" not in props or props["services"] is not False:
|
2019-12-31 17:44:47 +00:00
|
|
|
Services.handlers.append(ServiceHandler(source))
|
2019-10-27 11:16:17 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def stop():
|
|
|
|
for handler in Services.handlers:
|
2019-10-31 21:24:31 +00:00
|
|
|
handler.shutdown()
|
2019-10-27 11:16:17 +00:00
|
|
|
Services.handlers = []
|