Merge branch 'develop' into pycsdr

This commit is contained in:
Jakob Ketterl
2021-07-15 18:09:39 +02:00
273 changed files with 16169 additions and 3734 deletions

View File

@ -6,10 +6,20 @@ import socket
import shlex
import time
import signal
import pkgutil
from abc import ABC, abstractmethod
from owrx.command import CommandMapper
from owrx.socket import getAvailablePort
from owrx.property import PropertyStack, PropertyLayer
from owrx.property import PropertyStack, PropertyLayer, PropertyFilter, PropertyCarousel, PropertyDeleted
from owrx.property.filter import ByLambda
from owrx.form.input import Input, TextInput, NumberInput, CheckboxInput, ModesInput, ExponentialInput
from owrx.form.input.converter import OptionalConverter
from owrx.form.input.device import GainInput, SchedulerInput, WaterfallLevelsInput
from owrx.form.input.validator import RequiredValidator
from owrx.form.section import OptionalSection
from owrx.feature import FeatureDetector
from typing import List
from enum import Enum
from pycsdr.modules import SocketClient, Buffer
@ -18,34 +28,85 @@ import logging
logger = logging.getLogger(__name__)
class SdrSourceEventClient(ABC):
@abstractmethod
def onStateChange(self, state):
class SdrSourceState(Enum):
STOPPED = "Stopped"
STARTING = "Starting"
RUNNING = "Running"
STOPPING = "Stopping"
TUNING = "Tuning"
def __str__(self):
return self.value
class SdrBusyState(Enum):
IDLE = 1
BUSY = 2
class SdrClientClass(Enum):
INACTIVE = 1
BACKGROUND = 2
USER = 3
class SdrSourceEventClient(object):
def onStateChange(self, state: SdrSourceState):
pass
@abstractmethod
def onBusyStateChange(self, state):
def onBusyStateChange(self, state: SdrBusyState):
pass
def getClientClass(self):
return SdrSource.CLIENT_INACTIVE
def onFail(self):
pass
def onShutdown(self):
pass
def onDisable(self):
pass
def onEnable(self):
pass
def getClientClass(self) -> SdrClientClass:
return SdrClientClass.INACTIVE
class SdrProfileCarousel(PropertyCarousel):
def __init__(self, props):
super().__init__()
if "profiles" not in props:
return
for profile_id, profile in props["profiles"].items():
self.addLayer(profile_id, profile)
# activate first available profile
self.switch()
props["profiles"].wire(self.handleProfileUpdate)
def addLayer(self, profile_id, profile):
profile_stack = PropertyStack()
profile_stack.addLayer(0, PropertyLayer(profile_id=profile_id).readonly())
profile_stack.addLayer(1, profile)
super().addLayer(profile_id, profile_stack)
def handleProfileUpdate(self, changes):
for profile_id, profile in changes.items():
if profile is PropertyDeleted:
self.removeLayer(profile_id)
else:
self.addLayer(profile_id, profile)
def _getDefaultLayer(self):
# return the first available profile, or the default empty layer if we don't have any
if self.layers:
return next(iter(self.layers.values()))
return super()._getDefaultLayer()
class SdrSource(ABC):
STATE_STOPPED = 0
STATE_STARTING = 1
STATE_RUNNING = 2
STATE_STOPPING = 3
STATE_TUNING = 4
STATE_FAILED = 5
BUSYSTATE_IDLE = 0
BUSYSTATE_BUSY = 1
CLIENT_INACTIVE = 0
CLIENT_BACKGROUND = 1
CLIENT_USER = 2
def __init__(self, id, props):
self.id = id
@ -54,13 +115,24 @@ class SdrSource(ABC):
self.buffer = None
self.props = PropertyStack()
# layer 0 reserved for profile properties
self.profileCarousel = SdrProfileCarousel(props)
# prevent profile names from overriding the device name
self.props.addLayer(0, PropertyFilter(self.profileCarousel, ByLambda(lambda x: x != "name")))
# props from our device config
self.props.addLayer(1, props)
self.props.addLayer(2, Config.get())
# the sdr_id is constant, so we put it in a separate layer
# this is used to detect device changes, that are then sent to the client
self.props.addLayer(2, PropertyLayer(sdr_id=id).readonly())
# finally, accept global config properties from the top-level config
self.props.addLayer(3, Config.get())
self.sdrProps = self.props.filter(*self.getEventNames())
self.profile_id = None
self.activateProfile()
self.wireEvents()
self.port = getAvailablePort()
@ -71,20 +143,46 @@ class SdrSource(ABC):
self.spectrumLock = threading.Lock()
self.process = None
self.modificationLock = threading.Lock()
self.state = SdrSourceState.STOPPED
self.enabled = "enabled" not in props or props["enabled"]
props.filter("enabled").wire(self._handleEnableChanged)
self.failed = False
self.state = SdrSource.STATE_STOPPED
self.busyState = SdrSource.BUSYSTATE_IDLE
self.busyState = SdrBusyState.IDLE
self.validateProfiles()
if self.isAlwaysOn():
if self.isAlwaysOn() and self.isEnabled():
self.start()
def isEnabled(self):
return self.enabled
def _handleEnableChanged(self, changes):
if "enabled" in changes and changes["enabled"] is not PropertyDeleted:
self.enabled = changes["enabled"]
else:
self.enabled = True
if not self.enabled:
self.stop()
for c in self.clients.copy():
if self.isEnabled():
c.onEnable()
else:
c.onDisable()
def isFailed(self):
return self.failed
def fail(self):
self.failed = True
for c in self.clients.copy():
c.onFail()
def validateProfiles(self):
props = PropertyStack()
props.addLayer(1, self.props)
for id, p in self.props["profiles"].items():
props.replaceLayer(0, self._getProfilePropertyLayer(p))
props.replaceLayer(0, p)
if "center_freq" not in props:
logger.warning('Profile "%s" does not specify a center_freq', id)
continue
@ -98,15 +196,6 @@ class SdrSource(ABC):
if start_freq < center_freq - srh or start_freq > center_freq + srh:
logger.warning('start_freq for profile "%s" is out of range', id)
def _getProfilePropertyLayer(self, profile):
layer = PropertyLayer()
for (key, value) in profile.items():
# skip the name, that would overwrite the source name.
if key == "name":
continue
layer[key] = value
return layer
def isAlwaysOn(self):
return "always-on" in self.props and self.props["always-on"]
@ -134,28 +223,18 @@ class SdrSource(ABC):
def getCommand(self):
return [self.getCommandMapper().map(self.getCommandValues())]
def activateProfile(self, profile_id=None):
profiles = self.props["profiles"]
if profile_id is None:
profile_id = list(profiles.keys())[0]
if profile_id not in profiles:
logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.id)
return
if profile_id == self.profile_id:
return
logger.debug("activating profile {0}".format(profile_id))
self.props["profile_id"] = profile_id
profile = profiles[profile_id]
self.profile_id = profile_id
layer = self._getProfilePropertyLayer(profile)
self.props.replaceLayer(0, layer)
def activateProfile(self, profile_id):
logger.debug("activating profile {0} for {1}".format(profile_id, self.getId()))
try:
self.profileCarousel.switch(profile_id)
except KeyError:
logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.getId())
def getId(self):
return self.id
def getProfileId(self):
return self.profile_id
return self.props["profile_id"]
def getProfiles(self):
return self.props["profiles"]
@ -219,22 +298,25 @@ class SdrSource(ABC):
logger.info("Started sdr source: " + cmd)
available = False
failed = False
def wait_for_process_to_end():
nonlocal failed
rc = self.process.wait()
logger.debug("shut down with RC={0}".format(rc))
self.process = None
self.monitor = None
if self.getState() == SdrSource.STATE_RUNNING:
self.failed = True
self.setState(SdrSource.STATE_FAILED)
if self.getState() is SdrSourceState.RUNNING:
self.fail()
else:
self.setState(SdrSource.STATE_STOPPED)
failed = True
self.setState(SdrSourceState.STOPPED)
self.monitor = threading.Thread(target=wait_for_process_to_end, name="source_monitor")
self.monitor.start()
retries = 1000
while retries > 0 and not self.isFailed():
while retries > 0 and not failed:
retries -= 1
if self.monitor is None:
break
@ -248,15 +330,18 @@ class SdrSource(ABC):
time.sleep(0.1)
if not available:
self.failed = True
failed = True
try:
self.postStart()
except Exception:
logger.exception("Exception during postStart()")
self.failed = True
failed = True
self.setState(SdrSource.STATE_FAILED if self.failed else SdrSource.STATE_RUNNING)
if failed:
self.fail()
else:
self.setState(SdrSourceState.RUNNING)
def preStart(self):
"""
@ -273,15 +358,10 @@ class SdrSource(ABC):
def isAvailable(self):
return self.monitor is not None
def isFailed(self):
return self.failed
def stop(self):
self.setState(SdrSource.STATE_STOPPING)
with self.modificationLock:
if self.process is not None:
self.setState(SdrSourceState.STOPPING)
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
except ProcessLookupError:
@ -294,33 +374,47 @@ class SdrSource(ABC):
self.socketClient = None
self.buffer = None
def shutdown(self):
self.stop()
for c in self.clients.copy():
c.onShutdown()
def getClients(self, *args):
if not args:
return self.clients
return [c for c in self.clients if c.getClientClass() in args]
def hasClients(self, *args):
clients = [c for c in self.clients if c.getClientClass() in args]
return len(clients) > 0
return len(self.getClients(*args)) > 0
def addClient(self, c: SdrSourceEventClient):
if c in self.clients:
return
self.clients.append(c)
c.onStateChange(self.getState())
hasUsers = self.hasClients(SdrSource.CLIENT_USER)
hasBackgroundTasks = self.hasClients(SdrSource.CLIENT_BACKGROUND)
hasUsers = self.hasClients(SdrClientClass.USER)
hasBackgroundTasks = self.hasClients(SdrClientClass.BACKGROUND)
if hasUsers or hasBackgroundTasks:
self.start()
self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE)
self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE)
def removeClient(self, c: SdrSourceEventClient):
try:
self.clients.remove(c)
except ValueError:
pass
if c not in self.clients:
return
hasUsers = self.hasClients(SdrSource.CLIENT_USER)
self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE)
self.clients.remove(c)
self.checkStatus()
def checkStatus(self):
hasUsers = self.hasClients(SdrClientClass.USER)
self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE)
# no need to check for users if we are always-on
if self.isAlwaysOn():
return
hasBackgroundTasks = self.hasClients(SdrSource.CLIENT_BACKGROUND)
hasBackgroundTasks = self.hasClients(SdrClientClass.BACKGROUND)
if not hasUsers and not hasBackgroundTasks:
self.stop()
@ -351,19 +445,142 @@ class SdrSource(ABC):
for c in self.spectrumClients:
c.write_spectrum_data(data)
def getState(self):
def getState(self) -> SdrSourceState:
return self.state
def setState(self, state):
def setState(self, state: SdrSourceState):
if state == self.state:
return
self.state = state
for c in self.clients:
for c in self.clients.copy():
c.onStateChange(state)
def setBusyState(self, state):
def setBusyState(self, state: SdrBusyState):
if state == self.busyState:
return
self.busyState = state
for c in self.clients:
for c in self.clients.copy():
c.onBusyStateChange(state)
class SdrDeviceDescriptionMissing(Exception):
pass
class SdrDeviceDescription(object):
@staticmethod
def getByType(sdr_type: str) -> "SdrDeviceDescription":
try:
className = "".join(x for x in sdr_type.title() if x.isalnum()) + "DeviceDescription"
module = __import__("owrx.source.{0}".format(sdr_type), fromlist=[className])
cls = getattr(module, className)
return cls()
except (ModuleNotFoundError, AttributeError):
raise SdrDeviceDescriptionMissing("Device description for type {} not available".format(sdr_type))
@staticmethod
def getTypes():
def get_description(module_name):
try:
description = SdrDeviceDescription.getByType(module_name)
return description.getName()
except SdrDeviceDescriptionMissing:
return None
descriptions = {
module_name: get_description(module_name) for _, module_name, _ in pkgutil.walk_packages(__path__)
}
# filter out empty names and unavailable types
fd = FeatureDetector()
return {k: v for k, v in descriptions.items() if v is not None and fd.is_available(k)}
def getName(self):
"""
must be overridden with a textual representation of the device, to be used for device type selection
:return: str
"""
return None
def getDeviceInputs(self) -> List[Input]:
keys = self.getDeviceMandatoryKeys() + self.getDeviceOptionalKeys()
return [TextInput("name", "Device name", validator=RequiredValidator())] + [
i for i in self.getInputs() if i.id in keys
]
def getProfileInputs(self) -> List[Input]:
keys = self.getProfileMandatoryKeys() + self.getProfileOptionalKeys()
return [TextInput("name", "Profile name", validator=RequiredValidator())] + [
i for i in self.getInputs() if i.id in keys
]
def getInputs(self) -> List[Input]:
return [
CheckboxInput("enabled", "Enable this device", converter=OptionalConverter(defaultFormValue=True)),
GainInput("rf_gain", "Device gain", self.hasAgc()),
NumberInput(
"ppm",
"Frequency correction",
append="ppm",
),
CheckboxInput(
"always-on",
"Keep device running at all times",
infotext="Prevents shutdown of the device when idle. Useful for devices with unreliable startup.",
),
CheckboxInput(
"services",
"Run background services on this device",
),
ExponentialInput(
"lfo_offset",
"Oscillator offset",
"Hz",
infotext="Use this when the actual receiving frequency differs from the frequency to be tuned on the"
+ " device. <br/> Formula: Center frequency + oscillator offset = sdr tune frequency",
),
WaterfallLevelsInput("waterfall_levels", "Waterfall levels"),
SchedulerInput("scheduler", "Scheduler"),
ExponentialInput("center_freq", "Center frequency", "Hz"),
ExponentialInput("samp_rate", "Sample rate", "S/s"),
ExponentialInput("start_freq", "Initial frequency", "Hz"),
ModesInput("start_mod", "Initial modulation"),
NumberInput("initial_squelch_level", "Initial squelch level", append="dBFS"),
]
def hasAgc(self):
# default is True since most devices have agc. override in subclasses if agc is not available
return True
def getDeviceMandatoryKeys(self):
return ["name", "enabled"]
def getDeviceOptionalKeys(self):
return [
"ppm",
"always-on",
"services",
"rf_gain",
"lfo_offset",
"waterfall_levels",
"scheduler",
]
def getProfileMandatoryKeys(self):
return ["name", "center_freq", "samp_rate", "start_freq", "start_mod"]
def getProfileOptionalKeys(self):
return ["initial_squelch_level", "rf_gain", "lfo_offset", "waterfall_levels"]
def getDeviceSection(self):
return OptionalSection(
"Device settings", self.getDeviceInputs(), self.getDeviceMandatoryKeys(), self.getDeviceOptionalKeys()
)
def getProfileSection(self):
return OptionalSection(
"Profile settings",
self.getProfileInputs(),
self.getProfileMandatoryKeys(),
self.getProfileOptionalKeys(),
)

View File

@ -1,5 +1,7 @@
from owrx.command import Flag
from .soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
from owrx.form.input import Input, CheckboxInput
from owrx.form.input.device import BiasTeeInput
from typing import List
class AirspySource(SoapyConnectorSource):
@ -15,3 +17,28 @@ class AirspySource(SoapyConnectorSource):
def getDriver(self):
return "airspy"
class AirspyDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "Airspy R2 or Mini"
def getInputs(self) -> List[Input]:
return super().getInputs() + [
BiasTeeInput(),
CheckboxInput(
"bitpack",
"Enable bit-packing",
infotext="Packs two 12-bit samples into 3 bytes."
+ " Lowers USB bandwidth consumption, increases CPU load",
),
]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["bias_tee", "bitpack"]
def getProfileOptionalKeys(self):
return super().getProfileOptionalKeys() + ["bias_tee"]
def getGainStages(self):
return ["LNA", "MIX", "VGA"]

View File

@ -1,6 +1,11 @@
from .soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
class AirspyhfSource(SoapyConnectorSource):
def getDriver(self):
return "airspyhf"
class AirspyhfDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "Airspy HF+ or Discovery"

View File

@ -1,7 +1,10 @@
from . import SdrSource
from owrx.source import SdrSource, SdrDeviceDescription
from owrx.socket import getAvailablePort
from owrx.property import PropertyDeleted
import socket
from owrx.command import CommandMapper, Flag, Option
from owrx.command import Flag, Option
from typing import List
from owrx.form.input import Input, NumberInput, CheckboxInput
import logging
@ -35,6 +38,8 @@ class ConnectorSource(SdrSource):
def sendControlMessage(self, changes):
for prop, value in changes.items():
if value is PropertyDeleted:
value = None
logger.debug("sending property change over control socket: {0} changed to {1}".format(prop, value))
self.controlSocket.sendall("{prop}:{value}\n".format(prop=prop, value=value).encode())
@ -69,3 +74,27 @@ class ConnectorSource(SdrSource):
values["port"] = self.getPort()
values["controlPort"] = self.getControlPort()
return values
class ConnectorDeviceDescription(SdrDeviceDescription):
def getInputs(self) -> List[Input]:
return super().getInputs() + [
NumberInput(
"rtltcp_compat",
"Port for rtl_tcp compatible data",
infotext="Activate an rtl_tcp compatible interface on the port number specified.<br />"
+ "Note: Port is only available on the local machine, not on the network.<br />"
+ "Note: IQ data may be degraded by the downsampling process to 8 bits.",
),
CheckboxInput(
"iqswap",
"Swap I and Q channels",
infotext="Swapping inverts the spectrum, so this is useful in combination with an inverting mixer",
),
]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["rtltcp_compat", "iqswap"]
def getProfileOptionalKeys(self):
return super().getProfileOptionalKeys() + ["iqswap"]

View File

@ -1,5 +1,5 @@
from abc import ABCMeta
from . import SdrSource
from owrx.source import SdrSource, SdrDeviceDescription
import logging
@ -13,10 +13,9 @@ class DirectSource(SdrSource, metaclass=ABCMeta):
self.sleepOnRestart()
self.start()
def getEventNames(self):
return super().getEventNames() + [
"nmux_memory",
]
def nmux_memory(self):
# in megabytes. This sets the approximate size of the circular buffer used by nmux.
return 50
def getNmuxCommand(self):
props = self.sdrProps
@ -24,13 +23,10 @@ class DirectSource(SdrSource, metaclass=ABCMeta):
nmux_bufcnt = nmux_bufsize = 0
while nmux_bufsize < props["samp_rate"] / 4:
nmux_bufsize += 4096
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6:
while nmux_bufsize * nmux_bufcnt < self.nmux_memory() * 1e6:
nmux_bufcnt += 1
if nmux_bufcnt == 0 or nmux_bufsize == 0:
raise ValueError(
"Error: nmux_bufsize or nmux_bufcnt is zero. "
"These depend on nmux_memory and samp_rate options in config_webrx.py"
)
raise ValueError("Error: unable to calculate nmux buffer parameters.")
return [
"nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1"
@ -51,3 +47,7 @@ class DirectSource(SdrSource, metaclass=ABCMeta):
# override this in subclasses, if necessary
def sleepOnRestart(self):
pass
class DirectSourceDeviceDescription(SdrDeviceDescription):
pass

View File

@ -1,17 +0,0 @@
from owrx.source.connector import ConnectorSource
from owrx.command import Argument, Flag
class Eb200Source(ConnectorSource):
def getCommandMapper(self):
return (
super()
.getCommandMapper()
.setBase("eb200_connector")
.setMappings(
{
"long": Flag("-l"),
"remote": Argument(),
}
)
)

View File

@ -1,6 +1,11 @@
from owrx.source.soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
class FcdppSource(SoapyConnectorSource):
def getDriver(self):
return "fcdpp"
class FcdppDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "FunCube Dongle Pro+"

View File

@ -1,5 +1,5 @@
from owrx.command import Option
from .direct import DirectSource
from owrx.source.direct import DirectSource, DirectSourceDeviceDescription
from subprocess import Popen
import logging
@ -37,3 +37,8 @@ class FifiSdrSource(DirectSource):
def onPropertyChange(self, changes):
if "center_freq" in changes:
self.sendRockProgFrequency(changes["center_freq"])
class FifiSdrDeviceDescription(DirectSourceDeviceDescription):
def getName(self):
return "FiFi SDR"

View File

@ -1,4 +1,7 @@
from .soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
from owrx.form.input import Input
from owrx.form.input.device import BiasTeeInput
from typing import List
class HackrfSource(SoapyConnectorSource):
@ -9,3 +12,20 @@ class HackrfSource(SoapyConnectorSource):
def getDriver(self):
return "hackrf"
class HackrfDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "HackRF"
def getInputs(self) -> List[Input]:
return super().getInputs() + [BiasTeeInput()]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["bias_tee"]
def getProfileOptionalKeys(self):
return super().getProfileOptionalKeys() + ["bias_tee"]
def getGainStages(self):
return ["LNA", "AMP", "VGA"]

View File

@ -1,5 +1,9 @@
from .connector import ConnectorSource
from owrx.command import Flag, Option
from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription
from owrx.command import Option
from owrx.form.error import ValidationError
from owrx.form.input import Input, NumberInput, TextInput
from owrx.form.input.validator import RangeValidator
from typing import List
# In order to use an HPSDR radio, you must install hpsdrconnector from https://github.com/jancona/hpsdrconnector
# These are the command line options available:
@ -33,3 +37,26 @@ class HpsdrSource(ConnectorSource):
}
)
)
class RemoteInput(TextInput):
def __init__(self):
super().__init__(
"remote", "Remote IP", infotext="Remote IP address to connect to."
)
class HpsdrDeviceDescription(ConnectorDeviceDescription):
def getName(self):
return "HPSDR devices (Hermes / Hermes Lite 2 / Red Pitaya)"
def getInputs(self) -> List[Input]:
return super().getInputs() + [
RemoteInput(),
NumberInput("rf_gain", "LNA Gain", "LNA gain between 0 (-12dB) and 60 (48dB)", validator=RangeValidator(0, 60)),
]
def getDeviceOptionalKeys(self):
return list(filter(lambda x : x not in ["rtltcp_compat", "iqswap"], super().getDeviceOptionalKeys())) + ["remote"]
def getProfileOptionalKeys(self):
return list(filter(lambda x : x != "iqswap", super().getProfileOptionalKeys()))

View File

@ -1,6 +1,11 @@
from .soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
class LimeSdrSource(SoapyConnectorSource):
def getDriver(self):
return "lime"
class LimeSdrDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "LimeSDR device"

View File

@ -1,5 +1,7 @@
from .direct import DirectSource
from owrx.command import Flag, Option
from owrx.source.direct import DirectSource, DirectSourceDeviceDescription
from owrx.command import Option, Flag
from owrx.form.input import Input, DropdownEnum, DropdownInput, CheckboxInput
from typing import List
#
@ -29,9 +31,49 @@ class PerseussdrSource(DirectSource):
"samp_rate": Option("-s"),
"tuner_freq": Option("-f"),
"attenuator": Option("-u"),
"adc_preamp": Option("-m"),
"adc_dither": Option("-x"),
"wideband": Option("-w"),
"adc_preamp": Flag("-m"),
"adc_dither": Flag("-x"),
"wideband": Flag("-w"),
}
)
)
class AttenuatorOptions(DropdownEnum):
ATTENUATOR_0 = 0
ATTENUATOR_10 = -10
ATTENUATOR_20 = -20
ATTENUATOR_30 = -30
def __str__(self):
return "{value} dB".format(value=self.value)
class PerseussdrDeviceDescription(DirectSourceDeviceDescription):
def getName(self):
return "Perseus SDR"
def getInputs(self) -> List[Input]:
return super().getInputs() + [
DropdownInput("attenuator", "Attenuator", options=AttenuatorOptions),
CheckboxInput("adc_preamp", "Activate ADC preamp"),
CheckboxInput("adc_dither", "Enable ADC dithering"),
CheckboxInput("wideband", "Disable analog filters"),
]
def getDeviceOptionalKeys(self):
# no rf_gain
return [key for key in super().getDeviceOptionalKeys() if key != "rf_gain"] + [
"attenuator",
"adc_preamp",
"adc_dither",
"wideband",
]
def getProfileOptionalKeys(self):
return [key for key in super().getProfileOptionalKeys() if key != "rf_gain"] + [
"attenuator",
"adc_preamp",
"adc_dither",
"wideband",
]

View File

@ -1,6 +1,11 @@
from .soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
class PlutoSdrSource(SoapyConnectorSource):
def getDriver(self):
return "plutosdr"
class PlutoSdrDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "PlutoSDR"

View File

@ -1,6 +1,11 @@
from .soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
class RadioberrySource(SoapyConnectorSource):
def getDriver(self):
return "radioberry"
class RadioberryDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "RadioBerry"

View File

@ -1,6 +0,0 @@
from .soapy import SoapyConnectorSource
class RedPitayaSource(SoapyConnectorSource):
def getDriver(self):
return "redpitaya"

View File

@ -1,5 +1,8 @@
from .connector import ConnectorSource
from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription
from owrx.command import Flag, Option
from typing import List
from owrx.form.input import Input, TextInput
from owrx.form.input.device import BiasTeeInput, DirectSamplingInput
class RtlSdrSource(ConnectorSource):
@ -10,3 +13,25 @@ class RtlSdrSource(ConnectorSource):
.setBase("rtl_connector")
.setMappings({"bias_tee": Flag("-b"), "direct_sampling": Option("-e")})
)
class RtlSdrDeviceDescription(ConnectorDeviceDescription):
def getName(self):
return "RTL-SDR device"
def getInputs(self) -> List[Input]:
return super().getInputs() + [
TextInput(
"device",
"Device identifier",
infotext="Device serial number or index",
),
BiasTeeInput(),
DirectSamplingInput(),
]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["device", "bias_tee", "direct_sampling"]
def getProfileOptionalKeys(self):
return super().getProfileOptionalKeys() + ["bias_tee", "direct_sampling"]

View File

@ -1,4 +1,7 @@
from .soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
from owrx.form.input import Input
from owrx.form.input.device import BiasTeeInput, DirectSamplingInput
from typing import List
class RtlSdrSoapySource(SoapyConnectorSource):
@ -9,3 +12,17 @@ class RtlSdrSoapySource(SoapyConnectorSource):
def getDriver(self):
return "rtlsdr"
class RtlSdrSoapyDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "RTL-SDR device (via SoapySDR)"
def getInputs(self) -> List[Input]:
return super().getInputs() + [BiasTeeInput(), DirectSamplingInput()]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["bias_tee", "direct_sampling"]
def getProfileOptionalKeys(self):
return super().getProfileOptionalKeys() + ["bias_tee", "direct_sampling"]

View File

@ -1,5 +1,8 @@
from .connector import ConnectorSource
from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription
from owrx.command import Flag, Option, Argument
from owrx.form.input import Input
from owrx.form.input.device import RemoteInput
from typing import List
class RtlTcpSource(ConnectorSource):
@ -16,3 +19,14 @@ class RtlTcpSource(ConnectorSource):
}
)
)
class RtlTcpDeviceDescription(ConnectorDeviceDescription):
def getName(self):
return "RTL-SDR device (via rtl_tcp)"
def getInputs(self) -> List[Input]:
return super().getInputs() + [RemoteInput()]
def getDeviceMandatoryKeys(self):
return super().getDeviceMandatoryKeys() + ["remote"]

54
owrx/source/runds.py Normal file
View File

@ -0,0 +1,54 @@
from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription
from owrx.command import Argument, Flag, Option
from owrx.form.input import Input, DropdownInput, DropdownEnum, CheckboxInput
from owrx.form.input.device import RemoteInput
from typing import List
class RundsSource(ConnectorSource):
def getCommandMapper(self):
return (
super()
.getCommandMapper()
.setBase("runds_connector")
.setMappings(
{
"long": Flag("-l"),
"remote": Argument(),
"protocol": Option("-m"),
}
)
)
class ProtocolOptions(DropdownEnum):
PROTOCOL_EB200 = ("eb200", "EB200 protocol")
PROTOCOL_AMMOS = ("ammos", "Ammos protocol")
def __new__(cls, *args, **kwargs):
value, description = args
obj = object.__new__(cls)
obj._value_ = value
obj.description = description
return obj
def __str__(self):
return self.description
class RundsDeviceDescription(ConnectorDeviceDescription):
def getName(self):
return "R&S device using EB200 or Ammos protocol"
def getInputs(self) -> List[Input]:
return super().getInputs() + [
RemoteInput(),
DropdownInput("protocol", "Protocol", ProtocolOptions),
CheckboxInput("long", "Use 32-bit sample size (LONG)"),
]
def getDeviceMandatoryKeys(self):
return super().getDeviceMandatoryKeys() + ["remote"]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["protocol", "long"]

View File

@ -1,6 +1,14 @@
from owrx.source.connector import ConnectorSource
from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription
class SddcSource(ConnectorSource):
def getCommandMapper(self):
return super().getCommandMapper().setBase("sddc_connector")
class SddcDeviceDescription(ConnectorDeviceDescription):
def getName(self):
return "BBRF103 / RX666 / RX888 device (libsddc)"
def hasAgc(self):
return False

View File

@ -1,4 +1,7 @@
from .soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
from owrx.form.input import Input, CheckboxInput, DropdownInput, DropdownEnum
from owrx.form.input.device import BiasTeeInput
from typing import List
class SdrplaySource(SoapyConnectorSource):
@ -17,3 +20,45 @@ class SdrplaySource(SoapyConnectorSource):
def getDriver(self):
return "sdrplay"
class IfModeOptions(DropdownEnum):
IFMODE_ZERO_IF = "Zero-IF"
IFMODE_450 = "450kHz"
IFMODE_1620 = "1620kHz"
IFMODE_2048 = "2048kHz"
def __str__(self):
return self.value
class SdrplayDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "SDRPlay device (RSP1, RSP2, RSPDuo, RSPDx)"
def getGainStages(self):
return ["RFGR", "IFGR"]
def getInputs(self) -> List[Input]:
return super().getInputs() + [
BiasTeeInput(),
CheckboxInput(
"rf_notch",
"Enable RF notch filter",
),
CheckboxInput(
"dab_notch",
"Enable DAB notch filter",
),
DropdownInput(
"if_mode",
"IF Mode",
IfModeOptions,
),
]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["bias_tee", "rf_notch", "dab_notch", "if_mode"]
def getProfileOptionalKeys(self):
return super().getProfileOptionalKeys() + ["bias_tee", "rf_notch", "dab_notch", "if_mode"]

View File

@ -1,6 +1,10 @@
from abc import ABCMeta, abstractmethod
from owrx.command import Option
from .connector import ConnectorSource
from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription
from typing import List
from owrx.form.input import Input, TextInput
from owrx.form.input.device import GainInput
from owrx.soapy import SoapySettings
class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
@ -29,25 +33,6 @@ class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
def getEventNames(self):
return super().getEventNames() + list(self.getSoapySettingsMappings().keys())
def parseDeviceString(self, dstr):
def decodeComponent(c):
kv = c.split("=", 1)
if len(kv) < 2:
return c
else:
return {kv[0]: kv[1]}
return [decodeComponent(c) for c in dstr.split(",")]
def encodeDeviceString(self, dobj):
def encodeComponent(c):
if isinstance(c, str):
return c
else:
return ",".join(["{0}={1}".format(key, value) for key, value in c.items()])
return ",".join([encodeComponent(c) for c in dobj])
def buildSoapyDeviceParameters(self, parsed, values):
"""
this method always attempts to inject a driver= part into the soapysdr query, depending on what connector was used.
@ -75,11 +60,11 @@ class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
def getCommandValues(self):
values = super().getCommandValues()
if "device" in values and values["device"] is not None:
parsed = self.parseDeviceString(values["device"])
parsed = SoapySettings.parse(values["device"])
else:
parsed = []
modified = self.buildSoapyDeviceParameters(parsed, values)
values["device"] = self.encodeDeviceString(modified)
values["device"] = SoapySettings.encode(modified)
settings = ",".join(["{0}={1}".format(k, v) for k, v in self.buildSoapySettings(values).items()])
if len(settings):
values["soapy_settings"] = settings
@ -94,3 +79,30 @@ class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
if settings:
changes["settings"] = ",".join("{0}={1}".format(k, v) for k, v in settings.items())
super().onPropertyChange(changes)
class SoapyConnectorDeviceDescription(ConnectorDeviceDescription):
def getInputs(self) -> List[Input]:
return super().getInputs() + [
TextInput(
"device",
"Device identifier",
infotext='SoapySDR device identifier string (example: "serial=123456789")',
),
GainInput(
"rf_gain",
"Device Gain",
gain_stages=self.getGainStages(),
has_agc=self.hasAgc(),
),
TextInput("antenna", "Antenna"),
]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["device", "rf_gain", "antenna"]
def getProfileOptionalKeys(self):
return super().getProfileOptionalKeys() + ["antenna"]
def getGainStages(self):
return None

View File

@ -1,4 +1,8 @@
from .soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
from owrx.form.input import Input, TextInput
from owrx.form.input.device import RemoteInput
from owrx.form.input.converter import OptionalConverter
from typing import List
class SoapyRemoteSource(SoapyConnectorSource):
@ -15,3 +19,25 @@ class SoapyRemoteSource(SoapyConnectorSource):
if "remote_driver" in values and values["remote_driver"] is not None:
params += [{"remote:driver": values["remote_driver"]}]
return params
class SoapyRemoteDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "Device connected to a SoapyRemote server"
def getInputs(self) -> List[Input]:
return super().getInputs() + [
RemoteInput(),
TextInput(
"remote_driver",
"Remote driver",
infotext="SoapySDR driver to be used on the remote SoapySDRServer",
converter=OptionalConverter(),
),
]
def getDeviceMandatoryKeys(self):
return super().getDeviceMandatoryKeys() + ["remote"]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["remote_driver"]

View File

@ -1,6 +1,11 @@
from .soapy import SoapyConnectorSource
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
class UhdSource(SoapyConnectorSource):
def getDriver(self):
return "uhd"
class UhdDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "Ettus Research USRP device"