Merge branch 'develop' into pycsdr

This commit is contained in:
Jakob Ketterl
2021-01-23 17:17:44 +01:00
50 changed files with 524 additions and 368 deletions

View File

@ -1,8 +1,3 @@
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
@ -14,12 +9,26 @@ from owrx.websocket import WebSocketConnection
from owrx.reporting import ReportingEngine
from owrx.version import openwebrx_version
from owrx.audio import DecoderQueue
import signal
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
class SignalException(Exception):
pass
def handleSignal(sig, frame):
raise SignalException("Received Signal {sig}".format(sig=sig))
def main():
print(
"""
@ -36,13 +45,14 @@ Support and info: https://groups.io/g/openwebrx
logger.info("OpenWebRX version {0} starting up...".format(openwebrx_version))
for sig in [signal.SIGINT, signal.SIGTERM]:
signal.signal(sig, handleSignal)
pm = Config.get()
configErrors = Config.validateConfig()
if configErrors:
logger.error(
"your configuration contains errors. please address the following errors:"
)
logger.error("your configuration contains errors. please address the following errors:")
for e in configErrors:
logger.error(e)
return
@ -65,7 +75,7 @@ Support and info: https://groups.io/g/openwebrx
try:
server = ThreadedHttpServer(("0.0.0.0", pm["web_port"]), RequestHandler)
server.serve_forever()
except KeyboardInterrupt:
except SignalException:
WebSocketConnection.closeAll()
Services.stop()
ReportingEngine.stopAll()

View File

@ -46,8 +46,6 @@ class QueueWorker(threading.Thread):
job = self.queue.get()
if job is PoisonPill:
self.doRun = False
# put the poison pill back on the queue for the next worker
self.queue.put(PoisonPill)
else:
try:
job.run()
@ -69,7 +67,9 @@ class DecoderQueue(Queue):
with DecoderQueue.creationLock:
if DecoderQueue.sharedInstance is None:
pm = Config.get()
DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"])
DecoderQueue.sharedInstance = DecoderQueue(
maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"]
)
return DecoderQueue.sharedInstance
@staticmethod
@ -100,10 +100,14 @@ class DecoderQueue(Queue):
while not self.empty():
job = self.get()
job.unlink()
self.task_done()
except Empty:
pass
# put() PoisonPill to tell workers to shut down
self.put(PoisonPill)
# put() a PoisonPill for all active workers to shut them down
for w in self.workers:
if w.is_alive():
self.put(PoisonPill)
self.join()
def put(self, item, **kwars):
self.inCounter.inc()

View File

@ -17,7 +17,7 @@ class Band(object):
for (mode, freqs) in dict["frequencies"].items():
if mode not in availableModes:
logger.info(
"Modulation \"{mode}\" is not available, bandplan bookmark will not be displayed".format(
'Modulation "{mode}" is not available, bandplan bookmark will not be displayed'.format(
mode=mode
)
)

View File

@ -112,9 +112,7 @@ class Config:
@staticmethod
def validateConfig():
pm = Config.get()
errors = [
Config.checkTempDirectory(pm)
]
errors = [Config.checkTempDirectory(pm)]
return [e for e in errors if e is not None]

View File

@ -135,7 +135,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.dsp = None
self.sdr = None
self.sdrConfigSubs = []
self.configSubs = []
self.connectionProperties = {}
try:
@ -145,9 +145,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.close()
raise
globalConfig = Config.get().filter(*OpenWebRxReceiverClient.global_config_keys)
self.globalConfigSub = globalConfig.wire(self.write_config)
self.write_config(globalConfig.__dict__())
self.setupGlobalConfig()
self.stack = self.setupStack()
self.setSdr()
@ -162,8 +161,51 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
CpuUsageThread.getSharedInstance().add_client(self)
def __del__(self):
if hasattr(self, "globalConfigSub"):
self.globalConfigSub.cancel()
if hasattr(self, "configSubs"):
while self.configSubs:
self.configSubs.pop().cancel()
def setupStack(self):
stack = PropertyStack()
# stack layer 0 reserved for sdr properties
# stack.addLayer(0, self.sdr.getProps())
stack.addLayer(1, Config.get())
configProps = stack.filter(*OpenWebRxReceiverClient.sdr_config_keys)
def sendConfig(changes=None):
if changes is None:
config = configProps.__dict__()
else:
config = changes
if (
(changes is None or "start_freq" in changes or "center_freq" in changes)
and "start_freq" in configProps
and "center_freq" in configProps
):
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
if (changes is None or "profile_id" in changes) and self.sdr is not None:
config["sdr_id"] = self.sdr.getId()
self.write_config(config)
def sendBookmarks(changes=None):
cf = configProps["center_freq"]
srh = configProps["samp_rate"] / 2
frequencyRange = (cf - srh, cf + srh)
self.write_dial_frequencies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange))
bookmarks = [b.__dict__() for b in Bookmarks.getSharedInstance().getBookmarks(frequencyRange)]
self.write_bookmarks(bookmarks)
self.configSubs.append(configProps.wire(sendConfig))
self.configSubs.append(stack.filter("center_freq", "samp_rate").wire(sendBookmarks))
# send initial config
sendConfig()
return stack
def setupGlobalConfig(self):
globalConfig = Config.get().filter(*OpenWebRxReceiverClient.global_config_keys)
self.configSubs.append(globalConfig.wire(self.write_config))
self.write_config(globalConfig.__dict__())
def onStateChange(self, state):
if state == SdrSource.STATE_RUNNING:
@ -242,9 +284,6 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.stopDsp()
while self.sdrConfigSubs:
self.sdrConfigSubs.pop().cancel()
if self.sdr is not None:
self.sdr.removeClient(self)
@ -259,37 +298,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def handleSdrAvailable(self):
self.getDsp().setProperties(self.connectionProperties)
self.stack.replaceLayer(0, self.sdr.getProps())
stack = PropertyStack()
stack.addLayer(0, self.sdr.getProps())
stack.addLayer(1, Config.get())
configProps = stack.filter(*OpenWebRxReceiverClient.sdr_config_keys)
def sendConfig(changes=None):
if changes is None:
config = configProps.__dict__()
else:
config = changes
if changes is None or "start_freq" in changes or "center_freq" in changes:
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
if changes is None or "profile_id" in changes:
config["sdr_id"] = self.sdr.getId()
self.write_config(config)
def sendBookmarks(changes=None):
cf = configProps["center_freq"]
srh = configProps["samp_rate"] / 2
frequencyRange = (cf - srh, cf + srh)
self.write_dial_frequencies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange))
bookmarks = [b.__dict__() for b in Bookmarks.getSharedInstance().getBookmarks(frequencyRange)]
self.write_bookmarks(bookmarks)
self.sdrConfigSubs.append(configProps.wire(sendConfig))
self.sdrConfigSubs.append(stack.filter("center_freq", "samp_rate").wire(sendBookmarks))
# send initial config
sendConfig()
sendBookmarks()
self.__sendProfiles()
self.sdr.addSpectrumClient(self)
@ -306,8 +316,6 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.stopDsp()
CpuUsageThread.getSharedInstance().remove_client(self)
ClientRegistry.getSharedInstance().removeClient(self)
while self.sdrConfigSubs:
self.sdrConfigSubs.pop().cancel()
super().close()
def stopDsp(self):
@ -325,11 +333,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
keys = config["configurable_keys"]
if not keys:
return
# only the keys in the protected property manager can be overridden from the web
stack = PropertyStack()
stack.addLayer(0, self.sdr.getProps())
stack.addLayer(1, config)
protected = stack.filter(*keys)
protected = self.stack.filter(*keys)
for key, value in params.items():
try:
protected[key] = value
@ -406,15 +410,20 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.send({"type": "backoff", "reason": reason})
def write_js8_message(self, frame: Js8Frame, freq: int):
self.send({"type": "js8_message", "value": {
"msg": str(frame),
"timestamp": frame.timestamp,
"db": frame.db,
"dt": frame.dt,
"freq": freq + frame.freq,
"thread_type": frame.thread_type,
"mode": frame.mode
}})
self.send(
{
"type": "js8_message",
"value": {
"msg": str(frame),
"timestamp": frame.timestamp,
"db": frame.db,
"dt": frame.dt,
"freq": freq + frame.freq,
"thread_type": frame.thread_type,
"mode": frame.mode,
},
}
)
def write_modes(self, modes):
def to_json(m):
@ -426,10 +435,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
"squelch": m.squelch,
}
if m.bandpass is not None:
res["bandpass"] = {
"low_cut": m.bandpass.low_cut,
"high_cut": m.bandpass.high_cut
}
res["bandpass"] = {"low_cut": m.bandpass.low_cut, "high_cut": m.bandpass.high_cut}
if isinstance(m, DigitalMode):
res["underlying"] = m.underlying
return res
@ -442,12 +448,14 @@ class MapConnection(OpenWebRxClient):
super().__init__(conn)
pm = Config.get()
self.write_config(pm.filter(
"google_maps_api_key",
"receiver_gps",
"map_position_retention_time",
"receiver_name",
).__dict__())
self.write_config(
pm.filter(
"google_maps_api_key",
"receiver_gps",
"map_position_retention_time",
"receiver_name",
).__dict__()
)
Map.getSharedInstance().addClient(self)

View File

@ -7,7 +7,9 @@ class Controller(object):
self.request = request
self.options = options
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None):
def send_response(
self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None
):
self.handler.send_response(code)
if headers is None:
headers = {}
@ -27,7 +29,7 @@ class Controller(object):
def send_redirect(self, location, code=303, cookies=None):
self.handler.send_response(code)
if cookies is not None:
self.handler.send_header("Set-Cookie", cookies.output(header=''))
self.handler.send_header("Set-Cookie", cookies.output(header=""))
self.handler.send_header("Location", location)
self.handler.end_headers()

View File

@ -13,9 +13,9 @@ logger = logging.getLogger(__name__)
class GzipMixin(object):
def send_response(self, content, headers=None, content_type="text/html", *args, **kwargs):
def send_response(self, content, headers=None, content_type="text/html", *args, **kwargs):
if self.zipable(content_type) and "accept-encoding" in self.request.headers:
accepted = [s.strip().lower() for s in self.request.headers['accept-encoding'].split(",")]
accepted = [s.strip().lower() for s in self.request.headers["accept-encoding"].split(",")]
if "gzip" in accepted:
if type(content) == str:
content = content.encode()
@ -26,11 +26,7 @@ class GzipMixin(object):
super().send_response(content, headers=headers, content_type=content_type, *args, **kwargs)
def zipable(self, content_type):
types = [
"application/javascript",
"text/css",
"text/html"
]
types = ["application/javascript", "text/css", "text/html"]
return content_type in types
def gzip(self, content):
@ -41,11 +37,11 @@ class ModificationAwareController(Controller, metaclass=ABCMeta):
@abstractmethod
def getModified(self, file):
pass
def wasModified(self, file):
try:
modified = self.getModified(file).replace(microsecond=0)
if modified is not None and "If-Modified-Since" in self.handler.headers:
client_modified = datetime.strptime(
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
@ -54,7 +50,7 @@ class ModificationAwareController(Controller, metaclass=ABCMeta):
return False
except FileNotFoundError:
pass
return True
@ -143,7 +139,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
"lib/settings/Input.js",
"lib/settings/SdrDevice.js",
"settings.js",
]
],
}
def indexAction(self):

View File

@ -8,15 +8,19 @@ class ReceiverIdController(Controller):
super().__init__(handler, request, options)
self.authHeader = None
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None):
def send_response(
self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None
):
if self.authHeader is not None:
if headers is None:
headers = {}
headers['Authorization'] = self.authHeader
super().send_response(content, code=code, content_type=content_type, last_modified=last_modified, max_age=max_age, headers=headers)
headers["Authorization"] = self.authHeader
super().send_response(
content, code=code, content_type=content_type, last_modified=last_modified, max_age=max_age, headers=headers
)
pass
def handle_request(self):
if "Authorization" in self.request.headers:
self.authHeader = ReceiverId.getResponseHeader(self.request.headers['Authorization'])
self.authHeader = ReceiverId.getResponseHeader(self.request.headers["Authorization"])
super().handle_request()

View File

@ -69,12 +69,16 @@ class SdrSettingsController(AdminController):
{form}
</div>
</div>
""".format(device_name=config["name"], form=self.render_form(device_id, config))
""".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)))
""".format(
device_id=device_id, formdata=quote(json.dumps(config))
)
def indexAction(self):
self.serve_template("sdrsettings.html", **self.template_variables())
@ -119,12 +123,18 @@ class GeneralSettingsController(AdminController):
DropdownInput(
"audio_compression",
"Audio compression",
options=[Option("adpcm", "ADPCM"), Option("none", "None"),],
options=[
Option("adpcm", "ADPCM"),
Option("none", "None"),
],
),
DropdownInput(
"fft_compression",
"Waterfall compression",
options=[Option("adpcm", "ADPCM"), Option("none", "None"),],
options=[
Option("adpcm", "ADPCM"),
Option("none", "None"),
],
),
),
Section(
@ -196,10 +206,7 @@ class GeneralSettingsController(AdminController):
"Js8Call decoding depth",
infotext="A higher decoding depth will allow more results, but will also consume more cpu",
),
Js8ProfileCheckboxInput(
"js8_enabled_profiles",
"Js8Call enabled modes"
),
Js8ProfileCheckboxInput("js8_enabled_profiles", "Js8Call enabled modes"),
),
Section(
"Background decoding",
@ -269,9 +276,7 @@ class GeneralSettingsController(AdminController):
def processFormData(self):
data = parse_qs(self.get_body().decode("utf-8"))
data = {
k: v for i in GeneralSettingsController.sections for k, v in i.parse(data).items()
}
data = {k: v for i in GeneralSettingsController.sections for k, v in i.parse(data).items()}
config = Config.get()
for k, v in data.items():
config[k] = v

View File

@ -22,7 +22,7 @@ class StatusController(ReceiverIdController):
"name": receiver.getName(),
# TODO would be better to have types from the config here
"type": type(receiver).__name__,
"profiles": [self.getProfileStats(p) for p in receiver.getProfiles().values()]
"profiles": [self.getProfileStats(p) for p in receiver.getProfiles().values()],
}
return stats
@ -38,6 +38,6 @@ class StatusController(ReceiverIdController):
},
"max_clients": pm["max_clients"],
"version": openwebrx_version,
"sdrs": [self.getReceiverStats(r) for r in SdrService.getSources().values()]
"sdrs": [self.getReceiverStats(r) for r in SdrService.getSources().values()],
}
self.send_response(json.dumps(status), content_type="application/json")

