Merge branch 'develop' into radioberry

This commit is contained in:
Jakob Ketterl
2020-06-01 18:27:44 +02:00
66 changed files with 1247 additions and 710 deletions

View File

@ -1,20 +1,19 @@
import logging
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
from http.server import HTTPServer
from owrx.http import RequestHandler
from owrx.config import Config
from owrx.feature import FeatureDetector
from owrx.sdr import SdrService
from socketserver import ThreadingMixIn
from owrx.sdrhu import SdrHuUpdater
from owrx.service import Services
from owrx.websocket import WebSocketConnection
from owrx.pskreporter import PskReporter
from owrx.version import openwebrx_version
import logging
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
pass
@ -59,10 +58,6 @@ Support and info: https://groups.io/g/openwebrx
# Get error messages about unknown / unavailable features as soon as possible
SdrService.loadProps()
if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
updater = SdrHuUpdater()
updater.start()
Services.start()
try:

View File

@ -25,6 +25,12 @@ class QueueJob(object):
def run(self):
self.decoder.decode(self)
def unlink(self):
try:
os.unlink(self.file)
except FileNotFoundError:
pass
class QueueWorker(threading.Thread):
def __init__(self, queue):
@ -40,6 +46,9 @@ class QueueWorker(threading.Thread):
except Exception:
logger.exception("failed to decode job")
self.queue.onError()
finally:
job.unlink()
self.queue.task_done()
@ -159,11 +168,12 @@ class AudioWriter(object):
self.switchingLock.release()
file.close()
job = QueueJob(self, filename, self.dsp.get_operating_freq())
try:
DecoderQueue.getSharedInstance().put(QueueJob(self, filename, self.dsp.get_operating_freq()))
DecoderQueue.getSharedInstance().put(job)
except Full:
logger.warning("decoding queue overflow; dropping one file")
os.unlink(filename)
job.unlink()
self._scheduleNextSwitch()
def decode(self, job: QueueJob):
@ -183,7 +193,6 @@ class AudioWriter(object):
except subprocess.TimeoutExpired:
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
decoder.kill()
os.unlink(job.file)
def start(self):
(self.wavefilename, self.wavefile) = self.getWaveFile()

View File

@ -26,6 +26,11 @@ class ConfigMigrator(ABC):
def migrate(self, config):
pass
def renameKey(self, config, old, new):
if old in config and not new in config:
config[new] = config[old]
del config[old]
class ConfigMigratorVersion1(ConfigMigrator):
def migrate(self, config):
@ -37,6 +42,9 @@ class ConfigMigratorVersion1(ConfigMigrator):
levels = config["waterfall_auto_level_margin"]
config["waterfall_auto_level_margin"] = {"min": levels[0], "max": levels[1]}
self.renameKey(config, "wsjt_queue_workers", "decoding_queue_workers")
self.renameKey(config, "wsjt_queue_length", "decoding_queue_length")
config["version"] = 2
return config

View File

@ -1,4 +1,5 @@
from owrx.config import Config
from owrx.details import ReceiverDetails
from owrx.dsp import DspManager
from owrx.cpu import CpuUsageThread
from owrx.sdr import SdrService
@ -9,7 +10,6 @@ from owrx.version import openwebrx_version
from owrx.bands import Bandplan
from owrx.bookmarks import Bookmarks
from owrx.map import Map
from owrx.locator import Locator
from owrx.property import PropertyStack
from owrx.modes import Modes, DigitalMode
from multiprocessing import Queue
@ -68,18 +68,10 @@ class OpenWebRxClient(Client, metaclass=ABCMeta):
def __init__(self, conn):
super().__init__(conn)
receiver_details = Config.get().filter(
"receiver_name",
"receiver_location",
"receiver_asl",
"receiver_gps",
"photo_title",
"photo_desc",
)
receiver_details = ReceiverDetails()
def send_receiver_info(*args):
receiver_info = receiver_details.__dict__()
receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
self.write_receiver_details(receiver_info)
# TODO unsubscribe

View File

