Merge branch 'develop' into pycsdr

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

View File

@ -3,7 +3,8 @@
- Removed `port` configuration option; `rtltcp_compat` takes the port number with the new connectors - Removed `port` configuration option; `rtltcp_compat` takes the port number with the new connectors
- Added support for new WSJT-X modes FST4 and FST4W (only available with WSJT-X 2.3) - Added support for new WSJT-X modes FST4 and FST4W (only available with WSJT-X 2.3)
- Added support for demodulating M17 digital voice signals using m17-cxx-demod - Added support for demodulating M17 digital voice signals using m17-cxx-demod
- New reporting infrastructur, allowing WSPR and FST4W spots to be sent to wsprnet.org - New reporting infrastructur3, allowing WSPR and FST4W spots to be sent to wsprnet.org
- Add some basic filtering capabilities to the map
- New devices supported: - New devices supported:
- HPSDR devices (Hermes Lite 2) - HPSDR devices (Hermes Lite 2)
- BBRF103 / RX666 / RX888 devices supported by libsddc - BBRF103 / RX666 / RX888 devices supported by libsddc

View File

@ -6,7 +6,7 @@ config_webrx: configuration options for OpenWebRX
This file is part of OpenWebRX, This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI. an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de> Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as
@ -325,11 +325,14 @@ js8_decoding_depth = 3
temporary_directory = "/tmp" temporary_directory = "/tmp"
# Enable background service for decoding digital data. You can find more information at:
# https://github.com/jketterl/openwebrx/wiki/Background-decoding
services_enabled = False services_enabled = False
services_decoders = ["ft8", "ft4", "wspr", "packet"] services_decoders = ["ft8", "ft4", "wspr", "packet"]
# === aprs igate settings === # === aprs igate settings ===
# if you want to share your APRS decodes with the aprs network, configure these settings accordingly # If you want to share your APRS decodes with the aprs network, configure these settings accordingly.
# Make sure that you have set services_enabled to true and customize services_decoders to your needs.
aprs_callsign = "N0CALL" aprs_callsign = "N0CALL"
aprs_igate_enabled = False aprs_igate_enabled = False
aprs_igate_server = "euro.aprs2.net" aprs_igate_server = "euro.aprs2.net"

View File

@ -4,7 +4,7 @@ OpenWebRX csdr plugin: do the signal processing with csdr
This file is part of OpenWebRX, This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI. an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de> Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as
@ -211,10 +211,7 @@ class dsp(object):
"csdr limit_ff", "csdr limit_ff",
] ]
chain += last_decimation_block chain += last_decimation_block
chain += [ chain += ["csdr deemphasis_wfm_ff {audio_rate} {wfm_deemphasis_tau}", "csdr convert_f_s16"]
"csdr deemphasis_wfm_ff {audio_rate} {wfm_deemphasis_tau}",
"csdr convert_f_s16"
]
elif self.isDigitalVoice(which): elif self.isDigitalVoice(which):
chain += ["csdr fmdemod_quadri_cf"] chain += ["csdr fmdemod_quadri_cf"]
chain += last_decimation_block chain += last_decimation_block
@ -473,7 +470,9 @@ class dsp(object):
def set_secondary_offset_freq(self, value): def set_secondary_offset_freq(self, value):
self.secondary_offset_freq = value self.secondary_offset_freq = value
if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"): if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"):
self.pipes["secondary_shift_pipe"].write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())) self.pipes["secondary_shift_pipe"].write(
"%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())
)
def stop_secondary_demodulator(self): def stop_secondary_demodulator(self):
if not self.secondary_processes_running: if not self.secondary_processes_running:
@ -548,25 +547,20 @@ class dsp(object):
self.restart() self.restart()
def calculate_decimation(self): def calculate_decimation(self):
(self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate()) (self.decimation, self.last_decimation) = self.get_decimation(self.samp_rate, self.get_audio_rate())
def get_decimation(self, input_rate, output_rate): def get_decimation(self, input_rate, output_rate):
decimation = 1 decimation = 1
correction = 1 target_rate = output_rate
# wideband fm has a much higher frequency deviation (75kHz). # wideband fm has a much higher frequency deviation (75kHz).
# we cannot cover this if we immediately decimate to the sample rate the audio will have later on, so we need # we cannot cover this if we immediately decimate to the sample rate the audio will have later on, so we need
# to compensate here. # to compensate here.
# the factor of 6 is by experimentation only, with a minimum audio rate of 36kHz (enforced by the client) if self.get_demodulator() == "wfm" and output_rate < 200000:
# this allows us to cover at least +/- 108kHz of frequency spectrum (may be higher, but that's the worst case). target_rate = 200000
# the correction factor is automatically compensated for by the secondary decimation stage, which comes while input_rate / (decimation + 1) >= target_rate:
# after the demodulator.
if self.get_demodulator() == "wfm":
correction = 6
while input_rate / (decimation + 1) >= output_rate * correction:
decimation += 1 decimation += 1
fraction = float(input_rate / decimation) / output_rate fraction = float(input_rate / decimation) / output_rate
intermediate_rate = input_rate / decimation return decimation, fraction
return decimation, fraction, intermediate_rate
def if_samp_rate(self): def if_samp_rate(self):
return self.samp_rate / self.decimation return self.samp_rate / self.decimation
@ -601,7 +595,7 @@ class dsp(object):
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w"] return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w"]
def isJs8(self, demodulator = None): def isJs8(self, demodulator=None):
if demodulator is None: if demodulator is None:
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
return demodulator == "js8" return demodulator == "js8"
@ -712,7 +706,11 @@ class dsp(object):
def set_squelch_level(self, squelch_level): def set_squelch_level(self, squelch_level):
self.squelch_level = squelch_level self.squelch_level = squelch_level
# no squelch required on digital voice modes # no squelch required on digital voice modes
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV() else self.squelch_level actual_squelch = (
-150
if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV()
else self.squelch_level
)
if self.running: if self.running:
self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch))) self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
@ -887,6 +885,7 @@ class dsp(object):
self.start_secondary_demodulator() self.start_secondary_demodulator()
if self.has_pipe("smeter_pipe"): if self.has_pipe("smeter_pipe"):
def read_smeter(): def read_smeter():
raw = self.pipes["smeter_pipe"].readline() raw = self.pipes["smeter_pipe"].readline()
if len(raw) == 0: if len(raw) == 0:
@ -896,6 +895,7 @@ class dsp(object):
self.output.send_output("smeter", read_smeter) self.output.send_output("smeter", read_smeter)
if self.has_pipe("meta_pipe"): if self.has_pipe("meta_pipe"):
def read_meta(): def read_meta():
raw = self.pipes["meta_pipe"].readline() raw = self.pipes["meta_pipe"].readline()
if len(raw) == 0: if len(raw) == 0:

View File

@ -42,6 +42,7 @@ class Pipe(object):
immediately here), resulting in empty reads until data is available. This is handled specially in the immediately here), resulting in empty reads until data is available. This is handled specially in the
ReadingPipe class. ReadingPipe class.
""" """
def opener(path, flags): def opener(path, flags):
fd = os.open(path, flags | os.O_NONBLOCK) fd = os.open(path, flags | os.O_NONBLOCK)
os.set_blocking(fd, True) os.set_blocking(fd, True)
@ -88,7 +89,7 @@ class WritingPipe(Pipe):
except OSError as error: except OSError as error:
# ENXIO = FIFO has not been opened for reading # ENXIO = FIFO has not been opened for reading
if error.errno == 6: if error.errno == 6:
time.sleep(.1) time.sleep(0.1)
retries += 1 retries += 1
else: else:
raise raise

3
debian/changelog vendored
View File

@ -7,8 +7,9 @@ openwebrx (0.21.0) UNRELEASED; urgency=low
WSJT-X 2.3) WSJT-X 2.3)
* Added support for demodulating M17 digital voice signals using * Added support for demodulating M17 digital voice signals using
m17-cxx-demod m17-cxx-demod
* New reporting infrastructur, allowing WSPR and FST4W spots to be sent to * New reporting infrastructure, allowing WSPR and FST4W spots to be sent to
wsprnet.org wsprnet.org
* Add some basic filtering capabilities to the map
* New devices supported: * New devices supported:
- HPSDR devices (Hermes Lite 2) (`"type": "hpsdr"`) - HPSDR devices (Hermes Lite 2) (`"type": "hpsdr"`)
- BBRF103 / RX666 / RX888 devices supported by libsddc (`"type": "sddc"`) - BBRF103 / RX666 / RX888 devices supported by libsddc (`"type": "sddc"`)

View File

