2019-05-13 17:19:15 +00:00
|
|
|
import subprocess
|
|
|
|
from functools import reduce
|
2019-12-27 10:37:12 +00:00
|
|
|
from operator import and_, or_
|
2019-06-15 11:29:59 +00:00
|
|
|
import re
|
|
|
|
from distutils.version import LooseVersion
|
2019-07-05 20:31:46 +00:00
|
|
|
import inspect
|
2019-11-14 21:13:02 +00:00
|
|
|
from owrx.config import PropertyManager
|
|
|
|
import shlex
|
2019-05-12 13:56:18 +00:00
|
|
|
|
|
|
|
import logging
|
2019-07-21 17:40:28 +00:00
|
|
|
|
2019-05-12 13:56:18 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class UnknownFeatureException(Exception):
|
|
|
|
pass
|
|
|
|
|
2019-07-05 20:31:46 +00:00
|
|
|
|
2019-05-12 13:56:18 +00:00
|
|
|
class FeatureDetector(object):
|
|
|
|
features = {
|
2019-12-21 18:24:14 +00:00
|
|
|
# core features; we won't start without these
|
2019-07-21 17:40:28 +00:00
|
|
|
"core": ["csdr", "nmux", "nc"],
|
2019-12-21 18:24:14 +00:00
|
|
|
# different types of sdrs and their requirements
|
|
|
|
"rtl_sdr": ["rtl_connector"],
|
2019-12-27 10:37:12 +00:00
|
|
|
"sdrplay": ["soapy_connector", "soapy_sdrplay"],
|
2019-07-21 17:40:28 +00:00
|
|
|
"hackrf": ["hackrf_transfer"],
|
2019-12-27 10:37:12 +00:00
|
|
|
"airspy": ["soapy_connector", "soapy_airspy"],
|
|
|
|
"airspyhf": ["soapy_connector", "soapy_airspyhf"],
|
2019-12-21 18:24:14 +00:00
|
|
|
"fifi_sdr": ["alsa"],
|
|
|
|
# optional features and their requirements
|
2019-07-21 17:40:28 +00:00
|
|
|
"digital_voice_digiham": ["digiham", "sox"],
|
|
|
|
"digital_voice_dsd": ["dsd", "sox", "digiham"],
|
|
|
|
"wsjt-x": ["wsjtx", "sox"],
|
2019-12-01 14:42:50 +00:00
|
|
|
"packet": ["direwolf", "sox"],
|
2020-01-06 21:08:17 +00:00
|
|
|
"pocsag": ["digiham", "sox"],
|
2019-05-12 13:56:18 +00:00
|
|
|
}
|
|
|
|
|
2019-05-13 17:19:15 +00:00
|
|
|
def feature_availability(self):
|
|
|
|
return {name: self.is_available(name) for name in FeatureDetector.features}
|
|
|
|
|
2019-07-05 17:30:24 +00:00
|
|
|
def feature_report(self):
|
2019-07-05 20:31:46 +00:00
|
|
|
def requirement_details(name):
|
|
|
|
available = self.has_requirement(name)
|
|
|
|
return {
|
|
|
|
"available": available,
|
|
|
|
# as of now, features are always enabled as soon as they are available. this may change in the future.
|
|
|
|
"enabled": available,
|
2019-07-21 17:40:28 +00:00
|
|
|
"description": self.get_requirement_description(name),
|
2019-07-05 20:31:46 +00:00
|
|
|
}
|
|
|
|
|
2019-07-05 17:30:24 +00:00
|
|
|
def feature_details(name):
|
2019-07-05 20:31:46 +00:00
|
|
|
return {
|
|
|
|
"description": "",
|
|
|
|
"available": self.is_available(name),
|
2019-07-21 17:40:28 +00:00
|
|
|
"requirements": {name: requirement_details(name) for name in self.get_requirements(name)},
|
2019-07-05 20:31:46 +00:00
|
|
|
}
|
|
|
|
|
2019-07-05 17:30:24 +00:00
|
|
|
return {name: feature_details(name) for name in FeatureDetector.features}
|
|
|
|
|
2019-05-12 13:56:18 +00:00
|
|
|
def is_available(self, feature):
|
|
|
|
return self.has_requirements(self.get_requirements(feature))
|
|
|
|
|
|
|
|
def get_requirements(self, feature):
|
|
|
|
try:
|
|
|
|
return FeatureDetector.features[feature]
|
|
|
|
except KeyError:
|
2019-07-21 17:40:28 +00:00
|
|
|
raise UnknownFeatureException('Feature "{0}" is not known.'.format(feature))
|
2019-05-12 13:56:18 +00:00
|
|
|
|
|
|
|
def has_requirements(self, requirements):
|
|
|
|
passed = True
|
|
|
|
for requirement in requirements:
|
2019-07-05 20:31:46 +00:00
|
|
|
passed = passed and self.has_requirement(requirement)
|
2019-05-12 13:56:18 +00:00
|
|
|
return passed
|
|
|
|
|
2019-07-05 20:31:46 +00:00
|
|
|
def _get_requirement_method(self, requirement):
|
|
|
|
methodname = "has_" + requirement
|
|
|
|
if hasattr(self, methodname) and callable(getattr(self, methodname)):
|
|
|
|
return getattr(self, methodname)
|
|
|
|
return None
|
|
|
|
|
|
|
|
def has_requirement(self, requirement):
|
|
|
|
method = self._get_requirement_method(requirement)
|
|
|
|
if method is not None:
|
|
|
|
return method()
|
|
|
|
else:
|
|
|
|
logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement))
|
|
|
|
return False
|
|
|
|
|
|
|
|
def get_requirement_description(self, requirement):
|
|
|
|
return inspect.getdoc(self._get_requirement_method(requirement))
|
|
|
|
|
2019-05-13 17:19:15 +00:00
|
|
|
def command_is_runnable(self, command):
|
2019-11-23 00:12:21 +00:00
|
|
|
tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"]
|
2019-11-14 21:13:02 +00:00
|
|
|
cmd = shlex.split(command)
|
|
|
|
try:
|
|
|
|
process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=tmp_dir)
|
|
|
|
return process.wait() != 32512
|
|
|
|
except FileNotFoundError:
|
|
|
|
return False
|
2019-05-13 17:19:15 +00:00
|
|
|
|
2019-05-12 13:56:18 +00:00
|
|
|
def has_csdr(self):
|
2019-07-05 20:31:46 +00:00
|
|
|
"""
|
|
|
|
OpenWebRX uses the demodulator and pipeline tools provided by the csdr project. Please check out [the project
|
|
|
|
page on github](https://github.com/simonyiszk/csdr) for further details and installation instructions.
|
|
|
|
"""
|
2019-05-13 17:19:15 +00:00
|
|
|
return self.command_is_runnable("csdr")
|
2019-05-12 13:56:18 +00:00
|
|
|
|
|
|
|
def has_nmux(self):
|
2019-07-05 20:31:46 +00:00
|
|
|
"""
|
|
|
|
Nmux is another tool provided by the csdr project. It is used for internal multiplexing of the IQ data streams.
|
|
|
|
If you're missing nmux even though you have csdr installed, please update your csdr version.
|
|
|
|
"""
|
2019-05-13 17:19:15 +00:00
|
|
|
return self.command_is_runnable("nmux --help")
|
2019-05-12 13:56:18 +00:00
|
|
|
|
2019-06-04 22:08:56 +00:00
|
|
|
def has_nc(self):
|
2019-07-05 20:31:46 +00:00
|
|
|
"""
|
|
|
|
Nc is the client used to connect to the nmux multiplexer. It is provided by either the BSD netcat (recommended
|
|
|
|
for better performance) or GNU netcat packages. Please check your distribution package manager for options.
|
|
|
|
"""
|
2019-07-21 17:40:28 +00:00
|
|
|
return self.command_is_runnable("nc --help")
|
2019-06-04 22:08:56 +00:00
|
|
|
|
2019-05-12 13:56:18 +00:00
|
|
|
def has_rtl_sdr(self):
|
2019-07-05 20:31:46 +00:00
|
|
|
"""
|
|
|
|
The rtl-sdr command is required to read I/Q data from an RTL SDR USB-Stick. It is available in most
|
|
|
|
distribution package managers.
|
|
|
|
"""
|
2019-05-13 17:19:15 +00:00
|
|
|
return self.command_is_runnable("rtl_sdr --help")
|
2019-05-12 13:56:18 +00:00
|
|
|
|
|
|
|
def has_hackrf_transfer(self):
|
2019-07-05 20:31:46 +00:00
|
|
|
"""
|
|
|
|
To use a HackRF, compile the HackRF host tools from its "stdout" branch:
|
|
|
|
```
|
|
|
|
git clone https://github.com/mossmann/hackrf/
|
|
|
|
cd hackrf
|
|
|
|
git fetch
|
|
|
|
git checkout origin/stdout
|
|
|
|
cd host
|
|
|
|
mkdir build
|
|
|
|
cd build
|
|
|
|
cmake .. -DINSTALL_UDEV_RULES=ON
|
|
|
|
make
|
|
|
|
sudo make install
|
|
|
|
```
|
|
|
|
"""
|
2019-05-12 13:56:18 +00:00
|
|
|
# TODO i don't have a hackrf, so somebody doublecheck this.
|
|
|
|
# TODO also check if it has the stdout feature
|
2019-05-13 17:19:15 +00:00
|
|
|
return self.command_is_runnable("hackrf_transfer --help")
|
|
|
|
|
|
|
|
def has_digiham(self):
|
2019-07-05 20:31:46 +00:00
|
|
|
"""
|
|
|
|
To use digital voice modes, the digiham package is required. You can find the package and installation
|
|
|
|
instructions [here](https://github.com/jketterl/digiham).
|
|
|
|
|
|
|
|
Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work.
|
|
|
|
If you have an older verison of digiham installed, please update it along with openwebrx.
|
|
|
|
As of now, we require version 0.2 of digiham.
|
|
|
|
"""
|
2019-06-15 11:29:59 +00:00
|
|
|
required_version = LooseVersion("0.2")
|
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
digiham_version_regex = re.compile("^digiham version (.*)$")
|
|
|
|
|
2019-06-15 11:29:59 +00:00
|
|
|
def check_digiham_version(command):
|
2019-05-13 17:19:15 +00:00
|
|
|
try:
|
2019-06-15 11:29:59 +00:00
|
|
|
process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
|
2019-11-11 17:07:14 +00:00
|
|
|
matches = digiham_version_regex.match(process.stdout.readline().decode())
|
|
|
|
if matches is None:
|
|
|
|
return False
|
|
|
|
version = LooseVersion(matches.group(1))
|
2019-06-15 11:29:59 +00:00
|
|
|
process.wait(1)
|
|
|
|
return version >= required_version
|
2019-05-13 17:19:15 +00:00
|
|
|
except FileNotFoundError:
|
|
|
|
return False
|
2019-07-21 17:40:28 +00:00
|
|
|
|
2019-07-13 21:16:25 +00:00
|
|
|
return reduce(
|
|
|
|
and_,
|
|
|
|
map(
|
|
|
|
check_digiham_version,
|
2019-07-21 17:40:28 +00:00
|
|
|
[
|
|
|
|
"rrc_filter",
|
|
|
|
"ysf_decoder",
|
|
|
|
"dmr_decoder",
|
|
|
|
"mbe_synthesizer",
|
|
|
|
"gfsk_demodulator",
|
|
|
|
"digitalvoice_filter",
|
2020-01-06 21:08:17 +00:00
|
|
|
"fsk_demodulator",
|
|
|
|
"pocsag_decoder",
|
2019-07-21 17:40:28 +00:00
|
|
|
],
|
2019-07-13 21:16:25 +00:00
|
|
|
),
|
2019-07-21 17:40:28 +00:00
|
|
|
True,
|
2019-07-13 21:16:25 +00:00
|
|
|
)
|
2019-05-13 17:19:15 +00:00
|
|
|
|
2019-11-21 14:31:37 +00:00
|
|
|
def _check_connector(self, command):
|
|
|
|
required_version = LooseVersion("0.1")
|
|
|
|
|
|
|
|
owrx_connector_version_regex = re.compile("^owrx-connector version (.*)$")
|
|
|
|
|
|
|
|
try:
|
|
|
|
process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
|
|
|
|
matches = owrx_connector_version_regex.match(process.stdout.readline().decode())
|
|
|
|
if matches is None:
|
|
|
|
return False
|
|
|
|
version = LooseVersion(matches.group(1))
|
|
|
|
process.wait(1)
|
|
|
|
return version >= required_version
|
|
|
|
except FileNotFoundError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
def has_rtl_connector(self):
|
2019-11-11 17:07:14 +00:00
|
|
|
"""
|
|
|
|
The owrx_connector package offers direct interfacing between your hardware and openwebrx. It allows quicker
|
|
|
|
frequency switching, uses less CPU and can even provide more stability in some cases.
|
|
|
|
|
2019-12-27 10:37:12 +00:00
|
|
|
You can get it [here](https://github.com/jketterl/owrx_connector).
|
2019-11-11 17:07:14 +00:00
|
|
|
"""
|
2019-11-21 14:31:37 +00:00
|
|
|
return self._check_connector("rtl_connector")
|
2019-11-11 17:07:14 +00:00
|
|
|
|
2019-11-21 14:31:37 +00:00
|
|
|
def has_soapy_connector(self):
|
|
|
|
"""
|
|
|
|
The owrx_connector package offers direct interfacing between your hardware and openwebrx. It allows quicker
|
|
|
|
frequency switching, uses less CPU and can even provide more stability in some cases.
|
2019-11-11 17:07:14 +00:00
|
|
|
|
2019-12-27 10:37:12 +00:00
|
|
|
You can get it [here](https://github.com/jketterl/owrx_connector).
|
2019-11-21 14:31:37 +00:00
|
|
|
"""
|
|
|
|
return self._check_connector("soapy_connector")
|
2019-11-11 17:07:14 +00:00
|
|
|
|
2019-12-27 10:37:12 +00:00
|
|
|
def _has_soapy_driver(self, driver):
|
2019-12-30 15:38:16 +00:00
|
|
|
try:
|
|
|
|
process = subprocess.Popen(["SoapySDRUtil", "--info"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
|
|
|
driverRegex = re.compile("^Module found: .*lib(.*)Support.so")
|
2019-12-28 00:24:07 +00:00
|
|
|
|
2019-12-30 15:38:16 +00:00
|
|
|
def matchLine(line):
|
|
|
|
matches = driverRegex.match(line.decode())
|
|
|
|
return matches is not None and matches.group(1) == driver
|
2019-12-28 00:24:07 +00:00
|
|
|
|
2019-12-30 15:38:16 +00:00
|
|
|
lines = [matchLine(line) for line in process.stdout]
|
|
|
|
return reduce(or_, lines, False)
|
|
|
|
except FileNotFoundError:
|
|
|
|
return False
|
2019-12-27 10:37:12 +00:00
|
|
|
|
|
|
|
def has_soapy_sdrplay(self):
|
|
|
|
"""
|
|
|
|
The SoapySDR module for sdrplay devices is required for interfacing with SDRPlay devices (RSP1*, RSP2*, RSPDuo)
|
|
|
|
|
|
|
|
You can get it [here](https://github.com/pothosware/SoapySDRPlay/wiki).
|
|
|
|
"""
|
|
|
|
return self._has_soapy_driver("sdrPlay")
|
|
|
|
|
|
|
|
def has_soapy_airspy(self):
|
|
|
|
"""
|
|
|
|
The SoapySDR module for airspy devices is required for interfacing with Airspy devices (Airspy R2, Airspy Mini).
|
|
|
|
|
|
|
|
You can get it [here](https://github.com/pothosware/SoapyAirspy/wiki).
|
|
|
|
"""
|
|
|
|
return self._has_soapy_driver("airspy")
|
|
|
|
|
|
|
|
def has_soapy_airspyhf(self):
|
|
|
|
"""
|
|
|
|
The SoapySDR module for airspyhf devices is required for interfacing with Airspy HF devices (Airspy HF+,
|
|
|
|
Airspy HF discovery).
|
|
|
|
|
|
|
|
You can get it [here](https://github.com/pothosware/SoapyAirspyHF/wiki).
|
|
|
|
"""
|
|
|
|
return self._has_soapy_driver("airspyhf")
|
|
|
|
|
2019-05-13 17:19:15 +00:00
|
|
|
def has_dsd(self):
|
2019-07-05 20:31:46 +00:00
|
|
|
"""
|
|
|
|
The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version
|
|
|
|
modified by F4EXB that provides stdin/stdout support. You can find it [here](https://github.com/f4exb/dsd).
|
|
|
|
"""
|
2019-05-13 17:19:15 +00:00
|
|
|
return self.command_is_runnable("dsd")
|
|
|
|
|
|
|
|
def has_sox(self):
|
2019-07-05 20:31:46 +00:00
|
|
|
"""
|
|
|
|
The sox audio library is used to convert between the typical 8 kHz audio sampling rate used by digital modes and
|
|
|
|
the audio sampling rate requested by the client.
|
|
|
|
|
|
|
|
It is available for most distributions through the respective package manager.
|
|
|
|
"""
|
2019-06-07 13:11:04 +00:00
|
|
|
return self.command_is_runnable("sox")
|
|
|
|
|
|
|
|
def has_direwolf(self):
|
2019-12-27 10:37:12 +00:00
|
|
|
"""
|
|
|
|
OpenWebRX uses the [direwolf](https://github.com/wb2osz/direwolf) software modem to decode Packet Radio and
|
|
|
|
report data back to APRS-IS. Direwolf is available from the package manager on many distributions, or you can
|
|
|
|
compile it from source.
|
|
|
|
"""
|
2019-06-22 16:20:01 +00:00
|
|
|
return self.command_is_runnable("direwolf --help")
|
|
|
|
|
2019-06-07 13:44:11 +00:00
|
|
|
def has_airspy_rx(self):
|
2019-07-05 20:31:46 +00:00
|
|
|
"""
|
|
|
|
In order to use an Airspy Receiver, you need to install the airspy_rx receiver software.
|
|
|
|
"""
|
2019-11-15 18:36:07 +00:00
|
|
|
return self.command_is_runnable("airspy_rx --help")
|
2019-07-08 18:16:29 +00:00
|
|
|
|
|
|
|
def has_wsjtx(self):
|
|
|
|
"""
|
|
|
|
To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the
|
|
|
|
[WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions
|
|
|
|
on how to build from source.
|
|
|
|
"""
|
2019-07-21 17:40:28 +00:00
|
|
|
return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True)
|
2019-12-19 20:37:19 +00:00
|
|
|
|
|
|
|
def has_alsa(self):
|
|
|
|
"""
|
|
|
|
Some SDR receivers are identifying themselves as a soundcard. In order to read their data, OpenWebRX relies
|
|
|
|
on the Alsa library. It is available as a package for most Linux distributions.
|
|
|
|
"""
|
|
|
|
return self.command_is_runnable("arecord --help")
|