add a new multi-checkbox to select background detection services
This commit is contained in:
parent
056a8a3289
commit
199dfe106a
@ -1,6 +1,7 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from .admin import AdminController
|
from .admin import AdminController
|
||||||
from owrx.config import Config
|
from owrx.config import Config
|
||||||
|
from owrx.service import ServiceDetector
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -109,6 +110,48 @@ class CheckboxInput(Input):
|
|||||||
return {self.id: self.id in data and data[self.id][0] == "on"}
|
return {self.id: self.id in data and data[self.id][0] == "on"}
|
||||||
|
|
||||||
|
|
||||||
|
class MultiCheckboxInput(Input):
|
||||||
|
def __init__(self, id, label, options, infotext=None):
|
||||||
|
super().__init__(id, label, infotext=infotext)
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
def render_input(self, value):
|
||||||
|
return "".join(self.render_checkbox(o, value) for o in self.options)
|
||||||
|
|
||||||
|
def checkbox_id(self, option):
|
||||||
|
return "{0}-{1}".format(self.id, option.value)
|
||||||
|
|
||||||
|
def render_checkbox(self, option, value):
|
||||||
|
return """
|
||||||
|
<div class="{classes}">
|
||||||
|
<input class="form-check-input" type="checkbox" id="{id}" name="{id}" {checked}>
|
||||||
|
<label class="form-check-label" for="{id}">
|
||||||
|
{checkboxText}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
""".format(
|
||||||
|
id=self.checkbox_id(option),
|
||||||
|
classes=self.input_classes(),
|
||||||
|
checked="checked" if option.value in value else "",
|
||||||
|
checkboxText=option.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse(self, data):
|
||||||
|
def in_response(option):
|
||||||
|
boxid = self.checkbox_id(option)
|
||||||
|
return boxid in data and data[boxid][0] == "on"
|
||||||
|
return {self.id: [o.value for o in self.options if in_response(o)]}
|
||||||
|
|
||||||
|
def input_classes(self):
|
||||||
|
return " ".join(["form-check", "form-control-sm"])
|
||||||
|
|
||||||
|
|
||||||
|
class ServicesCheckboxInput(MultiCheckboxInput):
|
||||||
|
def __init__(self, id, label, infotext=None):
|
||||||
|
services = [DropdownOption(s, s.upper()) for s in ServiceDetector.getAvailableServices()]
|
||||||
|
super().__init__(id, label, services, infotext)
|
||||||
|
|
||||||
|
|
||||||
class DropdownOption(object):
|
class DropdownOption(object):
|
||||||
def __init__(self, value, text):
|
def __init__(self, value, text):
|
||||||
self.value = value
|
self.value = value
|
||||||
@ -270,6 +313,7 @@ class SettingsController(AdminController):
|
|||||||
Section(
|
Section(
|
||||||
"Background decoding",
|
"Background decoding",
|
||||||
CheckboxInput("services_enabled", "Service", checkboxText="Enable background decoding services"),
|
CheckboxInput("services_enabled", "Service", checkboxText="Enable background decoding services"),
|
||||||
|
ServicesCheckboxInput("services_decoders", "Enabled services")
|
||||||
),
|
),
|
||||||
Section(
|
Section(
|
||||||
"APRS settings",
|
"APRS settings",
|
||||||
|
@ -11,6 +11,7 @@ from owrx.feature import FeatureDetector
|
|||||||
from owrx.property import PropertyLayer
|
from owrx.property import PropertyLayer
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from .schedule import ServiceScheduler
|
from .schedule import ServiceScheduler
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -49,6 +50,30 @@ class AprsServiceOutput(ServiceOutput):
|
|||||||
return t == "packet_demod"
|
return t == "packet_demod"
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceDetector(object):
|
||||||
|
requirements = {
|
||||||
|
"ft8": ["wsjt-x"],
|
||||||
|
"ft4": ["wsjt-x"],
|
||||||
|
"jt65": ["wsjt-x"],
|
||||||
|
"jt9": ["wsjt-x"],
|
||||||
|
"wspr": ["wsjt-x"],
|
||||||
|
"packet": ["packet"],
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getAvailableServices():
|
||||||
|
# TODO this should be in a more central place (the frontend also needs this)
|
||||||
|
fd = FeatureDetector()
|
||||||
|
|
||||||
|
return [
|
||||||
|
name
|
||||||
|
for name, requirements in ServiceDetector.requirements.items()
|
||||||
|
if reduce(
|
||||||
|
lambda a, b: a and b, [fd.is_available(r) for r in requirements], True
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ServiceHandler(object):
|
class ServiceHandler(object):
|
||||||
def __init__(self, source):
|
def __init__(self, source):
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
@ -83,24 +108,9 @@ class ServiceHandler(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def isSupported(self, mode):
|
def isSupported(self, mode):
|
||||||
# TODO this should be in a more central place (the frontend also needs this)
|
|
||||||
requirements = {
|
|
||||||
"ft8": "wsjt-x",
|
|
||||||
"ft4": "wsjt-x",
|
|
||||||
"jt65": "wsjt-x",
|
|
||||||
"jt9": "wsjt-x",
|
|
||||||
"wspr": "wsjt-x",
|
|
||||||
"packet": "packet",
|
|
||||||
}
|
|
||||||
fd = FeatureDetector()
|
|
||||||
|
|
||||||
# this looks overly complicated... but i'd like modes with no requirements to be always available without
|
|
||||||
# being listed in the hash above
|
|
||||||
unavailable = [mode for mode, req in requirements.items() if not fd.is_available(req)]
|
|
||||||
configured = Config.get()["services_decoders"]
|
configured = Config.get()["services_decoders"]
|
||||||
available = [mode for mode in configured if mode not in unavailable]
|
available = ServiceDetector.getAvailableServices()
|
||||||
|
return mode in configured and mode in available
|
||||||
return mode in available
|
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.stopServices()
|
self.stopServices()
|
||||||
@ -141,7 +151,9 @@ class ServiceHandler(object):
|
|||||||
|
|
||||||
dials = [
|
dials = [
|
||||||
dial
|
dial
|
||||||
for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range)
|
for dial in Bandplan.getSharedInstance().collectDialFrequencies(
|
||||||
|
frequency_range
|
||||||
|
)
|
||||||
if self.isSupported(dial["mode"])
|
if self.isSupported(dial["mode"])
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -155,7 +167,9 @@ class ServiceHandler(object):
|
|||||||
groups = self.optimizeResampling(dials, sr)
|
groups = self.optimizeResampling(dials, sr)
|
||||||
if groups is None:
|
if groups is None:
|
||||||
for dial in dials:
|
for dial in dials:
|
||||||
self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source))
|
self.services.append(
|
||||||
|
self.setupService(dial["mode"], dial["frequency"], self.source)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
for group in groups:
|
for group in groups:
|
||||||
frequencies = sorted([f["frequency"] for f in group])
|
frequencies = sorted([f["frequency"] for f in group])
|
||||||
@ -163,7 +177,9 @@ class ServiceHandler(object):
|
|||||||
max = frequencies[-1]
|
max = frequencies[-1]
|
||||||
cf = (min + max) / 2
|
cf = (min + max) / 2
|
||||||
bw = max - min
|
bw = max - min
|
||||||
logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw))
|
logger.debug(
|
||||||
|
"group center frequency: {0}, bandwidth: {1}".format(cf, bw)
|
||||||
|
)
|
||||||
resampler_props = PropertyLayer()
|
resampler_props = PropertyLayer()
|
||||||
resampler_props["center_freq"] = cf
|
resampler_props["center_freq"] = cf
|
||||||
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
|
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
|
||||||
@ -172,7 +188,11 @@ class ServiceHandler(object):
|
|||||||
resampler.start()
|
resampler.start()
|
||||||
|
|
||||||
for dial in group:
|
for dial in group:
|
||||||
self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler))
|
self.services.append(
|
||||||
|
self.setupService(
|
||||||
|
dial["mode"], dial["frequency"], resampler
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# resampler goes in after the services since it must not be shutdown as long as the services are still running
|
# resampler goes in after the services since it must not be shutdown as long as the services are still running
|
||||||
self.services.append(resampler)
|
self.services.append(resampler)
|
||||||
@ -180,7 +200,10 @@ class ServiceHandler(object):
|
|||||||
def optimizeResampling(self, freqs, bandwidth):
|
def optimizeResampling(self, freqs, bandwidth):
|
||||||
freqs = sorted(freqs, key=lambda f: f["frequency"])
|
freqs = sorted(freqs, key=lambda f: f["frequency"])
|
||||||
distances = [
|
distances = [
|
||||||
{"frequency": freqs[i]["frequency"], "distance": freqs[i + 1]["frequency"] - freqs[i]["frequency"]}
|
{
|
||||||
|
"frequency": freqs[i]["frequency"],
|
||||||
|
"distance": freqs[i + 1]["frequency"] - freqs[i]["frequency"],
|
||||||
|
}
|
||||||
for i in range(0, len(freqs) - 1)
|
for i in range(0, len(freqs) - 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -203,15 +226,27 @@ class ServiceHandler(object):
|
|||||||
return bandwidth + len(group) * (freqs[-1] - freqs[0] + 24000)
|
return bandwidth + len(group) * (freqs[-1] - freqs[0] + 24000)
|
||||||
|
|
||||||
total_bandwidth = sum([get_bandwitdh(group) for group in groups])
|
total_bandwidth = sum([get_bandwitdh(group) for group in groups])
|
||||||
return {"num_splits": num_splits, "total_bandwidth": total_bandwidth, "groups": groups}
|
return {
|
||||||
|
"num_splits": num_splits,
|
||||||
|
"total_bandwidth": total_bandwidth,
|
||||||
|
"groups": groups,
|
||||||
|
}
|
||||||
|
|
||||||
usages = [calculate_usage(i) for i in range(0, len(freqs))]
|
usages = [calculate_usage(i) for i in range(0, len(freqs))]
|
||||||
# another possible outcome might be that it's best not to resample at all. this is a special case.
|
# another possible outcome might be that it's best not to resample at all. this is a special case.
|
||||||
usages += [{"num_splits": None, "total_bandwidth": bandwidth * len(freqs), "groups": [freqs]}]
|
usages += [
|
||||||
|
{
|
||||||
|
"num_splits": None,
|
||||||
|
"total_bandwidth": bandwidth * len(freqs),
|
||||||
|
"groups": [freqs],
|
||||||
|
}
|
||||||
|
]
|
||||||
results = sorted(usages, key=lambda f: f["total_bandwidth"])
|
results = sorted(usages, key=lambda f: f["total_bandwidth"])
|
||||||
|
|
||||||
for r in results:
|
for r in results:
|
||||||
logger.debug("splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"]))
|
logger.debug(
|
||||||
|
"splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"])
|
||||||
|
)
|
||||||
|
|
||||||
best = results[0]
|
best = results[0]
|
||||||
if best["num_splits"] is None:
|
if best["num_splits"] is None:
|
||||||
|
Loading…
Reference in New Issue
Block a user