More stuff merged.

This commit is contained in:
Marat Fayzullin 2024-01-28 23:58:46 -05:00
parent ad78c76649
commit 7a336dad2e
9 changed files with 142 additions and 35 deletions

View File

@ -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()

View File

@ -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")

View File

@ -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()

View File

@ -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):

View File

@ -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}

View File

@ -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))

View File

@ -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:

View File

@ -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"]),

View File

@ -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)]