View File

@ -152,7 +152,14 @@ class FeatureDetector(object):
# prevent X11 programs from opening windows if called from a GUI shell
env.pop("DISPLAY", None)
try:
process = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=tmp_dir, env=env)
process = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=tmp_dir,
env=env,
)
rc = process.wait()
if expected_result is None:
return rc != 32512
@ -214,7 +221,6 @@ class FeatureDetector(object):
"""
return self.command_is_runnable("perseustest -h")
def has_digiham(self):
"""
To use digital voice modes, the digiham package is required. You can find the package and installation
@ -547,4 +553,4 @@ class FeatureDetector(object):
You can find more information [here](https://github.com/jketterl/eb200_connector).
"""
return self._check_connector("eb200_connector")
return self._check_connector("eb200_connector")

View File

@ -10,9 +10,7 @@ class Input(ABC):
self.infotext = infotext
def bootstrap_decorate(self, input):
infotext = (
"<small>{text}</small>".format(text=self.infotext) if self.infotext else ""
)
infotext = "<small>{text}</small>".format(text=self.infotext) if self.infotext else ""
return """
<div class="form-group row">
<label class="col-form-label col-form-label-sm col-3" for="{id}">{label}</label>
@ -108,9 +106,7 @@ class LocationInput(Input):
)
def parse(self, data):
return {
self.id: {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]}
}
return {self.id: {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]}}
class TextAreaInput(Input):
@ -195,9 +191,7 @@ class MultiCheckboxInput(Input):
class ServicesCheckboxInput(MultiCheckboxInput):
def __init__(self, id, label, infotext=None):
services = [
Option(s.modulation, s.name) for s in Modes.getAvailableServices()
]
services = [Option(s.modulation, s.name) for s in Modes.getAvailableServices()]
super().__init__(id, label, services, infotext)