@ -1,5 +1,6 @@
from . import Controller
from owrx.feature import FeatureDetector
from owrx.details import ReceiverDetails
import json
@ -7,3 +8,8 @@ class ApiController(Controller):
def indexAction(self):
data = json.dumps(FeatureDetector().feature_report())
self.send_response(data, content_type="application/json")
def receiverDetails(self):
receiver_details = ReceiverDetails()
data = json.dumps(receiver_details.__dict__())
self.send_response(data, content_type="application/json")

View File

@ -87,6 +87,13 @@ class CompiledAssetsController(Controller):
"lib/Header.js",
"map.js",
],
"settings.js": [
"lib/jquery-3.2.1.min.js",
"lib/Header.js",
"lib/settings/Input.js",
"lib/settings/SdrDevice.js",
"settings.js",
]
}
def indexAction(self):

View File

@ -13,6 +13,8 @@ from owrx.form import (
ServicesCheckboxInput,
Js8ProfileCheckboxInput,
)
from urllib.parse import quote
import json
import logging
logger = logging.getLogger(__name__)
@ -55,18 +57,24 @@ class SdrSettingsController(AdminController):
return variables
def render_devices(self):
def render_devicde(device_id, config):
return """
<div class="card device bg-dark text-white">
<div class="card-header">
{device_name}
</div>
<div class="card-body">
device settings go here
</div>
return "".join(self.render_device(key, value) for key, value in Config.get()["sdrs"].items())
def render_device(self, device_id, config):
return """
<div class="card device bg-dark text-white">
<div class="card-header">
{device_name}
</div>
""".format(device_name=config["name"])
return "".join(render_devicde(key, value) for key, value in Config.get()["sdrs"].items())
<div class="card-body">
{form}
</div>
</div>
""".format(device_name=config["name"], form=self.render_form(device_id, config))
def render_form(self, device_id, config):
return """
<form class="sdrdevice" data-config="{formdata}"></form>
""".format(device_id=device_id, formdata=quote(json.dumps(config)))
def indexAction(self):
self.serve_template("sdrsettings.html", **self.template_variables())
@ -236,18 +244,6 @@ class GeneralSettingsController(AdminController):
infotext="This callsign will be used to send spots to pskreporter.info",
),
),
Section(
"sdr.hu",
TextInput(
"sdrhu_key",
"sdr.hu key",
infotext='Please obtain your personal key on <a href="https://sdr.hu" target="_blank">sdr.hu</a>',
),
CheckboxInput(
"sdrhu_public_listing", "List on sdr.hu", "List my receiver on sdr.hu"
),
TextInput("server_hostname", "Hostname"),
),
]
def render_sections(self):

View File

@ -9,26 +9,6 @@ import pkg_resources
class StatusController(Controller):
def indexAction(self):
pm = Config.get()
# convert to old format
gps = (pm["receiver_gps"]["lat"], pm["receiver_gps"]["lon"])
avatar_path = pkg_resources.resource_filename("htdocs", "gfx/openwebrx-avatar.png")
# TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna
vars = {
"status": "active",
"name": pm["receiver_name"],
"op_email": pm["receiver_admin"],
"users": ClientRegistry.getSharedInstance().clientCount(),
"users_max": pm["max_clients"],
"gps": gps,
"asl": pm["receiver_asl"],
"loc": pm["receiver_location"],
"sw_version": openwebrx_version,
"avatar_ctime": os.path.getctime(avatar_path),
}
self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()]))
def getProfileStats(self, profile):
return {
"name": profile["name"],
@ -45,7 +25,7 @@ class StatusController(Controller):
}
return stats
def jsonAction(self):
def indexAction(self):
pm = Config.get()
status = {

View File

@ -23,7 +23,7 @@ class WebpageController(TemplateController):
settingslink = ""
pm = Config.get()
if "webadmin_enabled" in pm and pm["webadmin_enabled"]:
settingslink = """<a class="button" href="settings" target="openwebrx-settings"><img src="static/gfx/openwebrx-panel-settings.png" /><br/>Settings</a>"""
settingslink = """<a class="button" href="settings" target="openwebrx-settings"><img src="static/gfx/openwebrx-panel-settings.png" alt="Settings"/><br/>Settings</a>"""
header = self.render_template("include/header.include.html", settingslink=settingslink)
return {"header": header}

21
owrx/details.py Normal file
View File

@ -0,0 +1,21 @@
from owrx.config import Config
from owrx.locator import Locator
from owrx.property import PropertyFilter
class ReceiverDetails(PropertyFilter):
def __init__(self):
super().__init__(
Config.get(),
"receiver_name",
"receiver_location",
"receiver_asl",
"receiver_gps",
"photo_title",
"photo_desc",
)
def __dict__(self):
receiver_info = super().__dict__()
receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
return receiver_info

View File

@ -24,12 +24,12 @@ class FeatureDetector(object):
"rtl_sdr": ["rtl_connector"],
"rtl_sdr_soapy": ["soapy_connector", "soapy_rtl_sdr"],
"sdrplay": ["soapy_connector", "soapy_sdrplay"],
"hackrf": ["hackrf_transfer"],
"hackrf": ["soapy_connector", "soapy_hackrf"],
"perseussdr": ["perseustest"],
"airspy": ["soapy_connector", "soapy_airspy"],
"airspyhf": ["soapy_connector", "soapy_airspyhf"],
"lime_sdr": ["soapy_connector", "soapy_lime_sdr"],
"fifi_sdr": ["alsa"],
"fifi_sdr": ["alsa", "rockprog"],
"pluto_sdr": ["soapy_connector", "soapy_pluto_sdr"],
"soapy_remote": ["soapy_connector", "soapy_remote"],
"uhd": ["soapy_connector", "soapy_uhd"],
@ -128,26 +128,6 @@ class FeatureDetector(object):
"""
return self.command_is_runnable("nc --help")
def has_hackrf_transfer(self):
"""
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
```
"""
# TODO i don't have a hackrf, so somebody doublecheck this.
# TODO also check if it has the stdout feature
return self.command_is_runnable("hackrf_transfer --help")
def has_perseustest(self):
"""
To use a Microtelecom Perseus HF receiver, compile and
@ -273,7 +253,7 @@ class FeatureDetector(object):
"""
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).
You can get it [here](https://github.com/SDRplay/SoapySDRPlay).
"""
return self._has_soapy_driver("sdrplay")
@ -342,6 +322,14 @@ class FeatureDetector(object):
"""
return self._has_soapy_driver("radioberry")
def has_soapy_hackrf(self):
"""
The SoapyHackRF allows HackRF to be used with SoapySDR.
You can get it [here](https://github.com/pothosware/SoapyHackRF/wiki).
"""
return self._has_soapy_driver("hackrf")
def has_dsd(self):
"""
The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version
@ -396,3 +384,11 @@ class FeatureDetector(object):
on the Alsa library. It is available as a package for most Linux distributions.
"""
return self.command_is_runnable("arecord --help")
def has_rockprog(self):
"""
The "rockprog" executable is required to send commands to your FiFiSDR. It needs to be installed separately.
You can find instructions and downloads [here](https://o28.sischa.net/fifisdr/trac/wiki/De%3Arockprog).
"""
return self.command_is_runnable("rockprog")

View File

@ -89,18 +89,16 @@ class Router(object):
def __init__(self):
self.routes = [
StaticRoute("/", IndexController),
StaticRoute("/status", StatusController),
StaticRoute("/status.json", StatusController, options={"action": "jsonAction"}),
StaticRoute("/status.json", StatusController),
RegexRoute("/static/(.+)", OwrxAssetsController),
RegexRoute("/compiled/(.+)", CompiledAssetsController),
RegexRoute("/aprs-symbols/(.+)", AprsSymbolsController),
StaticRoute("/ws/", WebSocketController),
RegexRoute("(/favicon.ico)", OwrxAssetsController),
# backwards compatibility for the sdr.hu portal
RegexRoute("(/gfx/openwebrx-avatar.png)", OwrxAssetsController),
StaticRoute("/map", MapController),
StaticRoute("/features", FeatureController),
StaticRoute("/api/features", ApiController),
StaticRoute("/api/receiverdetails", ApiController, options={"action": "receiverDetails"}),
StaticRoute("/metrics", MetricsController),
StaticRoute("/settings", SettingsController),
StaticRoute("/generalsettings", GeneralSettingsController),

View File

@ -40,6 +40,10 @@ class PropertyManager(ABC):
def __dict__(self):
pass
@abstractmethod
def __delitem__(self, key):
pass
@abstractmethod
def keys(self):
pass
@ -98,6 +102,9 @@ class PropertyLayer(PropertyManager):
def __dict__(self):
return {k: v for k, v in self.properties.items()}
def __delitem__(self, key):
return self.properties.__delitem__(key)
def keys(self):
return self.properties.keys()
@ -132,6 +139,11 @@ class PropertyFilter(PropertyManager):
def __dict__(self):
return {k: v for k, v in self.pm.__dict__().items() if k in self.props}
def __delitem__(self, key):
if key not in self.props:
raise KeyError(key)
return self.pm.__delitem__(key)
def keys(self):
return [k for k in self.pm.keys() if k in self.props]
@ -226,5 +238,9 @@ class PropertyStack(PropertyManager):
def __dict__(self):
return {k: self.__getitem__(k) for k in self.keys()}
def __delitem__(self, key):
for layer in self.layers:
layer["props"].__delitem__(key)
def keys(self):
return set([key for l in self.layers for key in l["props"].keys()])

View File

@ -1,43 +0,0 @@
import threading
import time
from owrx.config import Config
from urllib import request, parse
import logging
logger = logging.getLogger(__name__)
class SdrHuUpdater(threading.Thread):
def __init__(self):
self.doRun = True
super().__init__(daemon=True)
def update(self):
pm = Config.get().filter("server_hostname", "web_port", "sdrhu_key")
data = parse.urlencode({
"url": "http://{server_hostname}:{web_port}".format(**pm.__dict__()),
"apikey": pm["sdrhu_key"]
}).encode()
res = request.urlopen("https://sdr.hu/update", data=data)
if res.getcode() < 200 or res.getcode() >= 300:
logger.warning('sdr.hu update failed with error code %i', res.getcode())
return 2
returned = res.read().decode("utf-8")
if "UPDATE:" not in returned:
logger.warning("Update failed, your receiver cannot be listed on sdr.hu!")
return 2
value = returned.split("UPDATE:")[1].split("\n", 1)[0]
if value.startswith("SUCCESS"):
logger.info("Update succeeded!")
else:
logger.warning("Update failed, your receiver cannot be listed on sdr.hu! Reason: %s", value)
return 20
def run(self):
while self.doRun:
retrytime_mins = self.update()
time.sleep(60 * retrytime_mins)

View File

@ -265,6 +265,7 @@ class ServiceHandler(object):
d.set_secondary_demodulator(mode)
d.set_audio_compression("none")
d.set_samp_rate(source.getProps()["samp_rate"])
d.set_temporary_directory(Config.get()['temporary_directory'])
d.set_service()
d.start()
return d

View File

@ -1,23 +1,11 @@
from .direct import DirectSource
from owrx.command import Option
import time
from .soapy import SoapyConnectorSource
class HackrfSource(DirectSource):
def getCommandMapper(self):
return super().getCommandMapper().setBase("hackrf_transfer").setMappings(
{
"samp_rate": Option("-s"),
"tuner_freq": Option("-f"),
"rf_gain": Option("-g"),
"lna_gain": Option("-l"),
"rf_amp": Option("-a"),
"ppm": Option("-C"),
}
).setStatic("-r-")
class HackrfSource(SoapyConnectorSource):
def getSoapySettingsMappings(self):
mappings = super().getSoapySettingsMappings()
mappings.update({"bias_tee": "bias_tx"})
return mappings
def getFormatConversion(self):
return ["csdr convert_s8_f"]
def sleepOnRestart(self):
time.sleep(1)
def getDriver(self):
return "hackrf"

View File

@ -1,10 +1,4 @@
from .direct import DirectSource
from . import SdrSource
import subprocess
import threading
import os
import socket
import time
import logging
@ -29,7 +23,7 @@ class Resampler(DirectSource):
def getCommand(self):
return [
"nc -v 127.0.0.1 {nc_port}".format(nc_port=self.sdr.getPort()),
"csdr shift_addition_cc {shift}".format(shift=self.shift),
"csdr shift_addfast_cc {shift}".format(shift=self.shift),
"csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING".format(
decimation=self.decimation, ddc_transition_bw=self.transition_bw
),