Merge branch 'develop' into radioberry
This commit is contained in:
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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 = {
|
||||
|
@ -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
21
owrx/details.py
Normal 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
|
@ -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")
|
||||
|
@ -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),
|
||||
|
@ -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()])
|
||||
|
@ -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)
|
@ -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
|
||||
|
@ -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"
|
@ -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
|
||||
),
|
||||
|
Reference in New Issue
Block a user