More stuff merged.
This commit is contained in:
parent
ad78c76649
commit
7a336dad2e
28
owrx/dsp.py
28
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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
21
owrx/fft.py
21
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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"]),
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue