Merge branch 'develop' into pycsdr
This commit is contained in:
commit
4e429d047d
@ -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
|
||||||
|
@ -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"
|
||||||
|
38
csdr/csdr.py
38
csdr/csdr.py
@ -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:
|
||||||
|
@ -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
3
debian/changelog
vendored
@ -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"`)
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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">
|
||||||
|
@ -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 = {
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
@ -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);
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
self.put(PoisonPill)
|
for w in self.workers:
|
||||||
|
if w.is_alive():
|
||||||
|
self.put(PoisonPill)
|
||||||
|
self.join()
|
||||||
|
|
||||||
def put(self, item, **kwars):
|
def put(self, item, **kwars):
|
||||||
self.inCounter.inc()
|
self.inCounter.inc()
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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(
|
||||||
"msg": str(frame),
|
{
|
||||||
"timestamp": frame.timestamp,
|
"type": "js8_message",
|
||||||
"db": frame.db,
|
"value": {
|
||||||
"dt": frame.dt,
|
"msg": str(frame),
|
||||||
"freq": freq + frame.freq,
|
"timestamp": frame.timestamp,
|
||||||
"thread_type": frame.thread_type,
|
"db": frame.db,
|
||||||
"mode": frame.mode
|
"dt": frame.dt,
|
||||||
}})
|
"freq": freq + frame.freq,
|
||||||
|
"thread_type": frame.thread_type,
|
||||||
|
"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(
|
||||||
"google_maps_api_key",
|
pm.filter(
|
||||||
"receiver_gps",
|
"google_maps_api_key",
|
||||||
"map_position_retention_time",
|
"receiver_gps",
|
||||||
"receiver_name",
|
"map_position_retention_time",
|
||||||
).__dict__())
|
"receiver_name",
|
||||||
|
).__dict__()
|
||||||
|
)
|
||||||
|
|
||||||
Map.getSharedInstance().addClient(self)
|
Map.getSharedInstance().addClient(self)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -13,9 +13,9 @@ 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):
|
||||||
@ -41,11 +37,11 @@ class ModificationAwareController(Controller, metaclass=ABCMeta):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def getModified(self, file):
|
def getModified(self, file):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def wasModified(self, file):
|
def wasModified(self, file):
|
||||||
try:
|
try:
|
||||||
modified = self.getModified(file).replace(microsecond=0)
|
modified = self.getModified(file).replace(microsecond=0)
|
||||||
|
|
||||||
if modified is not None and "If-Modified-Since" in self.handler.headers:
|
if modified is not None and "If-Modified-Since" in self.handler.headers:
|
||||||
client_modified = datetime.strptime(
|
client_modified = datetime.strptime(
|
||||||
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
|
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
|
return False
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
@ -547,4 +553,4 @@ class FeatureDetector(object):
|
|||||||
|
|
||||||
You can find more information [here](https://github.com/jketterl/eb200_connector).
|
You can find more information [here](https://github.com/jketterl/eb200_connector).
|
||||||
"""
|
"""
|
||||||
return self._check_connector("eb200_connector")
|
return self._check_connector("eb200_connector")
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
16
owrx/http.py
16
owrx/http.py
@ -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"}),
|
||||||
|
20
owrx/js8.py
20
owrx/js8.py
@ -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,
|
{
|
||||||
"mode": "JS8",
|
"callsign": frame.callsign,
|
||||||
"locator": frame.grid,
|
"mode": "JS8",
|
||||||
"freq": self.dial_freq + frame.freq,
|
"locator": frame.grid,
|
||||||
"db": frame.db,
|
"freq": self.dial_freq + frame.freq,
|
||||||
"timestamp": frame.timestamp,
|
"db": frame.db,
|
||||||
"msg": str(frame)
|
"timestamp": frame.timestamp,
|
||||||
})
|
"msg": str(frame),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("error while parsing js8 message")
|
logger.exception("error while parsing js8 message")
|
||||||
|
34
owrx/kiss.py
34
owrx/kiss.py
@ -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,18 +65,21 @@ 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)
|
||||||
|
|
||||||
config += "\n" + pbeacon + "\n"
|
config += "\n" + pbeacon + "\n"
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@ -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:
|
||||||
|
@ -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",
|
||||||
|
@ -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):
|
||||||
fields = raw.decode("ascii", "replace").rstrip("\n").split(";")
|
try:
|
||||||
meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""}
|
fields = raw.decode("ascii", "replace").rstrip("\n").split(";")
|
||||||
if "address" in meta:
|
meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""}
|
||||||
meta["address"] = int(meta["address"])
|
if "address" in meta:
|
||||||
self.handler.write_pocsag_data(meta)
|
meta["address"] = int(meta["address"])
|
||||||
|
self.handler.write_pocsag_data(meta)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Exception while parsing Pocsag message")
|
||||||
|
@ -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]
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -15,18 +15,22 @@ class ConnectorSource(SdrSource):
|
|||||||
super().__init__(id, props)
|
super().__init__(id, props)
|
||||||
|
|
||||||
def getCommandMapper(self):
|
def getCommandMapper(self):
|
||||||
return super().getCommandMapper().setMappings(
|
return (
|
||||||
{
|
super()
|
||||||
"samp_rate": Option("-s"),
|
.getCommandMapper()
|
||||||
"tuner_freq": Option("-f"),
|
.setMappings(
|
||||||
"port": Option("-p"),
|
{
|
||||||
"controlPort": Option("-c"),
|
"samp_rate": Option("-s"),
|
||||||
"device": Option("-d"),
|
"tuner_freq": Option("-f"),
|
||||||
"iqswap": Flag("-i"),
|
"port": Option("-p"),
|
||||||
"rtltcp_compat": Option("-r"),
|
"controlPort": Option("-c"),
|
||||||
"ppm": Option("-P"),
|
"device": Option("-d"),
|
||||||
"rf_gain": Option("-g"),
|
"iqswap": Flag("-i"),
|
||||||
}
|
"rtltcp_compat": Option("-r"),
|
||||||
|
"ppm": Option("-P"),
|
||||||
|
"rf_gain": Option("-g"),
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def sendControlMessage(self, changes):
|
def sendControlMessage(self, changes):
|
||||||
|
@ -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,
|
"nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1"
|
||||||
nmux_bufcnt,
|
% (
|
||||||
self.port,
|
nmux_bufsize,
|
||||||
)]
|
nmux_bufcnt,
|
||||||
|
self.port,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def getCommand(self):
|
def getCommand(self):
|
||||||
return super().getCommand() + self.getFormatConversion() + self.getNmuxCommand()
|
return super().getCommand() + self.getFormatConversion() + self.getNmuxCommand()
|
||||||
|
@ -8,8 +8,10 @@ class Eb200Source(ConnectorSource):
|
|||||||
super()
|
super()
|
||||||
.getCommandMapper()
|
.getCommandMapper()
|
||||||
.setBase("eb200_connector")
|
.setBase("eb200_connector")
|
||||||
.setMappings({
|
.setMappings(
|
||||||
"long": Flag("-l"),
|
{
|
||||||
"remote": Argument(),
|
"long": Flag("-l"),
|
||||||
})
|
"remote": Argument(),
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
@ -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:
|
||||||
|
@ -8,4 +8,4 @@ class HackrfSource(SoapyConnectorSource):
|
|||||||
return mappings
|
return mappings
|
||||||
|
|
||||||
def getDriver(self):
|
def getDriver(self):
|
||||||
return "hackrf"
|
return "hackrf"
|
||||||
|
@ -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 (
|
||||||
@ -24,10 +25,11 @@ class HpsdrSource(ConnectorSource):
|
|||||||
.getCommandMapper()
|
.getCommandMapper()
|
||||||
.setBase("hpsdrconnector")
|
.setBase("hpsdrconnector")
|
||||||
.setMappings(
|
.setMappings(
|
||||||
{
|
{
|
||||||
"tuner_freq": Option("--frequency"),
|
"tuner_freq": Option("--frequency"),
|
||||||
"samp_rate": Option("--samplerate"),
|
"samp_rate": Option("--samplerate"),
|
||||||
"remote": Option("--radio"),
|
"remote": Option("--radio"),
|
||||||
"rf_gain": Option("--gain"),
|
"rf_gain": Option("--gain"),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
@ -17,15 +17,21 @@ 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()
|
||||||
"samp_rate": Option("-s"),
|
.getCommandMapper()
|
||||||
"tuner_freq": Option("-f"),
|
.setBase("perseustest -p -d -1 -a -t 0 -o - ")
|
||||||
"attenuator": Option("-u"),
|
.setMappings(
|
||||||
"adc_preamp": Option("-m"),
|
{
|
||||||
"adc_dither": Option("-x"),
|
"samp_rate": Option("-s"),
|
||||||
"wideband": Option("-w"),
|
"tuner_freq": Option("-f"),
|
||||||
}
|
"attenuator": Option("-u"),
|
||||||
|
"adc_preamp": Option("-m"),
|
||||||
|
"adc_dither": Option("-x"),
|
||||||
|
"wideband": Option("-w"),
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
@ -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"),
|
{
|
||||||
"direct_sampling": Option("-e"),
|
"bias_tee": Flag("-b"),
|
||||||
"remote": Argument(),
|
"direct_sampling": Option("-e"),
|
||||||
})
|
"remote": Argument(),
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
@ -5,11 +5,16 @@ 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()
|
||||||
"antenna": Option("-a"),
|
.getCommandMapper()
|
||||||
"soapy_settings": Option("-t"),
|
.setBase("soapy_connector")
|
||||||
}
|
.setMappings(
|
||||||
|
{
|
||||||
|
"antenna": Option("-a"),
|
||||||
|
"soapy_settings": Option("-t"),
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
|
@ -99,7 +99,7 @@ class Ft4Profile(WsjtProfile):
|
|||||||
|
|
||||||
|
|
||||||
class Fst4Profile(WsjtProfile):
|
class Fst4Profile(WsjtProfile):
|
||||||
availableIntervals = [15, 30, 60, 120, 300, 900, 1800]
|
availableIntervals = [15, 30, 60, 120, 300, 900, 1800]
|
||||||
|
|
||||||
def __init__(self, interval):
|
def __init__(self, interval):
|
||||||
self.interval = interval
|
self.interval = interval
|
||||||
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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
|
# 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",
|
{
|
||||||
"date": date.strftime("%y%m%d"),
|
"function": "wspr",
|
||||||
"time": date.strftime("%H%M"),
|
"date": date.strftime("%y%m%d"),
|
||||||
"sig": spot["db"],
|
"time": date.strftime("%H%M"),
|
||||||
"dt": spot["dt"],
|
"sig": spot["db"],
|
||||||
# FST4W does not have drift
|
"dt": spot["dt"],
|
||||||
"drift": spot["drift"] if "drift" in spot else 0,
|
# FST4W does not have drift
|
||||||
"tqrg": spot["freq"] / 1E6,
|
"drift": spot["drift"] if "drift" in spot else 0,
|
||||||
"tcall": spot["callsign"],
|
"tqrg": spot["freq"] / 1e6,
|
||||||
"tgrid": spot["locator"],
|
"tcall": spot["callsign"],
|
||||||
"dbm": spot["dbm"],
|
"tgrid": spot["locator"],
|
||||||
"version": openwebrx_version,
|
"dbm": spot["dbm"],
|
||||||
"rcall": self.callsign,
|
"version": openwebrx_version,
|
||||||
"rgrid": self.locator,
|
"rcall": self.callsign,
|
||||||
# mode 2 = WSPR 2 minutes
|
"rgrid": self.locator,
|
||||||
"mode": self._getMode(spot)
|
# mode 2 = WSPR 2 minutes
|
||||||
}).encode()
|
"mode": self._getMode(spot),
|
||||||
|
}
|
||||||
|
).encode()
|
||||||
request.urlopen("http://wsprnet.org/post/", data)
|
request.urlopen("http://wsprnet.org/post/", data)
|
||||||
|
|
||||||
|
|
||||||
|
14
setup.py
14
setup.py
@ -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/",
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=OpenWebRX WebSDR receiver
|
Description=OpenWebRX WebSDR receiver
|
||||||
After=multi-user.target
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user