@ -31,6 +31,7 @@ ul {
background-color: #fff; background-color: #fff;
padding: 10px; padding: 10px;
margin: 10px; margin: 10px;
user-select: none;
} }
/* show it as soon as google maps has moved it to its container */ /* show it as soon as google maps has moved it to its container */
@ -43,6 +44,15 @@ ul {
padding: 0; padding: 0;
} }
.openwebrx-map-legend ul li {
cursor: pointer;
}
.openwebrx-map-legend ul li.disabled {
opacity: .3;
filter: grayscale(70%);
}
.openwebrx-map-legend li.square .illustration { .openwebrx-map-legend li.square .illustration {
display: inline-block; display: inline-block;
width: 30px; width: 30px;

View File

@ -3,7 +3,7 @@
This file is part of OpenWebRX, This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI. an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de> Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as
@ -932,7 +932,8 @@ img.openwebrx-mirror-img
.openwebrx-meta-slot > * { .openwebrx-meta-slot > * {
flex: 0; flex: 0;
flex-basis: 1.125em; flex-basis: 1.2em;
line-height: 1.2em;
} }
.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before { .openwebrx-meta-slot, .openwebrx-meta-slot.muted:before {
@ -978,18 +979,39 @@ img.openwebrx-mirror-img
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.openwebrx-meta-slot.active .openwebrx-meta-user-image { .openwebrx-meta-slot.active.direct .openwebrx-meta-user-image,
#openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image {
background-image: url("../gfx/openwebrx-directcall.png"); background-image: url("../gfx/openwebrx-directcall.png");
} }
.openwebrx-meta-slot.active .openwebrx-meta-user-image.group { .openwebrx-meta-slot.active.group .openwebrx-meta-user-image {
background-image: url("../gfx/openwebrx-groupcall.png"); background-image: url("../gfx/openwebrx-groupcall.png");
} }
.openwebrx-meta-slot.group .openwebrx-dmr-target:not(:empty):before {
content: "Talkgroup: ";
}
.openwebrx-meta-slot.direct .openwebrx-dmr-target:not(:empty):before {
content: "Direct: ";
}
.openwebrx-dmr-timeslot-panel * { .openwebrx-dmr-timeslot-panel * {
cursor: pointer; cursor: pointer;
} }
.openwebrx-ysf-mode:not(:empty):before {
content: "Mode: ";
}
.openwebrx-ysf-up:not(:empty):before {
content: "Up: ";
}
.openwebrx-ysf-down:not(:empty):before {
content: "Down: ";
}
.openwebrx-maps-pin { .openwebrx-maps-pin {
background-image: url("../gfx/google_maps_pin.svg"); background-image: url("../gfx/google_maps_pin.svg");
background-position: center; background-position: center;

View File

@ -4,7 +4,7 @@
This file is part of OpenWebRX, This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI. an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de> Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as
@ -28,6 +28,8 @@
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" /> <link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" /> <link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#222" />
</head> </head>
<body onload="openwebrx_init();"> <body onload="openwebrx_init();">
<div id="webrx-page-container"> <div id="webrx-page-container">

View File

@ -24,12 +24,8 @@ DmrMetaSlot.prototype.update = function(data) {
if (data['sync'] && data['sync'] === "voice") { if (data['sync'] && data['sync'] === "voice") {
this.setId(data['additional'] && data['additional']['callsign'] || data['source']); this.setId(data['additional'] && data['additional']['callsign'] || data['source']);
this.setName(data['additional'] && data['additional']['fname']); this.setName(data['additional'] && data['additional']['fname']);
if (data['type'] === "group") { this.setMode(['group', 'direct'].includes(data['type']) ? data['type'] : undefined);
this.setTalkgroup(data['target']); this.setTarget(data['target']);
}
if (data['type'] === "direct") {
this.setDirect(data['target']);
}
this.el.addClass("active"); this.el.addClass("active");
} else { } else {
this.clear(); this.clear();
@ -48,35 +44,26 @@ DmrMetaSlot.prototype.setName = function(name) {
this.el.find('.openwebrx-dmr-name').text(name || ''); this.el.find('.openwebrx-dmr-name').text(name || '');
}; };
DmrMetaSlot.prototype.setTalkgroup = function(talkgroup) { DmrMetaSlot.prototype.setMode = function(mode) {
if (this.talkgroup === talkgroup && this.targetMode === 'talkgroup') return; if (this.mode === mode) return;
this.talkgroup = talkgroup; this.mode = mode;
this.targetMode = 'talkgroup'; var classes = ['group', 'direct'].filter(function(c){
var text = ''; return c !== mode;
if (talkgroup && talkgroup != '') { });
text = 'Talkgroup: ' + talkgroup; this.el.removeClass(classes.join(' ')).addClass(mode);
} }
this.el.find('.openwebrx-dmr-target').text(text);
this.el.find(".openwebrx-meta-user-image").addClass("group");
};
DmrMetaSlot.prototype.setDirect = function(call) { DmrMetaSlot.prototype.setTarget = function(target) {
if (this.call === call && this.targetMode === 'direct') return; if (this.target === target) return;
this.call = call; this.target = target;
this.targetMode = 'direct'; this.el.find('.openwebrx-dmr-target').text(target || '');
var text = ''; }
if (call && call != '') {
text = 'Direct: ' + call;
}
this.el.find('.openwebrx-dmr-target').text(text);
this.el.find(".openwebrx-meta-user-image").removeClass("group");
};
DmrMetaSlot.prototype.clear = function() { DmrMetaSlot.prototype.clear = function() {
this.setId(); this.setId();
this.setName(); this.setName();
this.setTalkgroup(); this.setMode();
this.setDirect(); this.setTarget();
this.el.removeClass("active"); this.el.removeClass("active");
}; };
@ -143,11 +130,7 @@ YsfMetaPanel.prototype.clear = function() {
YsfMetaPanel.prototype.setMode = function(mode) { YsfMetaPanel.prototype.setMode = function(mode) {
if (this.mode === mode) return; if (this.mode === mode) return;
this.mode = mode; this.mode = mode;
var text = ''; this.el.find('.openwebrx-ysf-mode').text(mode || '');
if (mode && mode != '') {
text = 'Mode: ' + mode;
}
this.el.find('.openwebrx-ysf-mode').text(text);
}; };
YsfMetaPanel.prototype.setSource = function(source) { YsfMetaPanel.prototype.setSource = function(source) {
@ -170,21 +153,13 @@ YsfMetaPanel.prototype.setLocation = function(lat, lon, callsign) {
YsfMetaPanel.prototype.setUp = function(up) { YsfMetaPanel.prototype.setUp = function(up) {
if (this.up === up) return; if (this.up === up) return;
this.up = up; this.up = up;
var text = ''; this.el.find('.openwebrx-ysf-up').text(up || '');
if (up && up != '') {
text = 'Up: ' + up;
}
this.el.find('.openwebrx-ysf-up').text(text);
}; };
YsfMetaPanel.prototype.setDown = function(down) { YsfMetaPanel.prototype.setDown = function(down) {
if (this.down === down) return; if (this.down === down) return;
this.down = down; this.down = down;
var text = ''; this.el.find('.openwebrx-ysf-down').text(down || '');
if (down && down != '') {
text = 'Down: ' + down;
}
this.el.find('.openwebrx-ysf-down').text(text);
} }
MetaPanel.types = { MetaPanel.types = {

View File

@ -30,7 +30,8 @@ var nite = {
fillOpacity: 0.1, fillOpacity: 0.1,
strokeOpacity: 0, strokeOpacity: 0,
clickable: false, clickable: false,
editable: false editable: false,
zIndex: 1
}); });
this.marker_twilight_nautical = new google.maps.Circle({ this.marker_twilight_nautical = new google.maps.Circle({
map: this.map, map: this.map,
@ -40,7 +41,8 @@ var nite = {
fillOpacity: 0.1, fillOpacity: 0.1,
strokeOpacity: 0, strokeOpacity: 0,
clickable: false, clickable: false,
editable: false editable: false,
zIndex: 1
}); });
this.marker_twilight_astronomical = new google.maps.Circle({ this.marker_twilight_astronomical = new google.maps.Circle({
map: this.map, map: this.map,
@ -50,7 +52,8 @@ var nite = {
fillOpacity: 0.1, fillOpacity: 0.1,
strokeOpacity: 0, strokeOpacity: 0,
clickable: false, clickable: false,
editable: false editable: false,
zIndex: 1
}); });
this.marker_night = new google.maps.Circle({ this.marker_night = new google.maps.Circle({
map: this.map, map: this.map,
@ -60,7 +63,8 @@ var nite = {
fillOpacity: 0.1, fillOpacity: 0.1,
strokeOpacity: 0, strokeOpacity: 0,
clickable: false, clickable: false,
editable: false editable: false,
zIndex: 1
}); });
}, },
getShadowRadiusFromAngle: function(angle) { getShadowRadiusFromAngle: function(angle) {

View File

@ -87,6 +87,7 @@
$('#openwebrx-map-colormode').on('change', function(){ $('#openwebrx-map-colormode').on('change', function(){
colorMode = $(this).val(); colorMode = $(this).val();
colorKeys = {}; colorKeys = {};
filterRectangles(allRectangles);
reColor(); reColor();
updateLegend(); updateLegend();
}); });
@ -94,7 +95,10 @@
var updateLegend = function() { var updateLegend = function() {
var lis = $.map(colorKeys, function(value, key) { var lis = $.map(colorKeys, function(value, key) {
return '<li class="square"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>'; // fake rectangle to test if the filter would match
var fakeRectangle = Object.fromEntries([[colorMode.slice(2), key]]);
var disabled = rectangleFilter(fakeRectangle) ? '' : ' disabled';
return '<li class="square' + disabled + '" data-selector="' + key + '"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>';
}); });
$(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>'); $(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>');
} }
@ -164,11 +168,17 @@
}); });
rectangles[update.callsign] = rectangle; rectangles[update.callsign] = rectangle;
} }
rectangle.lastseen = update.lastseen;
rectangle.locator = update.location.locator;
rectangle.mode = update.mode;
rectangle.band = update.band;
rectangle.center = center;
rectangle.setOptions($.extend({ rectangle.setOptions($.extend({
strokeColor: color, strokeColor: color,
strokeWeight: 2, strokeWeight: 2,
fillColor: color, fillColor: color,
map: map, map: rectangleFilter(rectangle) ? map : undefined,
bounds:{ bounds:{
north: lat, north: lat,
south: lat + 1, south: lat + 1,
@ -176,11 +186,6 @@
east: lon + 2 east: lon + 2
} }
}, getRectangleOpacityOptions(update.lastseen) )); }, getRectangleOpacityOptions(update.lastseen) ));
rectangle.lastseen = update.lastseen;
rectangle.locator = update.location.locator;
rectangle.mode = update.mode;
rectangle.band = update.band;
rectangle.center = center;
if (expectedLocator && expectedLocator == update.location.locator) { if (expectedLocator && expectedLocator == update.location.locator) {
map.panTo(center); map.panTo(center);
@ -246,7 +251,10 @@
processUpdates(updateQueue); processUpdates(updateQueue);
updateQueue = []; updateQueue = [];
}); });
map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($(".openwebrx-map-legend")[0]);
var $legend = $(".openwebrx-map-legend");
setupLegendFilters($legend);
map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($legend[0]);
if (!receiverMarker) { if (!receiverMarker) {
receiverMarker = new google.maps.Marker(); receiverMarker = new google.maps.Marker();
@ -329,7 +337,7 @@
infowindow.locator = locator; infowindow.locator = locator;
var inLocator = $.map(rectangles, function(r, callsign) { var inLocator = $.map(rectangles, function(r, callsign) {
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band} return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
}).filter(function(d) { }).filter(rectangleFilter).filter(function(d) {
return d.locator == locator; return d.locator == locator;
}).sort(function(a, b){ }).sort(function(a, b){
return b.lastseen - a.lastseen; return b.lastseen - a.lastseen;
@ -424,4 +432,36 @@
}); });
}, 1000); }, 1000);
var rectangleFilter = allRectangles = function() { return true; };
var filterRectangles = function(filter) {
rectangleFilter = filter;
$.each(rectangles, function(_, r) {
r.setMap(rectangleFilter(r) ? map : undefined);
});
};
var setupLegendFilters = function($legend) {
$content = $legend.find('.content');
$content.on('click', 'li', function() {
var $el = $(this);
$lis = $content.find('li');
if ($lis.hasClass('disabled') && !$el.hasClass('disabled')) {
$lis.removeClass('disabled');
filterRectangles(allRectangles);
} else {
$el.removeClass('disabled');
$lis.filter(function() {
return this != $el[0]
}).addClass('disabled');
var key = colorMode.slice(2);
var selector = $el.data('selector');
filterRectangles(function(r) {
return r[key] === selector;
});
}
});
}
})(); })();