View File

@ -1,14 +1,6 @@
from owrx.controllers.status import StatusController
from owrx.controllers.template import (
IndexController,
MapController,
FeatureController
)
from owrx.controllers.assets import (
OwrxAssetsController,
AprsSymbolsController,
CompiledAssetsController
)
from owrx.controllers.template import IndexController, MapController, FeatureController
from owrx.controllers.assets import OwrxAssetsController, AprsSymbolsController, CompiledAssetsController
from owrx.controllers.websocket import WebSocketController
from owrx.controllers.api import ApiController
from owrx.controllers.metrics import MetricsController
@ -109,7 +101,9 @@ class Router(object):
StaticRoute("/metrics", MetricsController),
StaticRoute("/settings", SettingsController),
StaticRoute("/generalsettings", GeneralSettingsController),
StaticRoute("/generalsettings", GeneralSettingsController, method="POST", options={"action": "processFormData"}),
StaticRoute(
"/generalsettings", GeneralSettingsController, method="POST", options={"action": "processFormData"}
),
StaticRoute("/sdrsettings", SdrSettingsController),
StaticRoute("/login", SessionController, options={"action": "loginAction"}),
StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}),

View File

@ -102,15 +102,17 @@ class Js8Parser(Parser):
Map.getSharedInstance().updateLocation(
frame.callsign, LocatorLocation(frame.grid), "JS8", self.band
)
ReportingEngine.getSharedInstance().spot({
"callsign": frame.callsign,
"mode": "JS8",
"locator": frame.grid,
"freq": self.dial_freq + frame.freq,
"db": frame.db,
"timestamp": frame.timestamp,
"msg": str(frame)
})
ReportingEngine.getSharedInstance().spot(
{
"callsign": frame.callsign,
"mode": "JS8",
"locator": frame.grid,
"freq": self.dial_freq + frame.freq,
"db": frame.db,
"timestamp": frame.timestamp,
"msg": str(frame),
}
)
except Exception:
logger.exception("error while parsing js8 message")

View File

@ -13,6 +13,7 @@ TFESC = 0xDD
FEET_PER_METER = 3.28084
class DirewolfConfig(object):
def getConfig(self, port, is_service):
pm = Config.get()
@ -40,7 +41,7 @@ IGLOGIN {callsign} {password}
)
if pm["aprs_igate_beacon"]:
#Format beacon lat/lon
# Format beacon lat/lon
lat = pm["receiver_gps"]["lat"]
lon = pm["receiver_gps"]["lon"]
direction_ns = "N" if lat > 0 else "S"
@ -50,13 +51,13 @@ IGLOGIN {callsign} {password}
lat = "{0:02d}^{1:05.2f}{2}".format(int(lat), (lat - int(lat)) * 60, direction_ns)
lon = "{0:03d}^{1:05.2f}{2}".format(int(lon), (lon - int(lon)) * 60, direction_we)
#Format beacon details
symbol = str(pm["aprs_igate_symbol"]) if "aprs_igate_symbol" in pm else "R&"
gain = "GAIN=" + str(pm["aprs_igate_gain"]) if "aprs_igate_gain" in pm else ""
adir = "DIR=" + str(pm["aprs_igate_dir"]) if "aprs_igate_dir" in pm else ""
comment = str(pm["aprs_igate_comment"]) if "aprs_igate_comment" in pm else "\"OpenWebRX APRS gateway\""
# Format beacon details
symbol = str(pm["aprs_igate_symbol"]) if "aprs_igate_symbol" in pm else "R&"
gain = "GAIN=" + str(pm["aprs_igate_gain"]) if "aprs_igate_gain" in pm else ""
adir = "DIR=" + str(pm["aprs_igate_dir"]) if "aprs_igate_dir" in pm else ""
comment = str(pm["aprs_igate_comment"]) if "aprs_igate_comment" in pm else '"OpenWebRX APRS gateway"'
#Convert height from meters to feet if specified
# Convert height from meters to feet if specified
height = ""
if "aprs_igate_height" in pm:
try:
@ -64,18 +65,21 @@ IGLOGIN {callsign} {password}
height_ft = round(height_m * FEET_PER_METER)
height = "HEIGHT=" + str(height_ft)
except:
logger.error("Cannot parse 'aprs_igate_height', expected float: " + str(pm["aprs_igate_height"]))
logger.error(
"Cannot parse 'aprs_igate_height', expected float: " + str(pm["aprs_igate_height"])
)
if((len(comment) > 0) and ((comment[0] != '"') or (comment[len(comment)-1] != '"'))):
comment = "\"" + comment + "\""
elif(len(comment) == 0):
comment = "\"\""
if (len(comment) > 0) and ((comment[0] != '"') or (comment[len(comment) - 1] != '"')):
comment = '"' + comment + '"'
elif len(comment) == 0:
comment = '""'
pbeacon = "PBEACON sendto=IG delay=0:30 every=60:00 symbol={symbol} lat={lat} long={lon} {height} {gain} {adir} comment={comment}".format(
symbol=symbol, lat=lat, lon=lon, height=height, gain=gain, adir=adir, comment=comment )
symbol=symbol, lat=lat, lon=lon, height=height, gain=gain, adir=adir, comment=comment
)
logger.info("APRS PBEACON String: " + pbeacon)
config += "\n" + pbeacon + "\n"
return config
@ -98,7 +102,7 @@ class KissClient(object):
pass
def __init__(self, port):
delay = .5
delay = 0.5
retries = 0
while True:
try:

View File

@ -60,24 +60,47 @@ class Modes(object):
AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)),
AnalogMode("cw", "CW", bandpass=Bandpass(700, 900)),
AnalogMode("dmr", "DMR", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False),
AnalogMode("dstar", "D-Star", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False),
AnalogMode(
"dstar", "D-Star", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False
),
AnalogMode("nxdn", "NXDN", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False),
AnalogMode("ysf", "YSF", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False),
AnalogMode("m17", "M17", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_m17"], squelch=False),
AnalogMode("freedv", "FreeDV", bandpass=Bandpass(300, 3000), requirements=["digital_voice_freedv"], squelch=False),
AnalogMode(
"freedv", "FreeDV", bandpass=Bandpass(300, 3000), requirements=["digital_voice_freedv"], squelch=False
),
AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False),
DigitalMode("bpsk31", "BPSK31", underlying=["usb"]),
DigitalMode("bpsk63", "BPSK63", underlying=["usb"]),
DigitalMode("ft8", "FT8", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode("ft4", "FT4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode("jt65", "JT65", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode("jt9", "JT9", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode(
"ft8", "FT8", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True
),
DigitalMode(
"ft4", "FT4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True
),
DigitalMode(
"jt65", "JT65", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True
),
DigitalMode(
"jt9", "JT9", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True
),
DigitalMode(
"wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True
),
DigitalMode("fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x-2-3"], service=True),
DigitalMode("fst4w", "FST4W", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"], service=True),
DigitalMode("js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True),
DigitalMode(
"fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x-2-3"], service=True
),
DigitalMode(
"fst4w",
"FST4W",
underlying=["usb"],
bandpass=Bandpass(1350, 1650),
requirements=["wsjt-x-2-3"],
service=True,
),
DigitalMode(
"js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True
),
DigitalMode(
"packet",
"Packet",

View File

@ -1,10 +1,17 @@
from owrx.parser import Parser
import logging
logger = logging.getLogger(__name__)
class PocsagParser(Parser):
def parse(self, raw):
fields = raw.decode("ascii", "replace").rstrip("\n").split(";")
meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""}
if "address" in meta:
meta["address"] = int(meta["address"])
self.handler.write_pocsag_data(meta)
try:
fields = raw.decode("ascii", "replace").rstrip("\n").split(";")
meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""}
if "address" in meta:
meta["address"] = int(meta["address"])
self.handler.write_pocsag_data(meta)
except Exception:
logger.exception("Exception while parsing Pocsag message")

View File

@ -150,10 +150,10 @@ class Uploader(object):
# id
[0x00, 0x03]
# length
+ list(length.to_bytes(2, 'big'))
+ list(length.to_bytes(2, "big"))
+ Uploader.receieverDelimiter
# number of fields
+ list(num_fields.to_bytes(2, 'big'))
+ list(num_fields.to_bytes(2, "big"))
# padding
+ [0x00, 0x00]
# receiverCallsign
@ -163,9 +163,7 @@ class Uploader(object):
# decodingSoftware
+ [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# antennaInformation
+ (
[0x80, 0x09, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] if with_antenna else []
)
+ ([0x80, 0x09, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] if with_antenna else [])
# padding
+ [0x00, 0x00]
)

View File

@ -77,6 +77,7 @@ class ReceiverId(object):
return Key(keyString)
except KeyException as e:
logger.error(e)
config = Config.get()
if "receiver_keys" not in config or config["receiver_keys"] is None:
return None

View File

@ -40,10 +40,12 @@ class ReportingEngine(object):
if "pskreporter_enabled" in config and config["pskreporter_enabled"]:
# inline import due to circular dependencies
from owrx.pskreporter import PskReporter
self.reporters += [PskReporter()]
if "wsprnet_enabled" in config and config["wsprnet_enabled"]:
# inline import due to circular dependencies
from owrx.wsprnet import WsprnetReporter
self.reporters += [WsprnetReporter()]
def stop(self):

View File

@ -65,12 +65,12 @@ class ServiceHandler(SdrSourceEventClient):
self.services = []
self.source = source
self.startupTimer = None
self.scheduler = None
self.source.addClient(self)
props = self.source.getProps()
props.filter("center_freq", "samp_rate").wire(self.onFrequencyChange)
if self.source.isAvailable():
self.scheduleServiceStartup()
self.scheduler = None
if "schedule" in props or "scheduler" in props:
self.scheduler = ServiceScheduler(self.source)
@ -137,9 +137,7 @@ class ServiceHandler(SdrSourceEventClient):
dials = [
dial
for dial in Bandplan.getSharedInstance().collectDialFrequencies(
frequency_range
)
for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range)
if self.isSupported(dial["mode"])
]
@ -150,16 +148,12 @@ class ServiceHandler(SdrSourceEventClient):
groups = self.optimizeResampling(dials, sr)
if groups is None:
for dial in dials:
self.services.append(
self.setupService(dial["mode"], dial["frequency"], self.source)
)
self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source))
else:
for group in groups:
cf = self.get_center_frequency(group)
bw = self.get_bandwidth(group)
logger.debug(
"group center frequency: {0}, bandwidth: {1}".format(cf, bw)
)
logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw))
resampler_props = PropertyLayer()
resampler_props["center_freq"] = cf
resampler_props["samp_rate"] = bw
@ -167,11 +161,7 @@ class ServiceHandler(SdrSourceEventClient):
resampler.start()
for dial in group:
self.services.append(
self.setupService(
dial["mode"], dial["frequency"], resampler
)
)
self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler))
# resampler goes in after the services since it must not be shutdown as long as the services are still running
self.services.append(resampler)
@ -238,9 +228,7 @@ class ServiceHandler(SdrSourceEventClient):
results = sorted(usages, key=lambda f: f["total_bandwidth"])
for r in results:
logger.debug(
"splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"])
)
logger.debug("splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"]))
best = results[0]
if best["num_splits"] is None:
@ -267,7 +255,7 @@ class ServiceHandler(SdrSourceEventClient):
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_temporary_directory(Config.get()["temporary_directory"])
d.set_service()
d.start()
return d

View File

@ -68,6 +68,7 @@ class DatetimeScheduleEntry(ScheduleEntry):
def getNextActivation(self):
return self.startTime
class Schedule(ABC):
@staticmethod
def parse(props):
@ -140,7 +141,7 @@ class DaylightSchedule(TimerangeSchedule):
degtorad = math.pi / 180
radtodeg = 180 / math.pi
#Number of days since 01/01
# Number of days since 01/01
days = date.timetuple().tm_yday
# Longitudinal correction

View File

@ -86,17 +86,17 @@ class SdrSource(ABC):
for id, p in self.props["profiles"].items():
props.replaceLayer(0, self._getProfilePropertyLayer(p))
if "center_freq" not in props:
logger.warning("Profile \"%s\" does not specify a center_freq", id)
logger.warning('Profile "%s" does not specify a center_freq', id)
continue
if "samp_rate" not in props:
logger.warning("Profile \"%s\" does not specify a samp_rate", id)
logger.warning('Profile "%s" does not specify a samp_rate', id)
continue
if "start_freq" in props:
start_freq = props["start_freq"]
srh = props["samp_rate"] / 2
center_freq = props["center_freq"]
if start_freq < center_freq - srh or start_freq > center_freq + srh:
logger.warning("start_freq for profile \"%s\" is out of range", id)
logger.warning('start_freq for profile "%s" is out of range', id)
def _getProfilePropertyLayer(self, profile):
layer = PropertyLayer()

View File

@ -15,18 +15,22 @@ class ConnectorSource(SdrSource):
super().__init__(id, props)
def getCommandMapper(self):
return super().getCommandMapper().setMappings(
{
"samp_rate": Option("-s"),
"tuner_freq": Option("-f"),
"port": Option("-p"),
"controlPort": Option("-c"),
"device": Option("-d"),
"iqswap": Flag("-i"),
"rtltcp_compat": Option("-r"),
"ppm": Option("-P"),
"rf_gain": Option("-g"),
}
return (
super()
.getCommandMapper()
.setMappings(
{
"samp_rate": Option("-s"),
"tuner_freq": Option("-f"),
"port": Option("-p"),
"controlPort": Option("-c"),
"device": Option("-d"),
"iqswap": Flag("-i"),
"rtltcp_compat": Option("-r"),
"ppm": Option("-P"),
"rf_gain": Option("-g"),
}
)
)
def sendControlMessage(self, changes):

View File

@ -32,11 +32,14 @@ class DirectSource(SdrSource, metaclass=ABCMeta):
"These depend on nmux_memory and samp_rate options in config_webrx.py"
)
return ["nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (
nmux_bufsize,
nmux_bufcnt,
self.port,
)]
return [
"nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1"
% (
nmux_bufsize,
nmux_bufcnt,
self.port,
)
]
def getCommand(self):
return super().getCommand() + self.getFormatConversion() + self.getNmuxCommand()

View File

@ -8,8 +8,10 @@ class Eb200Source(ConnectorSource):
super()
.getCommandMapper()
.setBase("eb200_connector")
.setMappings({
"long": Flag("-l"),
"remote": Argument(),
})
.setMappings(
{
"long": Flag("-l"),
"remote": Argument(),
}
)
)

View File

@ -9,9 +9,13 @@ logger = logging.getLogger(__name__)
class FifiSdrSource(DirectSource):
def getCommandMapper(self):
return super().getCommandMapper().setBase("arecord").setMappings(
{"device": Option("-D"), "samp_rate": Option("-r")}
).setStatic("-t raw -f S16_LE -c2 -")
return (
super()
.getCommandMapper()
.setBase("arecord")
.setMappings({"device": Option("-D"), "samp_rate": Option("-r")})
.setStatic("-t raw -f S16_LE -c2 -")
)
def getEventNames(self):
return super().getEventNames() + ["device"]
@ -20,7 +24,7 @@ class FifiSdrSource(DirectSource):
return ["csdr convert_s16_f", "csdr gain_ff 5"]
def sendRockProgFrequency(self, frequency):
process = Popen(["rockprog", "--vco", "-w", "--freq={}".format(frequency / 1E6)])
process = Popen(["rockprog", "--vco", "-w", "--freq={}".format(frequency / 1e6)])
process.communicate()
rc = process.wait()
if rc != 0:

View File

@ -8,4 +8,4 @@ class HackrfSource(SoapyConnectorSource):
return mappings
def getDriver(self):
return "hackrf"
return "hackrf"

View File

@ -17,6 +17,7 @@ from owrx.command import Flag, Option
# If you omit `remote` from config_webrx.py, hpsdrconnector will use the HPSDR discovery protocol
# to find radios on your local network and will connect to the first radio it discovered.
class HpsdrSource(ConnectorSource):
def getCommandMapper(self):
return (
@ -24,10 +25,11 @@ class HpsdrSource(ConnectorSource):
.getCommandMapper()
.setBase("hpsdrconnector")
.setMappings(
{
"tuner_freq": Option("--frequency"),
"samp_rate": Option("--samplerate"),
"remote": Option("--radio"),
"rf_gain": Option("--gain"),
})
{
"tuner_freq": Option("--frequency"),
"samp_rate": Option("--samplerate"),
"remote": Option("--radio"),
"rf_gain": Option("--gain"),
}
)
)

View File

