diff --git a/owrx/dsp.py b/owrx/dsp.py index 56e47d0d..60c002b9 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -3,7 +3,9 @@ from owrx.property import PropertyStack, PropertyLayer, PropertyValidator, Prope from owrx.property.validators import OrValidator, RegexValidator, BoolValidator from owrx.modes import Modes, DigitalMode from csdr.chain import Chain -from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, DeemphasisTauChain, DemodulatorError +from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, \ + SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, \ + DeemphasisTauChain, DemodulatorError, RdsChain, DabServiceSelector from csdr.chain.selector import Selector, SecondarySelector from csdr.chain.clientaudio import ClientAudioChain from csdr.chain.fft import FftChain @@ -111,6 +113,9 @@ class ClientDemodulatorChain(Chain): if isinstance(self.demodulator, DeemphasisTauChain): self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau) + if isinstance(self.demodulator, RdsChain): + self.demodulator.setRdsRbds(self.rdsRbds) + self._updateDialFrequency() self._syncSquelch() @@ -332,6 +337,11 @@ class ClientDemodulatorChain(Chain): return self.demodulator.setSlotFilter(filter) + def setDabServiceId(self, serviceId: int) -> None: + if not isinstance(self.demodulator, DabServiceSelector): + return + self.demodulator.setDabServiceId(serviceId) + def setSecondaryFftSize(self, size: int) -> None: if size == self.secondaryFftSize: return @@ -396,6 +406,13 @@ class ClientDemodulatorChain(Chain): if isinstance(self.demodulator, DeemphasisTauChain): self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau) + def setRdsRbds(self, rdsRbds: bool) -> None: + if rdsRbds == self.rdsRbds: + return + self.rdsRbds = rdsRbds + if isinstance(self.demodulator, RdsChain): + self.demodulator.setRdsRbds(self.rdsRbds) + class ModulationValidator(OrValidator): """ @@ -430,6 +447,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) "mod": ModulationValidator(), "secondary_offset_freq": "int", "dmr_filter": "int", + "dab_service_id": "int", "nr_enabled": "bool", "nr_threshold": "int", } @@ -448,6 +466,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) "start_mod", "start_freq", "wfm_deemphasis_tau", + "wfm_rds_rbds", "digital_voice_codecserver", ), ) @@ -515,7 +534,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) self.props.wireProperty("high_cut", self.setHighCut), self.props.wireProperty("mod", self.setDemodulator), self.props.wireProperty("dmr_filter", self.chain.setSlotFilter), + self.props.wireProperty("dab_service_id", self.chain.setDabServiceId), self.props.wireProperty("wfm_deemphasis_tau", self.chain.setWfmDeemphasisTau), + self.props.wireProperty("wfm_rds_rbds", self.chain.setRdsRbds), self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator), self.props.wireProperty("secondary_offset_freq", self.chain.setSecondaryFrequencyOffset), self.props.wireProperty("nr_enabled", self.chain.setNrEnabled), @@ -559,7 +580,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) return NFm(self.props["output_rate"]) elif demod == "wfm": from csdr.chain.analog import WFm - return WFm(self.props["hd_output_rate"], self.props["wfm_deemphasis_tau"]) + return WFm(self.props["hd_output_rate"], self.props["wfm_deemphasis_tau"], self.props["wfm_rds_rbds"]) elif demod == "am": from csdr.chain.analog import Am return Am() @@ -590,6 +611,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) elif demod == "freedv": from csdr.chain.freedv import FreeDV return FreeDV() + elif demod == "dab": + from csdr.chain.dablin import Dablin + return Dablin() elif demod == "empty": from csdr.chain.analog import Empty return Empty() diff --git a/owrx/feature.py b/owrx/feature.py index aa189cbc..1292b612 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -61,6 +61,7 @@ class FeatureDetector(object): "perseussdr": ["perseustest", "nmux"], "airspy": ["soapy_connector", "soapy_airspy"], "airspyhf": ["soapy_connector", "soapy_airspyhf"], + "afedri": ["soapy_connector", "soapy_afedri"], "lime_sdr": ["soapy_connector", "soapy_lime_sdr"], "fifi_sdr": ["alsa", "rockprog", "nmux"], "pluto_sdr": ["soapy_connector", "soapy_pluto_sdr"], @@ -92,6 +93,7 @@ class FeatureDetector(object): "page": ["multimon"], "selcall": ["multimon"], "rds": ["redsea"], + "dab": ["csdreti", "dablin"] "png": ["imagemagick"], } @@ -353,6 +355,14 @@ class FeatureDetector(object): """ return self._has_soapy_driver("airspyhf") + def has_soapy_afedri(self): + """ + The SoapyAfedri module allows using Afedri SDR-Net devices with SoapySDR. + + You can get it [here](https://github.com/alexander-sholohov/SoapyAfedri). + """ + return self._has_soapy_driver("afedri") + def has_soapy_lime_sdr(self): """ The Lime Suite installs - amongst others - a Soapy driver for the LimeSDR device series. @@ -637,6 +647,48 @@ class FeatureDetector(object): """ return self.command_is_runnable("dumpvdl2 --version") + def has_redsea(self): + """ + OpenWebRX can decode RDS data on WFM broadcast station if the `redsea` decoder is available. + + You can find more information [here](https://github.com/windytan/redsea) + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `redsea`. + """ + return self.command_is_runnable("redsea --version") + + def has_csdreti(self): + """ + To decode DAB broadcast signals, OpenWebRX needs the ETI decoder from the + [`csdr-eti`](https://github.com/jketterl/csdr-eti) project, together with the + associated python bindings from [`pycsdr-eti`](https://github.com/jketterl/pycsdr-eti). + + If you are using the OpenWebRX Debian or Ubuntu repository, the `python3-csdr-eti` package should be all you + need. + """ + required_version = LooseVersion("0.1") + + try: + from csdreti.modules import csdreti_version + from csdreti.modules import version as pycsdreti_version + + return ( + LooseVersion(csdreti_version) >= required_version + and LooseVersion(pycsdreti_version) >= required_version + ) + except ImportError: + return False + + def has_dablin(self): + """ + To decode DAB broadcast signals, OpenWebRX needs the [`dablin`](https://github.com/Opendigitalradio/dablin) + decoding software. + + Dablin comes packaged with Debian and Ubuntu, so installing the `dablin` package should get you going. + """ + return self.command_is_runnable("dablin -h") + def has_acarsdec(self): """ OpenWebRX uses the [acarsdec](https://github.com/TLeconte/acarsdec) tool to decode ACARS @@ -659,9 +711,3 @@ class FeatureDetector(object): """ return self.command_is_runnable("multimon-ng --help") - def has_redsea(self): - """ - OpenWebRX uses the [redsea](https://github.com/windytan/redsea) tool to decode RDS - information from FM broadcasts. You will have to compile it from source. - """ - return self.command_is_runnable("redsea --version") diff --git a/owrx/fft.py b/owrx/fft.py index dd1f10d3..9ad88e91 100644 --- a/owrx/fft.py +++ b/owrx/fft.py @@ -72,14 +72,16 @@ class SpectrumThread(SdrSourceEventClient): self.reader = buffer.getReader() threading.Thread(target=self.dsp.pump(self.reader.read, self.sdrSource.writeSpectrumData)).start() - def stop(self): + def stopDsp(self): if self.dsp is None: return self.dsp.stop() self.dsp = None - if self.reader: - self.reader.stop() - self.reader = None + self.reader.stop() + self.reader = None + + def stop(self): + self.stopDsp() self.sdrSource.removeClient(self) while self.subscriptions: self.subscriptions.pop().cancel() @@ -93,8 +95,7 @@ class SpectrumThread(SdrSourceEventClient): def onStateChange(self, state: SdrSourceState): if state is SdrSourceState.STOPPING: - if self.dsp: - self.dsp.stop() + self.stopDsp() elif state == SdrSourceState.RUNNING: if self.dsp is None: self.start() @@ -102,11 +103,7 @@ class SpectrumThread(SdrSourceEventClient): self.dsp.setReader(self.sdrSource.getBuffer().getReader()) def onFail(self): - if self.dsp is None: - return - self.dsp.stop() + self.stopDsp() def onShutdown(self): - if self.dsp is None: - return - self.dsp.stop() + self.stopDsp() diff --git a/owrx/form/input/__init__.py b/owrx/form/input/__init__.py index 57e59b7a..3366b7ab 100644 --- a/owrx/form/input/__init__.py +++ b/owrx/form/input/__init__.py @@ -91,11 +91,13 @@ class Input(ABC): def parse(self, data): if self.id in data: value = self.converter.convert_from_form(data[self.id][0]) - if self.validator is not None: - self.validator.validate(self.id, value) return {self.id: value} return {} + def validate(self, data): + if self.id in data and self.validator is not None: + self.validator.validate(self.id, data[self.id]) + def getLabel(self): return self.label @@ -329,8 +331,8 @@ class ModesInput(DropdownInput): class ExponentialInput(Input): - def __init__(self, id, label, unit, infotext=None): - super().__init__(id, label, infotext=infotext) + def __init__(self, id, label, unit, infotext=None, validator: Validator = None): + super().__init__(id, label, infotext=infotext, validator=validator) self.unit = unit def defaultConverter(self): diff --git a/owrx/form/input/location.py b/owrx/form/input/location.py index 3a9fcb31..36245596 100644 --- a/owrx/form/input/location.py +++ b/owrx/form/input/location.py @@ -59,6 +59,4 @@ class LocationInput(Input): def parse(self, data): value = {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]} - if self.validator is not None: - self.validator.validate(self.id, value) return {self.id: value} diff --git a/owrx/form/input/validator.py b/owrx/form/input/validator.py index fe4e16e5..b5591ea1 100644 --- a/owrx/form/input/validator.py +++ b/owrx/form/input/validator.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from owrx.form.error import ValidationError +from typing import List class Validator(ABC): @@ -14,16 +15,42 @@ class RequiredValidator(Validator): raise ValidationError(key, "Field is required") +class Range(object): + def __init__(self, start: int, end: int = None): + self.start = start + self.end = end if end is not None else start + + def isInRange(self, value): + return self.start <= value <= self.end + + def __str__(self): + if self.start == self.end: + return str(self.start) + return "{start}...{end}".format(**vars(self)) + + class RangeValidator(Validator): def __init__(self, minValue, maxValue): - self.minValue = minValue - self.maxValue = maxValue + self.range = Range(minValue, maxValue) def validate(self, key, value) -> None: if value is None or value == "": return # Ignore empty values - n = float(value) - if n < self.minValue or n > self.maxValue: + if not self.range.isInRange(float(value)): raise ValidationError( - key, "Value must be between {min} and {max}".format(min=self.minValue, max=self.maxValue) + key, "Value must be between {min} and {max}".format(min=self.range.start, max=self.range.end) ) + + +class RangeListValidator(Validator): + def __init__(self, rangeList: List[Range]): + self.rangeList = rangeList + + def validate(self, key, value) -> None: + if not any(range for range in self.rangeList if range.isInRange(value)): + raise ValidationError( + key, "Value is outside of the allowed range(s) {}".format(self._rangeStr()) + ) + + def _rangeStr(self): + return "[{}]".format(", ".join(str(r) for r in self.rangeList)) diff --git a/owrx/form/section.py b/owrx/form/section.py index 1eb9b9e6..f50c65f9 100644 --- a/owrx/form/section.py +++ b/owrx/form/section.py @@ -34,7 +34,9 @@ class Section(object): errors = [] for i in self.inputs: try: - parsed_data.update(i.parse(data)) + res = i.parse(data) + parsed_data.update(res) + i.validate(res) except FormError as e: errors.append(e) except Exception as e: diff --git a/owrx/modes.py b/owrx/modes.py index 6bde728a..ec6e9726 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -133,6 +133,7 @@ class Modes(object): "freedv", "FreeDV", bandpass=Bandpass(300, 3000), requirements=["digital_voice_freedv"], squelch=False ), AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False), + AnalogMode("dab", "DAB", bandpass=Bandpass(-800000, 800000), requirements=["dab"], squelch=False), DigitalMode("bpsk31", "BPSK31", underlying=["usb"]), DigitalMode("bpsk63", "BPSK63", underlying=["usb"]), DigitalMode("rtty170", "RTTY-170 (45)", underlying=["usb", "lsb"]), diff --git a/owrx/source/__init__.py b/owrx/source/__init__.py index f9d9cb4e..9a66f418 100644 --- a/owrx/source/__init__.py +++ b/owrx/source/__init__.py @@ -15,7 +15,7 @@ from owrx.property.filter import ByLambda from owrx.form.input import Input, TextInput, NumberInput, CheckboxInput, ModesInput, ExponentialInput, DropdownInput, Option from owrx.form.input.converter import Converter, OptionalConverter, IntConverter from owrx.form.input.device import GainInput, SchedulerInput, WaterfallLevelsInput -from owrx.form.input.validator import RequiredValidator, RangeValidator +from owrx.form.input.validator import RequiredValidator, Range, RangeListValidator from owrx.form.section import OptionalSection from owrx.feature import FeatureDetector from owrx.log import LogPipe, HistoryHandler @@ -691,7 +691,12 @@ class SdrDeviceDescription(object): ), SchedulerInput("scheduler", "Scheduler"), ExponentialInput("center_freq", "Center frequency", "Hz"), - ExponentialInput("samp_rate", "Sample rate", "S/s"), + ExponentialInput( + "samp_rate", + "Sample rate", + "S/s", + validator=RangeListValidator(self.getSampleRateRanges()) + ), ExponentialInput("start_freq", "Initial frequency", "Hz"), ModesInput("start_mod", "Initial modulation"), NumberInput("initial_squelch_level", "Initial squelch level", append="dBFS"), @@ -726,7 +731,7 @@ class SdrDeviceDescription(object): return True def getDeviceMandatoryKeys(self): - return ["name", "enabled"] + return ["name", "type", "enabled"] def getDeviceOptionalKeys(self): keys = [ @@ -736,6 +741,7 @@ class SdrDeviceDescription(object): "rf_gain", "lfo_offset", "waterfall_levels", + "waterfall_auto_level_default_mode", "scheduler", ] if self.supportsPpm(): @@ -768,3 +774,7 @@ class SdrDeviceDescription(object): self.getProfileMandatoryKeys(), self.getProfileOptionalKeys(), ) + + def getSampleRateRanges(self) -> List[Range]: + # semi-sane default value. should be overridden with more specific values per device. + return [Range(500000, 10000000)]