View File

@ -3,7 +3,7 @@
This file is part of OpenWebRX, This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI. an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de> Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as
@ -694,7 +694,17 @@ function on_ws_recv(evt) {
networkSpeedMeasurement.add(evt.data.length); networkSpeedMeasurement.add(evt.data.length);
if (evt.data.substr(0, 16) === "CLIENT DE SERVER") { if (evt.data.substr(0, 16) === "CLIENT DE SERVER") {
divlog("Server acknowledged WebSocket connection."); params = Object.fromEntries(
evt.data.slice(17).split(' ').map(function(param) {
var args = param.split('=');
return [args[0], args.slice(1).join('=')]
})
);
var versionInfo = 'Unknown server';
if (params.server && params.server === 'openwebrx' && params.version) {
versionInfo = 'OpenWebRX version: ' + params.version;
}
divlog('Server acknowledged WebSocket connection, ' + versionInfo);
} else { } else {
try { try {
var json = JSON.parse(evt.data); var json = JSON.parse(evt.data);

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

View File

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

View File

@ -17,7 +17,7 @@ class Band(object):
for (mode, freqs) in dict["frequencies"].items(): for (mode, freqs) in dict["frequencies"].items():
if mode not in availableModes: if mode not in availableModes:
logger.info( 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 mode=mode
) )
) )

View File

@ -112,9 +112,7 @@ class Config:
@staticmethod @staticmethod
def validateConfig(): def validateConfig():
pm = Config.get() pm = Config.get()
errors = [ errors = [Config.checkTempDirectory(pm)]
Config.checkTempDirectory(pm)
]
return [e for e in errors if e is not None] 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.dsp = None
self.sdr = None self.sdr = None
self.sdrConfigSubs = [] self.configSubs = []
self.connectionProperties = {} self.connectionProperties = {}
try: try:
@ -145,9 +145,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.close() self.close()
raise raise
globalConfig = Config.get().filter(*OpenWebRxReceiverClient.global_config_keys) self.setupGlobalConfig()
self.globalConfigSub = globalConfig.wire(self.write_config) self.stack = self.setupStack()
self.write_config(globalConfig.__dict__())
self.setSdr() self.setSdr()
@ -162,8 +161,51 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
CpuUsageThread.getSharedInstance().add_client(self) CpuUsageThread.getSharedInstance().add_client(self)
def __del__(self): def __del__(self):
if hasattr(self, "globalConfigSub"): if hasattr(self, "configSubs"):
self.globalConfigSub.cancel() 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): def onStateChange(self, state):
if state == SdrSource.STATE_RUNNING: if state == SdrSource.STATE_RUNNING:
@ -242,9 +284,6 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.stopDsp() self.stopDsp()
while self.sdrConfigSubs:
self.sdrConfigSubs.pop().cancel()
if self.sdr is not None: if self.sdr is not None:
self.sdr.removeClient(self) self.sdr.removeClient(self)
@ -259,37 +298,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def handleSdrAvailable(self): def handleSdrAvailable(self):
self.getDsp().setProperties(self.connectionProperties) 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.__sendProfiles()
self.sdr.addSpectrumClient(self) self.sdr.addSpectrumClient(self)
@ -306,8 +316,6 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.stopDsp() self.stopDsp()
CpuUsageThread.getSharedInstance().remove_client(self) CpuUsageThread.getSharedInstance().remove_client(self)
ClientRegistry.getSharedInstance().removeClient(self) ClientRegistry.getSharedInstance().removeClient(self)
while self.sdrConfigSubs:
self.sdrConfigSubs.pop().cancel()
super().close() super().close()
def stopDsp(self): def stopDsp(self):
@ -325,11 +333,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
keys = config["configurable_keys"] keys = config["configurable_keys"]
if not keys: if not keys:
return return
# only the keys in the protected property manager can be overridden from the web protected = self.stack.filter(*keys)
stack = PropertyStack()
stack.addLayer(0, self.sdr.getProps())
stack.addLayer(1, config)
protected = stack.filter(*keys)
for key, value in params.items(): for key, value in params.items():
try: try:
protected[key] = value protected[key] = value
@ -406,15 +410,20 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.send({"type": "backoff", "reason": reason}) self.send({"type": "backoff", "reason": reason})
def write_js8_message(self, frame: Js8Frame, freq: int): def write_js8_message(self, frame: Js8Frame, freq: int):
self.send({"type": "js8_message", "value": { self.send(
{
"type": "js8_message",
"value": {
"msg": str(frame), "msg": str(frame),
"timestamp": frame.timestamp, "timestamp": frame.timestamp,
"db": frame.db, "db": frame.db,
"dt": frame.dt, "dt": frame.dt,
"freq": freq + frame.freq, "freq": freq + frame.freq,
"thread_type": frame.thread_type, "thread_type": frame.thread_type,
"mode": frame.mode "mode": frame.mode,
}}) },
}
)
def write_modes(self, modes): def write_modes(self, modes):
def to_json(m): def to_json(m):
@ -426,10 +435,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
"squelch": m.squelch, "squelch": m.squelch,
} }
if m.bandpass is not None: if m.bandpass is not None:
res["bandpass"] = { res["bandpass"] = {"low_cut": m.bandpass.low_cut, "high_cut": m.bandpass.high_cut}
"low_cut": m.bandpass.low_cut,
"high_cut": m.bandpass.high_cut
}
if isinstance(m, DigitalMode): if isinstance(m, DigitalMode):
res["underlying"] = m.underlying res["underlying"] = m.underlying
return res return res
@ -442,12 +448,14 @@ class MapConnection(OpenWebRxClient):
super().__init__(conn) super().__init__(conn)
pm = Config.get() pm = Config.get()
self.write_config(pm.filter( self.write_config(
pm.filter(
"google_maps_api_key", "google_maps_api_key",
"receiver_gps", "receiver_gps",
"map_position_retention_time", "map_position_retention_time",
"receiver_name", "receiver_name",
).__dict__()) ).__dict__()
)
Map.getSharedInstance().addClient(self) Map.getSharedInstance().addClient(self)

View File

@ -7,7 +7,9 @@ class Controller(object):
self.request = request self.request = request
self.options = options 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) self.handler.send_response(code)
if headers is None: if headers is None:
headers = {} headers = {}
@ -27,7 +29,7 @@ class Controller(object):
def send_redirect(self, location, code=303, cookies=None): def send_redirect(self, location, code=303, cookies=None):
self.handler.send_response(code) self.handler.send_response(code)
if cookies is not None: 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.send_header("Location", location)
self.handler.end_headers() self.handler.end_headers()

View File

@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
class GzipMixin(object): 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: 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 "gzip" in accepted:
if type(content) == str: if type(content) == str:
content = content.encode() content = content.encode()
@ -26,11 +26,7 @@ class GzipMixin(object):
super().send_response(content, headers=headers, content_type=content_type, *args, **kwargs) super().send_response(content, headers=headers, content_type=content_type, *args, **kwargs)
def zipable(self, content_type): def zipable(self, content_type):
types = [ types = ["application/javascript", "text/css", "text/html"]
"application/javascript",
"text/css",
"text/html"
]
return content_type in types return content_type in types
def gzip(self, content): def gzip(self, content):
@ -143,7 +139,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
"lib/settings/Input.js", "lib/settings/Input.js",
"lib/settings/SdrDevice.js", "lib/settings/SdrDevice.js",
"settings.js", "settings.js",
] ],
} }
def indexAction(self): def indexAction(self):

View File

@ -8,15 +8,19 @@ class ReceiverIdController(Controller):
super().__init__(handler, request, options) super().__init__(handler, request, options)
self.authHeader = None 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 self.authHeader is not None:
if headers is None: if headers is None:
headers = {} headers = {}
headers['Authorization'] = self.authHeader headers["Authorization"] = self.authHeader
super().send_response(content, code=code, content_type=content_type, last_modified=last_modified, max_age=max_age, headers=headers) super().send_response(
content, code=code, content_type=content_type, last_modified=last_modified, max_age=max_age, headers=headers
)
pass pass
def handle_request(self): def handle_request(self):
if "Authorization" in self.request.headers: 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() super().handle_request()

View File

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

View File

@ -22,7 +22,7 @@ class StatusController(ReceiverIdController):
"name": receiver.getName(), "name": receiver.getName(),
# TODO would be better to have types from the config here # TODO would be better to have types from the config here
"type": type(receiver).__name__, "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 return stats
@ -38,6 +38,6 @@ class StatusController(ReceiverIdController):
}, },
"max_clients": pm["max_clients"], "max_clients": pm["max_clients"],
"version": openwebrx_version, "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") 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 # prevent X11 programs from opening windows if called from a GUI shell
env.pop("DISPLAY", None) env.pop("DISPLAY", None)
try: 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() rc = process.wait()
if expected_result is None: if expected_result is None:
return rc != 32512 return rc != 32512
@ -214,7 +221,6 @@ class FeatureDetector(object):
""" """
return self.command_is_runnable("perseustest -h") return self.command_is_runnable("perseustest -h")
def has_digiham(self): def has_digiham(self):
""" """
To use digital voice modes, the digiham package is required. You can find the package and installation To use digital voice modes, the digiham package is required. You can find the package and installation

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ TFESC = 0xDD
FEET_PER_METER = 3.28084 FEET_PER_METER = 3.28084
class DirewolfConfig(object): class DirewolfConfig(object):
def getConfig(self, port, is_service): def getConfig(self, port, is_service):
pm = Config.get() pm = Config.get()
@ -40,7 +41,7 @@ IGLOGIN {callsign} {password}
) )
if pm["aprs_igate_beacon"]: if pm["aprs_igate_beacon"]:
#Format beacon lat/lon # Format beacon lat/lon
lat = pm["receiver_gps"]["lat"] lat = pm["receiver_gps"]["lat"]
lon = pm["receiver_gps"]["lon"] lon = pm["receiver_gps"]["lon"]
direction_ns = "N" if lat > 0 else "S" 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) 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) lon = "{0:03d}^{1:05.2f}{2}".format(int(lon), (lon - int(lon)) * 60, direction_we)
#Format beacon details # Format beacon details
symbol = str(pm["aprs_igate_symbol"]) if "aprs_igate_symbol" in pm else "R&" 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 "" 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 "" 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\"" 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 = "" height = ""
if "aprs_igate_height" in pm: if "aprs_igate_height" in pm:
try: try:
@ -64,15 +65,18 @@ IGLOGIN {callsign} {password}
height_ft = round(height_m * FEET_PER_METER) height_ft = round(height_m * FEET_PER_METER)
height = "HEIGHT=" + str(height_ft) height = "HEIGHT=" + str(height_ft)
except: 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] != '"'))): if (len(comment) > 0) and ((comment[0] != '"') or (comment[len(comment) - 1] != '"')):
comment = "\"" + comment + "\"" comment = '"' + comment + '"'
elif(len(comment) == 0): elif len(comment) == 0:
comment = "\"\"" comment = '""'
pbeacon = "PBEACON sendto=IG delay=0:30 every=60:00 symbol={symbol} lat={lat} long={lon} {height} {gain} {adir} comment={comment}".format( 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) logger.info("APRS PBEACON String: " + pbeacon)
@ -98,7 +102,7 @@ class KissClient(object):
pass pass
def __init__(self, port): def __init__(self, port):
delay = .5 delay = 0.5
retries = 0 retries = 0
while True: while True:
try: try:

View File

@ -60,24 +60,47 @@ class Modes(object):
AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)), AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)),
AnalogMode("cw", "CW", bandpass=Bandpass(700, 900)), AnalogMode("cw", "CW", bandpass=Bandpass(700, 900)),
AnalogMode("dmr", "DMR", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False), 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("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("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("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), AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False),
DigitalMode("bpsk31", "BPSK31", underlying=["usb"]), DigitalMode("bpsk31", "BPSK31", underlying=["usb"]),
DigitalMode("bpsk63", "BPSK63", underlying=["usb"]), DigitalMode("bpsk63", "BPSK63", underlying=["usb"]),
DigitalMode("ft8", "FT8", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), DigitalMode(
DigitalMode("ft4", "FT4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), "ft8", "FT8", 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(
"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( DigitalMode(
"wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True "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(
DigitalMode("fst4w", "FST4W", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"], service=True), "fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x-2-3"], service=True
DigitalMode("js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], 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( DigitalMode(
"packet", "packet",
"Packet", "Packet",

View File

@ -1,10 +1,17 @@
from owrx.parser import Parser from owrx.parser import Parser
import logging
logger = logging.getLogger(__name__)
class PocsagParser(Parser): class PocsagParser(Parser):
def parse(self, raw): def parse(self, raw):
try:
fields = raw.decode("ascii", "replace").rstrip("\n").split(";") 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] != ""} meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""}
if "address" in meta: if "address" in meta:
meta["address"] = int(meta["address"]) meta["address"] = int(meta["address"])
self.handler.write_pocsag_data(meta) 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 # id
[0x00, 0x03] [0x00, 0x03]
# length # length
+ list(length.to_bytes(2, 'big')) + list(length.to_bytes(2, "big"))
+ Uploader.receieverDelimiter + Uploader.receieverDelimiter
# number of fields # number of fields
+ list(num_fields.to_bytes(2, 'big')) + list(num_fields.to_bytes(2, "big"))
# padding # padding
+ [0x00, 0x00] + [0x00, 0x00]
# receiverCallsign # receiverCallsign
@ -163,9 +163,7 @@ class Uploader(object):
# decodingSoftware # decodingSoftware
+ [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# antennaInformation # 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 # padding
+ [0x00, 0x00] + [0x00, 0x00]
) )

View File

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

View File

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

View File

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

View File

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

View File

@ -86,17 +86,17 @@ class SdrSource(ABC):
for id, p in self.props["profiles"].items(): for id, p in self.props["profiles"].items():
props.replaceLayer(0, self._getProfilePropertyLayer(p)) props.replaceLayer(0, self._getProfilePropertyLayer(p))
if "center_freq" not in props: 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 continue
if "samp_rate" not in props: 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 continue
if "start_freq" in props: if "start_freq" in props:
start_freq = props["start_freq"] start_freq = props["start_freq"]
srh = props["samp_rate"] / 2 srh = props["samp_rate"] / 2
center_freq = props["center_freq"] center_freq = props["center_freq"]
if start_freq < center_freq - srh or start_freq > center_freq + srh: 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): def _getProfilePropertyLayer(self, profile):
layer = PropertyLayer() layer = PropertyLayer()

View File

@ -15,7 +15,10 @@ class ConnectorSource(SdrSource):
super().__init__(id, props) super().__init__(id, props)
def getCommandMapper(self): def getCommandMapper(self):
return super().getCommandMapper().setMappings( return (
super()
.getCommandMapper()
.setMappings(
{ {
"samp_rate": Option("-s"), "samp_rate": Option("-s"),
"tuner_freq": Option("-f"), "tuner_freq": Option("-f"),
@ -28,6 +31,7 @@ class ConnectorSource(SdrSource):
"rf_gain": Option("-g"), "rf_gain": Option("-g"),
} }
) )
)
def sendControlMessage(self, changes): def sendControlMessage(self, changes):
for prop, value in changes.items(): for prop, value in changes.items():

View File

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

View File

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

View File

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

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 # 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. # to find radios on your local network and will connect to the first radio it discovered.
class HpsdrSource(ConnectorSource): class HpsdrSource(ConnectorSource):
def getCommandMapper(self): def getCommandMapper(self):
return ( return (
@ -29,5 +30,6 @@ class HpsdrSource(ConnectorSource):
"samp_rate": Option("--samplerate"), "samp_rate": Option("--samplerate"),
"remote": Option("--radio"), "remote": Option("--radio"),
"rf_gain": Option("--gain"), "rf_gain": Option("--gain"),
}) }
)
) )

View File

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

View File

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

View File

@ -5,12 +5,17 @@ from .connector import ConnectorSource
class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta): class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
def getCommandMapper(self): def getCommandMapper(self):
return super().getCommandMapper().setBase("soapy_connector").setMappings( return (
super()
.getCommandMapper()
.setBase("soapy_connector")
.setMappings(
{ {
"antenna": Option("-a"), "antenna": Option("-a"),
"soapy_settings": Option("-t"), "soapy_settings": Option("-t"),
} }
) )
)
""" """
must be implemented by child classes to be able to build a driver-based device selector by default. must be implemented by child classes to be able to build a driver-based device selector by default.

View File

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

View File

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

View File

@ -43,7 +43,8 @@ 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 # 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'} # {'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) date = datetime.fromtimestamp(spot["timestamp"] / 1000, tz=timezone.utc)
data = parse.urlencode({ data = parse.urlencode(
{
"function": "wspr", "function": "wspr",
"date": date.strftime("%y%m%d"), "date": date.strftime("%y%m%d"),
"time": date.strftime("%H%M"), "time": date.strftime("%H%M"),
@ -51,7 +52,7 @@ class Worker(threading.Thread):
"dt": spot["dt"], "dt": spot["dt"],
# FST4W does not have drift # FST4W does not have drift
"drift": spot["drift"] if "drift" in spot else 0, "drift": spot["drift"] if "drift" in spot else 0,
"tqrg": spot["freq"] / 1E6, "tqrg": spot["freq"] / 1e6,
"tcall": spot["callsign"], "tcall": spot["callsign"],
"tgrid": spot["locator"], "tgrid": spot["locator"],
"dbm": spot["dbm"], "dbm": spot["dbm"],
@ -59,8 +60,9 @@ class Worker(threading.Thread):
"rcall": self.callsign, "rcall": self.callsign,
"rgrid": self.locator, "rgrid": self.locator,
# mode 2 = WSPR 2 minutes # mode 2 = WSPR 2 minutes
"mode": self._getMode(spot) "mode": self._getMode(spot),
}).encode() }
).encode()
request.urlopen("http://wsprnet.org/post/", data) request.urlopen("http://wsprnet.org/post/", data)

View File

@ -6,12 +6,24 @@ try:
from setuptools import find_namespace_packages from setuptools import find_namespace_packages
except ImportError: except ImportError:
from setuptools import PEP420PackageFinder from setuptools import PEP420PackageFinder
find_namespace_packages = PEP420PackageFinder.find find_namespace_packages = PEP420PackageFinder.find
setup( setup(
name="OpenWebRX", name="OpenWebRX",
version=str(looseversion), version=str(looseversion),
packages=find_namespace_packages(include=["owrx", "owrx.source", "owrx.service", "owrx.controllers", "owrx.property", "owrx.form", "csdr", "htdocs"]), packages=find_namespace_packages(
include=[
"owrx",
"owrx.source",
"owrx.service",
"owrx.controllers",
"owrx.property",
"owrx.form",
"csdr",
"htdocs",
]
),
package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]}, package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]},
entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]}, entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]},
url="https://www.openwebrx.de/", url="https://www.openwebrx.de/",

View File

@ -1,6 +1,5 @@
[Unit] [Unit]
Description=OpenWebRX WebSDR receiver Description=OpenWebRX WebSDR receiver
After=multi-user.target
[Service] [Service]
Type=simple Type=simple

View File

@ -4,7 +4,6 @@ from owrx.property import PropertyLayer, PropertyFilter
class PropertyFilterTest(TestCase): class PropertyFilterTest(TestCase):
def testPassesProperty(self): def testPassesProperty(self):
pm = PropertyLayer() pm = PropertyLayer()
pm["testkey"] = "testvalue" pm["testkey"] = "testvalue"