add a new multi-checkbox to select background detection services
This commit is contained in:
		| @@ -1,6 +1,7 @@ | ||||
| from abc import ABC, abstractmethod | ||||
| from .admin import AdminController | ||||
| from owrx.config import Config | ||||
| from owrx.service import ServiceDetector | ||||
| from urllib.parse import parse_qs | ||||
| import logging | ||||
|  | ||||
| @@ -109,6 +110,48 @@ class CheckboxInput(Input): | ||||
|         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): | ||||
|     def __init__(self, value, text): | ||||
|         self.value = value | ||||
| @@ -270,6 +313,7 @@ class SettingsController(AdminController): | ||||
|         Section( | ||||
|             "Background decoding", | ||||
|             CheckboxInput("services_enabled", "Service", checkboxText="Enable background decoding services"), | ||||
|             ServicesCheckboxInput("services_decoders", "Enabled services") | ||||
|         ), | ||||
|         Section( | ||||
|             "APRS settings", | ||||
|   | ||||
| @@ -11,6 +11,7 @@ from owrx.feature import FeatureDetector | ||||
| from owrx.property import PropertyLayer | ||||
| from abc import ABCMeta, abstractmethod | ||||
| from .schedule import ServiceScheduler | ||||
| from functools import reduce | ||||
|  | ||||
| import logging | ||||
|  | ||||
| @@ -49,6 +50,30 @@ class AprsServiceOutput(ServiceOutput): | ||||
|         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): | ||||
|     def __init__(self, source): | ||||
|         self.lock = threading.Lock() | ||||
| @@ -83,24 +108,9 @@ class ServiceHandler(object): | ||||
|         pass | ||||
|  | ||||
|     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"] | ||||
|         available = [mode for mode in configured if mode not in unavailable] | ||||
|  | ||||
|         return mode in available | ||||
|         available = ServiceDetector.getAvailableServices() | ||||
|         return mode in configured and mode in available | ||||
|  | ||||
|     def shutdown(self): | ||||
|         self.stopServices() | ||||
| @@ -141,7 +151,9 @@ class ServiceHandler(object): | ||||
|  | ||||
|         dials = [ | ||||
|             dial | ||||
|             for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range) | ||||
|             for dial in Bandplan.getSharedInstance().collectDialFrequencies( | ||||
|                 frequency_range | ||||
|             ) | ||||
|             if self.isSupported(dial["mode"]) | ||||
|         ] | ||||
|  | ||||
| @@ -155,7 +167,9 @@ class ServiceHandler(object): | ||||
|             groups = self.optimizeResampling(dials, sr) | ||||
|             if groups is None: | ||||
|                 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: | ||||
|                 for group in groups: | ||||
|                     frequencies = sorted([f["frequency"] for f in group]) | ||||
| @@ -163,7 +177,9 @@ class ServiceHandler(object): | ||||
|                     max = frequencies[-1] | ||||
|                     cf = (min + max) / 2 | ||||
|                     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["center_freq"] = cf | ||||
|                     # 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() | ||||
|  | ||||
|                     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 | ||||
|                     self.services.append(resampler) | ||||
| @@ -180,7 +200,10 @@ class ServiceHandler(object): | ||||
|     def optimizeResampling(self, freqs, bandwidth): | ||||
|         freqs = sorted(freqs, key=lambda f: f["frequency"]) | ||||
|         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) | ||||
|         ] | ||||
|  | ||||
| @@ -203,15 +226,27 @@ class ServiceHandler(object): | ||||
|                 return bandwidth + len(group) * (freqs[-1] - freqs[0] + 24000) | ||||
|  | ||||
|             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))] | ||||
|         # 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"]) | ||||
|  | ||||
|         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] | ||||
|         if best["num_splits"] is None: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jakob Ketterl
					Jakob Ketterl