@ -17,15 +17,21 @@ from owrx.command import Flag, Option
# floating points (option -p),no need for further conversions,
# so the method getFormatConversion(self) is not implemented at all.
class PerseussdrSource(DirectSource):
def getCommandMapper(self):
return super().getCommandMapper().setBase("perseustest -p -d -1 -a -t 0 -o - ").setMappings(
{
"samp_rate": Option("-s"),
"tuner_freq": Option("-f"),
"attenuator": Option("-u"),
"adc_preamp": Option("-m"),
"adc_dither": Option("-x"),
"wideband": Option("-w"),
}
return (
super()
.getCommandMapper()
.setBase("perseustest -p -d -1 -a -t 0 -o - ")
.setMappings(
{
"samp_rate": Option("-s"),
"tuner_freq": Option("-f"),
"attenuator": Option("-u"),
"adc_preamp": Option("-m"),
"adc_dither": Option("-x"),
"wideband": Option("-w"),
}
)
)

View File

@ -8,9 +8,11 @@ class RtlTcpSource(ConnectorSource):
super()
.getCommandMapper()
.setBase("rtl_tcp_connector")
.setMappings({
"bias_tee": Flag("-b"),
"direct_sampling": Option("-e"),
"remote": Argument(),
})
.setMappings(
{
"bias_tee": Flag("-b"),
"direct_sampling": Option("-e"),
"remote": Argument(),
}
)
)

View File

@ -5,11 +5,16 @@ from .connector import ConnectorSource
class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
def getCommandMapper(self):
return super().getCommandMapper().setBase("soapy_connector").setMappings(
{
"antenna": Option("-a"),
"soapy_settings": Option("-t"),
}
return (
super()
.getCommandMapper()
.setBase("soapy_connector")
.setMappings(
{
"antenna": Option("-a"),
"soapy_settings": Option("-t"),
}
)
)
"""

View File

@ -29,7 +29,7 @@ class Password(ABC):
class CleartextPassword(Password):
def is_valid(self, inp: str):
return self.pwinfo['value'] == inp
return self.pwinfo["value"] == inp
class User(object):

View File

@ -99,7 +99,7 @@ class Ft4Profile(WsjtProfile):
class Fst4Profile(WsjtProfile):
availableIntervals = [15, 30, 60, 120, 300, 900, 1800]
availableIntervals = [15, 30, 60, 120, 300, 900, 1800]
def __init__(self, interval):
self.interval = interval
@ -176,8 +176,8 @@ class WsjtParser(Parser):
ReportingEngine.getSharedInstance().spot(out)
self.handler.write_wsjt_message(out)
except (ValueError, IndexError):
logger.exception("error while parsing wsjt message")
except Exception:
logger.exception("Exception while parsing wsjt message")
def pushDecode(self, mode):
metrics = Metrics.getSharedInstance()
@ -208,7 +208,7 @@ class Decoder(ABC):
dateformat = self.profile.getTimestampFormat()
remain = instring[len(dateformat) + 1:]
try:
ts = datetime.strptime(instring[0:len(dateformat)], dateformat)
ts = datetime.strptime(instring[0: len(dateformat)], dateformat)
return remain, int(
datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000
)

View File

@ -43,24 +43,26 @@ class Worker(threading.Thread):
# function=wspr&date=210114&time=1732&sig=-15&dt=0.5&drift=0&tqrg=7.040019&tcall=DF2UU&tgrid=JN48&dbm=37&version=2.3.0-rc3&rcall=DD5JFK&rgrid=JN58SC&rqrg=7.040047&mode=2
# {'timestamp': 1610655960000, 'db': -23.0, 'dt': 0.3, 'freq': 7040048, 'drift': -1, 'msg': 'LA3JJ JO59 37', 'callsign': 'LA3JJ', 'locator': 'JO59', 'mode': 'WSPR'}
date = datetime.fromtimestamp(spot["timestamp"] / 1000, tz=timezone.utc)
data = parse.urlencode({
"function": "wspr",
"date": date.strftime("%y%m%d"),
"time": date.strftime("%H%M"),
"sig": spot["db"],
"dt": spot["dt"],
# FST4W does not have drift
"drift": spot["drift"] if "drift" in spot else 0,
"tqrg": spot["freq"] / 1E6,
"tcall": spot["callsign"],
"tgrid": spot["locator"],
"dbm": spot["dbm"],
"version": openwebrx_version,
"rcall": self.callsign,
"rgrid": self.locator,
# mode 2 = WSPR 2 minutes
"mode": self._getMode(spot)
}).encode()
data = parse.urlencode(
{
"function": "wspr",
"date": date.strftime("%y%m%d"),
"time": date.strftime("%H%M"),
"sig": spot["db"],
"dt": spot["dt"],
# FST4W does not have drift
"drift": spot["drift"] if "drift" in spot else 0,
"tqrg": spot["freq"] / 1e6,
"tcall": spot["callsign"],
"tgrid": spot["locator"],
"dbm": spot["dbm"],
"version": openwebrx_version,
"rcall": self.callsign,
"rgrid": self.locator,
# mode 2 = WSPR 2 minutes
"mode": self._getMode(spot),
}
).encode()
request.urlopen("http://wsprnet.org/post/", data)