2019-07-21 21:39:11 +00:00
|
|
|
import threading
|
2021-03-18 18:34:53 +00:00
|
|
|
from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
|
2019-12-21 19:58:28 +00:00
|
|
|
from owrx.sdr import SdrService
|
2019-07-21 21:39:11 +00:00
|
|
|
from owrx.bands import Bandplan
|
2021-02-11 18:31:44 +00:00
|
|
|
from owrx.config import Config
|
2019-12-21 19:58:28 +00:00
|
|
|
from owrx.source.resampler import Resampler
|
2021-02-26 20:27:42 +00:00
|
|
|
from owrx.property import PropertyLayer, PropertyDeleted
|
2021-03-20 00:56:07 +00:00
|
|
|
from owrx.service.schedule import ServiceScheduler
|
2021-08-31 19:53:15 +00:00
|
|
|
from owrx.service.chain import ServiceDemodulatorChain
|
|
|
|
from owrx.modes import Modes, DigitalMode
|
2021-09-23 16:43:41 +00:00
|
|
|
from typing import Union, Optional
|
|
|
|
from csdr.chain.demodulator import BaseDemodulatorChain, ServiceDemodulator, DialFrequencyReceiver
|
2021-09-06 13:05:33 +00:00
|
|
|
from pycsdr.modules import Buffer
|
2019-07-21 21:39:11 +00:00
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2020-08-30 21:47:04 +00:00
|
|
|
class ServiceHandler(SdrSourceEventClient):
|
2019-07-21 21:39:11 +00:00
|
|
|
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
|
2021-02-26 20:27:42 +00:00
|
|
|
self.activitySub = None
|
|
|
|
self.running = False
|
|
|
|
props = self.source.getProps()
|
|
|
|
self.enabledSub = props.wireProperty("services", self._receiveEvent)
|
2021-02-26 22:50:58 +00:00
|
|
|
self.decodersSub = None
|
2021-02-26 20:27:42 +00:00
|
|
|
# need to call _start() manually if property is not set since the default is True, but the initial call is only
|
|
|
|
# made if the property is present
|
|
|
|
if "services" not in props:
|
|
|
|
self._start()
|
|
|
|
|
|
|
|
def _receiveEvent(self, state):
|
|
|
|
# deletion means fall back to default, which is True
|
|
|
|
if state is PropertyDeleted:
|
|
|
|
state = True
|
|
|
|
if self.running == state:
|
|
|
|
return
|
|
|
|
if state:
|
|
|
|
self._start()
|
|
|
|
else:
|
|
|
|
self._stop()
|
|
|
|
|
|
|
|
def _start(self):
|
|
|
|
self.running = True
|
2019-07-21 21:39:11 +00:00
|
|
|
self.source.addClient(self)
|
2019-09-15 22:31:35 +00:00
|
|
|
props = self.source.getProps()
|
2021-02-26 20:27:42 +00:00
|
|
|
self.activitySub = props.filter("center_freq", "samp_rate").wire(self.onFrequencyChange)
|
2021-02-26 22:50:58 +00:00
|
|
|
self.decodersSub = Config.get().wireProperty("services_decoders", self.onFrequencyChange)
|
2019-09-15 22:31:35 +00:00
|
|
|
if self.source.isAvailable():
|
2021-02-26 22:50:58 +00:00
|
|
|
self._scheduleServiceStartup()
|
2021-02-26 20:27:42 +00:00
|
|
|
|
|
|
|
def _stop(self):
|
|
|
|
if self.activitySub is not None:
|
|
|
|
self.activitySub.cancel()
|
|
|
|
self.activitySub = None
|
2021-02-26 22:50:58 +00:00
|
|
|
if self.decodersSub is not None:
|
|
|
|
self.decodersSub.cancel()
|
|
|
|
self.decodersSub = None
|
2021-02-26 20:27:42 +00:00
|
|
|
self._cancelStartupTimer()
|
|
|
|
self.source.removeClient(self)
|
|
|
|
self.stopServices()
|
|
|
|
self.running = False
|
2019-09-15 22:31:35 +00:00
|
|
|
|
2021-02-20 21:54:07 +00:00
|
|
|
def getClientClass(self) -> SdrClientClass:
|
|
|
|
return SdrClientClass.INACTIVE
|
2019-07-21 21:39:11 +00:00
|
|
|
|
2021-02-20 21:54:07 +00:00
|
|
|
def onStateChange(self, state: SdrSourceState):
|
|
|
|
if state is SdrSourceState.RUNNING:
|
2021-02-26 22:50:58 +00:00
|
|
|
self._scheduleServiceStartup()
|
2021-02-20 21:54:07 +00:00
|
|
|
elif state is SdrSourceState.STOPPING:
|
2019-11-15 21:13:00 +00:00
|
|
|
logger.debug("sdr source becoming unavailable; stopping services.")
|
|
|
|
self.stopServices()
|
2019-10-12 18:19:34 +00:00
|
|
|
|
2021-03-18 18:34:53 +00:00
|
|
|
def onFail(self):
|
|
|
|
logger.debug("sdr source failed; stopping services.")
|
|
|
|
self.stopServices()
|
2019-11-15 22:05:52 +00:00
|
|
|
|
2021-03-18 21:59:46 +00:00
|
|
|
def onShutdown(self):
|
|
|
|
logger.debug("sdr source is shutting down; shutting down service handler, too.")
|
|
|
|
self.shutdown()
|
|
|
|
|
2021-03-18 18:59:10 +00:00
|
|
|
def onEnable(self):
|
|
|
|
self._scheduleServiceStartup()
|
|
|
|
|
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):
|
2021-02-26 20:27:42 +00:00
|
|
|
self._stop()
|
|
|
|
if self.enabledSub is not None:
|
|
|
|
self.enabledSub.cancel()
|
|
|
|
self.enabledSub = None
|
2019-10-31 21:24:31 +00:00
|
|
|
|
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
|
|
|
|
2020-12-30 16:18:46 +00:00
|
|
|
def onFrequencyChange(self, changes):
|
2019-08-03 21:44:56 +00:00
|
|
|
self.stopServices()
|
2019-07-21 21:39:11 +00:00
|
|
|
if not self.source.isAvailable():
|
|
|
|
return
|
2021-02-26 22:50:58 +00:00
|
|
|
self._scheduleServiceStartup()
|
2019-08-03 21:44:56 +00:00
|
|
|
|
2021-02-26 20:27:42 +00:00
|
|
|
def _cancelStartupTimer(self):
|
2019-08-03 21:44:56 +00:00
|
|
|
if self.startupTimer:
|
|
|
|
self.startupTimer.cancel()
|
2021-02-26 20:27:42 +00:00
|
|
|
self.startupTimer = None
|
|
|
|
|
2021-02-26 22:50:58 +00:00
|
|
|
def _scheduleServiceStartup(self):
|
2021-02-26 20:27:42 +00:00
|
|
|
self._cancelStartupTimer()
|
2019-08-03 21:44:56 +00:00
|
|
|
self.startupTimer = threading.Timer(10, self.updateServices)
|
|
|
|
self.startupTimer.start()
|
2019-07-28 10:11:22 +00:00
|
|
|
|
|
|
|
def updateServices(self):
|
2023-02-19 15:14:08 +00:00
|
|
|
def addService(dial, source):
|
|
|
|
mode = dial["mode"]
|
|
|
|
frequency = dial["frequency"]
|
|
|
|
try:
|
|
|
|
service = self.setupService(mode, frequency, source)
|
|
|
|
self.services.append(service)
|
|
|
|
except Exception:
|
|
|
|
logger.exception("Error setting up service %s on frequency %d", mode, frequency)
|
|
|
|
|
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
|
2021-01-20 16:01:46 +00:00
|
|
|
for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range)
|
2020-07-30 19:35:31 +00:00
|
|
|
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:
|
2023-02-19 15:14:08 +00:00
|
|
|
addService(dial, self.source)
|
2019-10-13 16:25:32 +00:00
|
|
|
else:
|
|
|
|
for group in groups:
|
2021-05-07 22:37:30 +00:00
|
|
|
if len(group) > 1:
|
|
|
|
cf = self.get_center_frequency(group)
|
|
|
|
bw = self.get_bandwidth(group)
|
2021-09-01 13:58:39 +00:00
|
|
|
logger.debug("setting up resampler on center frequency: {0}, bandwidth: {1}".format(cf, bw))
|
|
|
|
resampler_props = PropertyLayer(center_freq=cf, samp_rate=bw)
|
2021-05-07 22:37:30 +00:00
|
|
|
resampler = Resampler(resampler_props, self.source)
|
|
|
|
|
|
|
|
for dial in group:
|
2023-02-19 15:14:08 +00:00
|
|
|
addService(dial, resampler)
|
2021-05-07 22:37:30 +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)
|
|
|
|
else:
|
|
|
|
dial = group[0]
|
2023-02-19 15:14:08 +00:00
|
|
|
addService(dial, self.source)
|
2019-10-31 18:13:33 +00:00
|
|
|
|
2020-12-05 23:36:20 +00:00
|
|
|
def get_min_max(self, group):
|
|
|
|
frequencies = sorted(group, key=lambda f: f["frequency"])
|
|
|
|
lowest = frequencies[0]
|
|
|
|
min = lowest["frequency"] + Modes.findByModulation(lowest["mode"]).get_bandpass().low_cut
|
|
|
|
highest = frequencies[-1]
|
|
|
|
max = highest["frequency"] + Modes.findByModulation(highest["mode"]).get_bandpass().high_cut
|
|
|
|
return min, max
|
|
|
|
|
|
|
|
def get_center_frequency(self, group):
|
|
|
|
min, max = self.get_min_max(group)
|
|
|
|
return (min + max) / 2
|
|
|
|
|
|
|
|
def get_bandwidth(self, group):
|
|
|
|
minFreq, maxFreq = self.get_min_max(group)
|
|
|
|
# minimum bandwidth for a resampler: 25kHz
|
2021-05-07 22:38:00 +00:00
|
|
|
return max((maxFreq - minFreq) * 1.15, 25000)
|
2020-12-05 23:36:20 +00:00
|
|
|
|
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"]])
|
|
|
|
|
2020-12-05 23:36:20 +00:00
|
|
|
def get_total_bandwidth(group):
|
2021-05-07 22:59:57 +00:00
|
|
|
if len(group) > 1:
|
|
|
|
return bandwidth + len(group) * self.get_bandwidth(group)
|
|
|
|
else:
|
|
|
|
return bandwidth
|
2019-09-10 22:30:14 +00:00
|
|
|
|
2020-12-05 23:36:20 +00:00
|
|
|
total_bandwidth = sum([get_total_bandwidth(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:
|
2021-01-20 16:01:46 +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))
|
2021-08-31 19:53:15 +00:00
|
|
|
|
2020-12-05 23:36:20 +00:00
|
|
|
modeObject = Modes.findByModulation(mode)
|
2021-08-31 19:53:15 +00:00
|
|
|
if not isinstance(modeObject, DigitalMode):
|
|
|
|
logger.warning("mode is not a digimode: %s", mode)
|
|
|
|
return None
|
|
|
|
|
2021-09-06 20:50:57 +00:00
|
|
|
demod = self._getDemodulator(modeObject.get_modulation())
|
2021-08-31 19:53:15 +00:00
|
|
|
secondaryDemod = self._getSecondaryDemodulator(modeObject.modulation)
|
|
|
|
center_freq = source.getProps()["center_freq"]
|
|
|
|
sampleRate = source.getProps()["samp_rate"]
|
|
|
|
bandpass = modeObject.get_bandpass()
|
2021-08-31 20:46:11 +00:00
|
|
|
if isinstance(secondaryDemod, DialFrequencyReceiver):
|
|
|
|
secondaryDemod.setDialFrequency(frequency)
|
2021-08-31 19:53:15 +00:00
|
|
|
|
2021-09-23 16:43:41 +00:00
|
|
|
chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, frequency - center_freq)
|
2021-08-31 19:53:15 +00:00
|
|
|
chain.setBandPass(bandpass.low_cut, bandpass.high_cut)
|
|
|
|
chain.setReader(source.getBuffer().getReader())
|
2021-09-06 13:05:33 +00:00
|
|
|
|
|
|
|
# dummy buffer, we don't use the output right now
|
|
|
|
buffer = Buffer(chain.getOutputFormat())
|
|
|
|
chain.setWriter(buffer)
|
2021-08-31 19:53:15 +00:00
|
|
|
return chain
|
|
|
|
|
|
|
|
# TODO move this elsewhere
|
2021-09-06 20:50:57 +00:00
|
|
|
def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]):
|
2021-08-31 19:53:15 +00:00
|
|
|
if isinstance(demod, BaseDemodulatorChain):
|
|
|
|
return demod
|
|
|
|
# TODO: move this to Modes
|
|
|
|
if demod == "nfm":
|
2021-09-20 14:14:23 +00:00
|
|
|
from csdr.chain.analog import NFm
|
2021-09-07 12:45:52 +00:00
|
|
|
return NFm(48000)
|
2021-08-31 19:53:15 +00:00
|
|
|
elif demod in ["usb", "lsb", "cw"]:
|
2021-09-20 14:14:23 +00:00
|
|
|
from csdr.chain.analog import Ssb
|
2021-09-07 12:45:52 +00:00
|
|
|
return Ssb()
|
2021-08-31 19:53:15 +00:00
|
|
|
|
|
|
|
# TODO move this elsewhere
|
2021-09-23 16:43:41 +00:00
|
|
|
def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]:
|
|
|
|
if isinstance(mod, ServiceDemodulatorChain):
|
2021-08-31 19:53:15 +00:00
|
|
|
return mod
|
|
|
|
if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]:
|
2021-09-20 14:14:23 +00:00
|
|
|
from csdr.chain.digimodes import AudioChopperDemodulator
|
2021-09-20 14:55:17 +00:00
|
|
|
from owrx.wsjt import WsjtParser
|
2021-08-31 19:53:15 +00:00
|
|
|
return AudioChopperDemodulator(mod, WsjtParser())
|
2023-02-19 15:18:08 +00:00
|
|
|
elif mod == "msk144":
|
|
|
|
from csdr.chain.digimodes import Msk144Demodulator
|
|
|
|
return Msk144Demodulator()
|
2021-09-06 20:50:57 +00:00
|
|
|
elif mod == "js8":
|
2021-09-20 14:14:23 +00:00
|
|
|
from csdr.chain.digimodes import AudioChopperDemodulator
|
2021-09-20 14:55:17 +00:00
|
|
|
from owrx.js8 import Js8Parser
|
2021-09-06 20:50:57 +00:00
|
|
|
return AudioChopperDemodulator(mod, Js8Parser())
|
2021-09-06 13:05:33 +00:00
|
|
|
elif mod == "packet":
|
2021-09-20 14:14:23 +00:00
|
|
|
from csdr.chain.digimodes import PacketDemodulator
|
2021-09-06 13:05:33 +00:00
|
|
|
return PacketDemodulator(service=True)
|
2023-02-19 15:14:08 +00:00
|
|
|
|
|
|
|
raise ValueError("unsupported service modulation: {}".format(mod))
|
2021-08-31 19:53:15 +00:00
|
|
|
|
2019-07-21 21:39:11 +00:00
|
|
|
|
2019-09-15 19:10:30 +00:00
|
|
|
class Services(object):
|
2021-03-20 00:56:07 +00:00
|
|
|
handlers = {}
|
|
|
|
schedulers = {}
|
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():
|
2021-02-26 16:53:06 +00:00
|
|
|
config = Config.get()
|
2021-03-20 00:56:07 +00:00
|
|
|
config.wireProperty("services_enabled", Services._receiveEnabledEvent)
|
|
|
|
activeSources = SdrService.getActiveSources()
|
|
|
|
activeSources.wire(Services._receiveDeviceEvent)
|
|
|
|
for key, source in activeSources.items():
|
|
|
|
Services.schedulers[key] = ServiceScheduler(source)
|
2021-02-26 16:53:06 +00:00
|
|
|
|
|
|
|
@staticmethod
|
2021-03-20 00:56:07 +00:00
|
|
|
def _receiveEnabledEvent(state):
|
2021-02-26 16:53:06 +00:00
|
|
|
if state:
|
2021-03-20 00:56:07 +00:00
|
|
|
for key, source in SdrService.getActiveSources().__dict__().items():
|
|
|
|
Services.handlers[key] = ServiceHandler(source)
|
2021-02-26 16:53:06 +00:00
|
|
|
else:
|
2021-03-20 16:24:00 +00:00
|
|
|
for handler in list(Services.handlers.values()):
|
2021-03-20 00:56:07 +00:00
|
|
|
handler.shutdown()
|
|
|
|
Services.handlers = {}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _receiveDeviceEvent(changes):
|
|
|
|
for key, source in changes.items():
|
|
|
|
if source is PropertyDeleted:
|
|
|
|
if key in Services.handlers:
|
|
|
|
Services.handlers[key].shutdown()
|
|
|
|
del Services.handlers[key]
|
|
|
|
if key in Services.schedulers:
|
|
|
|
Services.schedulers[key].shutdown()
|
|
|
|
del Services.schedulers[key]
|
|
|
|
else:
|
|
|
|
Services.schedulers[key] = ServiceScheduler(source)
|
|
|
|
if Config.get()["services_enabled"]:
|
|
|
|
Services.handlers[key] = ServiceHandler(source)
|
2019-10-27 11:16:17 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def stop():
|
2021-03-20 16:24:00 +00:00
|
|
|
for handler in list(Services.handlers.values()):
|
2021-03-20 00:56:07 +00:00
|
|
|
handler.shutdown()
|
|
|
|
Services.handlers = {}
|
2021-03-20 16:24:00 +00:00
|
|
|
for scheduler in list(Services.schedulers.values()):
|
2021-03-20 00:56:07 +00:00
|
|
|
scheduler.shutdown()
|
|
|
|
Services.schedulers = {}
|