Merge branch 'develop' into pycsdr

This commit is contained in:
Jakob Ketterl 2021-01-17 18:16:32 +01:00
commit 297d6b540d
32 changed files with 758 additions and 346 deletions

View File

@ -3,6 +3,7 @@
- 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 devices supported: - New devices supported:
- HPSDR devices (Hermes Lite 2) - HPSDR devices (Hermes Lite 2)
- BBRF103 / RX666 / RX888 devices supported by libsddc - BBRF103 / RX666 / RX888 devices supported by libsddc

View File

@ -361,7 +361,7 @@ aprs_symbols_path = "/usr/share/aprs-symbols/png"
# Antenna direction (N, NE, E, SE, S, SW, W, NW). Omnidirectional by default # Antenna direction (N, NE, E, SE, S, SW, W, NW). Omnidirectional by default
# aprs_igate_dir = "NE" # aprs_igate_dir = "NE"
# === PSK Reporter setting === # === PSK Reporter settings ===
# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info # enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator # this also uses the receiver_gps setting from above, so make sure it contains a correct locator
pskreporter_enabled = False pskreporter_enabled = False
@ -369,6 +369,12 @@ pskreporter_callsign = "N0CALL"
# optional antenna information, uncomment to enable # optional antenna information, uncomment to enable
#pskreporter_antenna_information = "Dipole" #pskreporter_antenna_information = "Dipole"
# === WSPRNet reporting settings
# enable this if you want to upload WSPR spots to wsprnet.ort
# in addition to these settings also make sure that receiver_gps contains your correct location
wsprnet_enabled = False
wsprnet_callsign = "N0CALL"
# === Web admin settings === # === Web admin settings ===
# this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote # this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote
# changes to the receiver settings. enable for testing in controlled environment only. # changes to the receiver settings. enable for testing in controlled environment only.

View File

@ -556,12 +556,12 @@ class dsp(object):
# 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 5 is by experimentation only, with a minimum audio rate of 36kHz (enforced by the client) # the factor of 6 is by experimentation only, with a minimum audio rate of 36kHz (enforced by the client)
# this allows us to cover at least +/- 80kHz of frequency spectrum (may be higher, but that's the worst case). # this allows us to cover at least +/- 108kHz of frequency spectrum (may be higher, but that's the worst case).
# the correction factor is automatically compensated for by the secondary decimation stage, which comes # the correction factor is automatically compensated for by the secondary decimation stage, which comes
# after the demodulator. # after the demodulator.
if self.get_demodulator() == "wfm": if self.get_demodulator() == "wfm":
correction = 5 correction = 6
while input_rate / (decimation + 1) >= output_rate * correction: 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
@ -933,6 +933,3 @@ class dsp(object):
return return
self.stop() self.stop()
self.start() self.start()
def __del__(self):
self.stop()

2
debian/changelog vendored
View File

@ -7,6 +7,8 @@ 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
wsprnet.org
* 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"`)

2
debian/control vendored
View File

@ -11,6 +11,6 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git
Package: openwebrx Package: openwebrx
Architecture: all Architecture: all
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.17), netcat, owrx-connector (>= 0.4), soapysdr-tools, python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends} Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.17), netcat, owrx-connector (>= 0.4), soapysdr-tools, python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends}
Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, eb200-connector, hpsdrconnector, aprs-symbols Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, eb200-connector, hpsdrconnector, aprs-symbols, m17-demod
Description: multi-user web sdr Description: multi-user web sdr
Open source, multi-user SDR receiver with a web interface Open source, multi-user SDR receiver with a web interface

View File

@ -25,12 +25,19 @@ ul {
padding-inline-start: 25px; padding-inline-start: 25px;
} }
/* don't show the filter in it's initial position */
.openwebrx-map-legend { .openwebrx-map-legend {
display: none;
background-color: #fff; background-color: #fff;
padding: 10px; padding: 10px;
margin: 10px; margin: 10px;
} }
/* show it as soon as google maps has moved it to its container */
.openwebrx-map .openwebrx-map-legend {
display: block;
}
.openwebrx-map-legend ul { .openwebrx-map-legend ul {
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;

View File

@ -1,32 +1,36 @@
#webrx-top-container #webrx-top-container {
{
position: relative; position: relative;
z-index:1000; z-index:1000;
background-color: #575757; background-color: #575757;
}
#webrx-top-photo background-image: url(../gfx/openwebrx-top-photo.jpg);
{ background-position-x: center;
width: 100%; background-position-y: top;
display: block; background-repeat: no-repeat;
} background-size: cover;
#webrx-top-photo-clip
{
min-height: 67px;
max-height: 67px;
height: 350px;
overflow: hidden; overflow: hidden;
position: relative;
} }
.webrx-top-bar-parts #openwebrx-description-container {
{ transition-property: height, opacity;
transition-duration: 1s;
transition-timing-function: ease-out;
opacity: 0;
height: 0;
/* originally, top-bar + description was 350px */
max-height: 283px;
overflow: hidden;
}
#openwebrx-description-container.expanded {
opacity: 1;
height: 283px;
}
.webrx-top-bar-parts {
height:67px; height:67px;
}
#webrx-top-bar
{
background: rgba(128, 128, 128, 0.15); background: rgba(128, 128, 128, 0.15);
margin:0; margin:0;
padding:0; padding:0;
@ -37,31 +41,28 @@
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
overflow: hidden; overflow: hidden;
position: absolute;
left: 0; display: flex;
top: 0; flex-direction: row;
right: 0;
} }
#webrx-tob-container, #webrx-top-container * { .webrx-top-bar-parts > * {
flex: 0;
}
#webrx-top-container, #webrx-top-container * {
line-height: initial; line-height: initial;
box-sizing: initial; box-sizing: initial;
} }
#webrx-top-container img { #webrx-top-logo {
vertical-align: initial;
}
#webrx-top-logo
{
padding: 12px; padding: 12px;
float: left; /* overwritten by media queries */
display: none;
} }
#webrx-rx-avatar #webrx-rx-avatar {
{
background-color: rgba(154, 154, 154, .5); background-color: rgba(154, 154, 154, .5);
float: left;
margin: 7px; margin: 7px;
cursor:pointer; cursor:pointer;
@ -73,49 +74,45 @@
} }
#webrx-rx-texts { #webrx-rx-texts {
float: left; /* minimum layout width */
padding: 10px; width: 0;
/* will be getting wider with flex */
flex: 1;
overflow: hidden;
} }
#webrx-rx-texts div { #webrx-rx-texts div {
margin: 0 10px;
padding: 3px; padding: 3px;
}
#webrx-rx-title
{
white-space:nowrap; white-space:nowrap;
overflow: hidden; overflow: hidden;
cursor:pointer; cursor:pointer;
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
color: #909090; color: #909090;
}
#webrx-rx-texts div:first-child {
margin-top: 10px;
}
#webrx-rx-title {
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
font-size: 11pt; font-size: 11pt;
font-weight: bold; font-weight: bold;
} }
#webrx-rx-desc #webrx-rx-desc {
{
white-space:nowrap;
overflow: hidden;
cursor:pointer;
font-size: 10pt; font-size: 10pt;
color: #909090;
} }
#webrx-rx-desc a #openwebrx-rx-details-arrow {
{
color: #909090;
}
#openwebrx-rx-details-arrow
{
cursor:pointer; cursor:pointer;
position: absolute; position: absolute;
left: 470px; bottom: 0;
top: 55px; left: 50%;
transform: translate(-50%, 0);
} }
#openwebrx-rx-details-arrow a #openwebrx-rx-details-arrow a {
{
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 0; line-height: 0;
@ -128,6 +125,11 @@
cursor:pointer; cursor:pointer;
} }
#openwebrx-main-buttons .button[data-toggle-panel] {
/* will be enabled by javascript if the panel is present in the DOM */
display: none;
}
#openwebrx-main-buttons .button img { #openwebrx-main-buttons .button img {
height: 38px; height: 38px;
} }
@ -137,23 +139,19 @@
text-decoration: inherit; text-decoration: inherit;
} }
#openwebrx-main-buttons .button:hover #openwebrx-main-buttons .button:hover {
{
background-color: rgba(255, 255, 255, 0.3); background-color: rgba(255, 255, 255, 0.3);
} }
#openwebrx-main-buttons .button:active #openwebrx-main-buttons .button:active {
{
background-color: rgba(255, 255, 255, 0.55); background-color: rgba(255, 255, 255, 0.55);
} }
#openwebrx-main-buttons #openwebrx-main-buttons {
{
padding: 5px 15px; padding: 5px 15px;
display: flex; display: flex;
list-style: none; list-style: none;
float: right;
margin:0; margin:0;
color: white; color: white;
text-shadow: 0px 0px 4px #000000; text-shadow: 0px 0px 4px #000000;
@ -162,23 +160,17 @@
font-weight: bold; font-weight: bold;
} }
#webrx-rx-photo-title #webrx-rx-photo-title {
{ margin: 10px 15px;
position: absolute; color: white;
left: 15px;
top: 78px;
color: White;
font-size: 16pt; font-size: 16pt;
text-shadow: 1px 1px 4px #444; text-shadow: 1px 1px 4px #444;
opacity: 1; opacity: 1;
} }
#webrx-rx-photo-desc #webrx-rx-photo-desc {
{ margin: 10px 15px;
position: absolute; color: white;
left: 15px;
top: 109px;
color: White;
font-size: 10pt; font-size: 10pt;
font-weight: bold; font-weight: bold;
text-shadow: 0px 0px 6px #444; text-shadow: 0px 0px 6px #444;
@ -186,12 +178,37 @@
line-height: 1.5em; line-height: 1.5em;
} }
#webrx-rx-photo-desc a #webrx-rx-photo-desc a {
{
color: #5ca8ff; color: #5ca8ff;
text-shadow: none; text-shadow: none;
} }
/*
* Responsive stuff
*/
@media (min-width: 576px) {
#webrx-rx-texts {
display: initial;
}
}
@media (min-width: 768px) {
}
@media (min-width: 992px) {
#webrx-top-logo {
display: initial;
}
}
@media (min-width: 1200px) {
}
/*
* Sprites (images)
*/
.sprite-panel-status { .sprite-panel-status {
background-position: 0 0; background-position: 0 0;
width: 44px; width: 44px;

View File

@ -573,9 +573,10 @@ img.openwebrx-mirror-img
.openwebrx-progressbar-text .openwebrx-progressbar-text
{ {
position: absolute; position: absolute;
left:0px; left:50;
top:4px; top:50%;
width: inherit; transform: translate(-50%, -50%);
white-space: nowrap;
z-index: 1; z-index: 1;
} }
@ -907,18 +908,30 @@ img.openwebrx-mirror-img
border-color: Red; border-color: Red;
} }
.openwebrx-meta-panel {
display: flex;
flex-direction: row;
gap: 10px;
}
.openwebrx-meta-slot { .openwebrx-meta-slot {
flex: 1;
width: 145px; width: 145px;
height: 196px; height: 196px;
float: left;
margin-right: 10px;
background-color: #676767; background-color: #676767;
padding: 2px 0; padding: 2px 0;
color: #333; color: #333;
text-align: center; text-align: center;
position: relative;
display: flex;
flex-direction: column;
}
.openwebrx-meta-slot > * {
flex: 0;
flex-basis: 1.125em;
} }
.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before { .openwebrx-meta-slot, .openwebrx-meta-slot.muted:before {
@ -960,13 +973,8 @@ img.openwebrx-mirror-img
box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px; box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px;
} }
.openwebrx-meta-slot:last-child {
margin-right: 0;
}
.openwebrx-meta-slot .openwebrx-meta-user-image { .openwebrx-meta-slot .openwebrx-meta-user-image {
width:100%; flex: 1;
height:133px;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
@ -1233,12 +1241,12 @@ img.openwebrx-mirror-img
height: 15px; height: 15px;
} }
#openwebrx-mute-on .sprite-speaker { .openwebrx-mute-button .sprite-speaker {
background-position: -117px -38px; background-position: -103px -38px;
} }
#openwebrx-mute-off .sprite-speaker { .openwebrx-mute-button.muted .sprite-speaker {
background-position: -103px -38px; background-position: -117px -38px;
} }
.sprite-squelch { .sprite-squelch {

View File

@ -1,26 +1,25 @@
<div id="webrx-top-container"> <div id="webrx-top-container">
<div id="webrx-top-photo-clip"> <div id="webrx-top-bar" class="webrx-top-bar-parts">
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo" alt="Receiver panorama"/> <a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a>
<div id="webrx-top-bar" class="webrx-top-bar-parts"> <img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
<a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a> <div id="webrx-rx-texts">
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/> <div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
<div id="webrx-rx-texts"> <div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
</div>
<div id="openwebrx-rx-details-arrow">
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger" style="display: none;"><span class="sprite sprite-rx-details-arrow-up"></span></a>
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><span class="sprite sprite-rx-details-arrow-down"></span></a>
</div>
<section id="openwebrx-main-buttons">
<div class="button" data-toggle-panel="openwebrx-panel-status"><span class="sprite sprite-panel-status"></span><br/>Status</div>
<div class="button" data-toggle-panel="openwebrx-panel-log"><span class="sprite sprite-panel-log"></span><br/>Log</div>
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><span class="sprite sprite-panel-receiver"></span><br/>Receiver</div>
<a class="button" href="map" target="openwebrx-map"><span class="sprite sprite-panel-map"></span><br/>Map</a>
${settingslink}
</section>
</div> </div>
<section id="openwebrx-main-buttons">
<div class="button" data-toggle-panel="openwebrx-panel-status"><span class="sprite sprite-panel-status"></span><br/>Status</div>
<div class="button" data-toggle-panel="openwebrx-panel-log"><span class="sprite sprite-panel-log"></span><br/>Log</div>
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><span class="sprite sprite-panel-receiver"></span><br/>Receiver</div>
<a class="button" href="map" target="openwebrx-map"><span class="sprite sprite-panel-map"></span><br/>Map</a>
${settingslink}
</section>
</div>
<div id="openwebrx-description-container">
<div id="webrx-rx-photo-title"></div> <div id="webrx-rx-photo-title"></div>
<div id="webrx-rx-photo-desc"></div> <div id="webrx-rx-photo-desc"></div>
</div> </div>
<div id="openwebrx-rx-details-arrow">
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger" style="display: none;"><span class="sprite sprite-rx-details-arrow-up"></span></a>
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><span class="sprite sprite-rx-details-arrow-down"></span></a>
</div>
</div> </div>

View File

@ -65,32 +65,28 @@
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"></div> <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"></div> <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"></div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" style="display: none;" data-panel-name="metadata-ysf"> <div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" style="display: none;" data-panel-name="metadata-ysf">
<div class="openwebrx-meta-frame"> <div class="openwebrx-meta-slot">
<div class="openwebrx-meta-slot"> <div class="openwebrx-ysf-mode"></div>
<div class="openwebrx-ysf-mode openwebrx-meta-autoclear"></div> <div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-meta-user-image"></div> <div class="openwebrx-ysf-source"><span class="location"></span><span class="callsign"></span></div>
<div class="openwebrx-ysf-source openwebrx-meta-autoclear"></div> <div class="openwebrx-ysf-up"></div>
<div class="openwebrx-ysf-up openwebrx-meta-autoclear"></div> <div class="openwebrx-ysf-down"></div>
<div class="openwebrx-ysf-down openwebrx-meta-autoclear"></div>
</div>
</div> </div>
</div> </div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" style="display: none;" data-panel-name="metadata-dmr"> <div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" style="display: none;" data-panel-name="metadata-dmr">
<div class="openwebrx-meta-frame"> <div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel"> <div class="openwebrx-dmr-slot">Timeslot 1</div>
<div class="openwebrx-dmr-slot">Timeslot 1</div> <div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-meta-user-image"></div> <div class="openwebrx-dmr-id"></div>
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div> <div class="openwebrx-dmr-name"></div>
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div> <div class="openwebrx-dmr-target"></div>
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div> </div>
</div> <div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel"> <div class="openwebrx-dmr-slot">Timeslot 2</div>
<div class="openwebrx-dmr-slot">Timeslot 2</div> <div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-meta-user-image"></div> <div class="openwebrx-dmr-id"></div>
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div> <div class="openwebrx-dmr-name"></div>
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div> <div class="openwebrx-dmr-target"></div>
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
</div>
</div> </div>
</div> </div>
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" style="width: 619px;"> <div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" style="width: 619px;">
@ -132,7 +128,7 @@
</div> </div>
<div class="openwebrx-modes openwebrx-panel-line"></div> <div class="openwebrx-modes openwebrx-panel-line"></div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><span class="sprite sprite-speaker openwebrx-sliderbtn-img"></span></div> <div title="Mute on/off" class="openwebrx-button openwebrx-mute-button" onclick="toggleMute();"><span class="sprite sprite-speaker openwebrx-sliderbtn-img"></span></div>
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()"> <input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
<div title="Auto-adjust waterfall colors (right-click for continuous)" id="openwebrx-waterfall-colors-auto" class="openwebrx-button"><span class="sprite sprite-waterfall-auto openwebrx-sliderbtn-img"></span></div> <div title="Auto-adjust waterfall colors (right-click for continuous)" id="openwebrx-waterfall-colors-auto" class="openwebrx-button"><span class="sprite sprite-waterfall-auto openwebrx-sliderbtn-img"></span></div>
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()"> <input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">

View File

@ -14,7 +14,19 @@ function AudioEngine(maxBufferLength, audioReporter) {
this.onStartCallbacks = []; this.onStartCallbacks = [];
this.started = false; this.started = false;
this.audioContext = new ctx(); // try common working sample rates
if (![48000, 44100].some(function(sr) {
try {
this.audioContext = new ctx({sampleRate: sr});
return true;
} catch (e) {
return false;
}
}, this)) {
// fallback: let the browser decide
// this may cause playback problems down the line
this.audioContext = new ctx();
}
var me = this; var me = this;
this.audioContext.onstatechange = function() { this.audioContext.onstatechange = function() {
if (me.audioContext.state !== 'running') return; if (me.audioContext.state !== 'running') return;

View File

@ -8,7 +8,7 @@ Filter.prototype.getLimits = function() {
if (this.demodulator.get_secondary_demod() === 'pocsag') { if (this.demodulator.get_secondary_demod() === 'pocsag') {
max_bw = 12500; max_bw = 12500;
} else if (this.demodulator.get_modulation() === 'wfm') { } else if (this.demodulator.get_modulation() === 'wfm') {
max_bw = 80000; max_bw = 100000;
} else if (this.demodulator.get_modulation() === 'drm') { } else if (this.demodulator.get_modulation() === 'drm') {
max_bw = 100000; max_bw = 100000;
} else if (this.demodulator.get_secondary_demod() === 'packet') { } else if (this.demodulator.get_secondary_demod() === 'packet') {
@ -236,7 +236,7 @@ Demodulator.prototype.emit = function(event, params) {
}; };
Demodulator.prototype.set_offset_frequency = function(to_what) { Demodulator.prototype.set_offset_frequency = function(to_what) {
if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return; if (typeof(to_what) == 'undefined' || to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
to_what = Math.round(to_what); to_what = Math.round(to_what);
if (this.offset_frequency === to_what) { if (this.offset_frequency === to_what) {
return; return;

View File

@ -165,10 +165,13 @@ DemodulatorPanel.prototype.updatePanels = function() {
modulation = this.getDemodulator().get_modulation(); modulation = this.getDemodulator().get_modulation();
var showing = 'openwebrx-panel-metadata-' + modulation; var showing = 'openwebrx-panel-metadata-' + modulation;
$(".openwebrx-meta-panel").each(function (_, p) { var metaPanels = $(".openwebrx-meta-panel");
metaPanels.each(function (_, p) {
toggle_panel(p.id, p.id === showing); toggle_panel(p.id, p.id === showing);
}); });
clear_metadata(); metaPanels.metaPanel().each(function() {
this.clear();
});
}; };
DemodulatorPanel.prototype.getDemodulator = function() { DemodulatorPanel.prototype.getDemodulator = function() {
@ -181,7 +184,7 @@ DemodulatorPanel.prototype.collectParams = function() {
squelch_level: -150, squelch_level: -150,
mod: 'nfm' mod: 'nfm'
} }
return $.extend(new Object(), defaults, this.initialParams, this.transformHashParams(this.parseHash())); return $.extend(new Object(), defaults, this.validateInitialParams(this.initialParams), this.transformHashParams(this.parseHash()));
}; };
DemodulatorPanel.prototype.startDemodulator = function() { DemodulatorPanel.prototype.startDemodulator = function() {
@ -287,7 +290,7 @@ DemodulatorPanel.prototype.validateHash = function(params) {
var self = this; var self = this;
params = Object.keys(params).filter(function(key) { params = Object.keys(params).filter(function(key) {
if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') { if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') {
return params.freq && Math.abs(params.freq - self.center_freq) < bandwidth / 2; return params.freq && Math.abs(params.freq - self.center_freq) <= bandwidth / 2;
} }
return true; return true;
}).reduce(function(p, key) { }).reduce(function(p, key) {
@ -303,6 +306,17 @@ DemodulatorPanel.prototype.validateHash = function(params) {
return params; return params;
}; };
DemodulatorPanel.prototype.validateInitialParams = function(params) {
return Object.fromEntries(
Object.entries(params).filter(function(a) {
if (a[0] == "offset_frequency") {
return Math.abs(a[1]) <= bandwidth / 2;
}
return true;
})
);
};
DemodulatorPanel.prototype.updateHash = function() { DemodulatorPanel.prototype.updateHash = function() {
var demod = this.getDemodulator(); var demod = this.getDemodulator();
if (!demod) return; if (!demod) return;

View File

@ -1,7 +1,12 @@
function Header(el) { function Header(el) {
this.el = el; this.el = el;
this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () { var $buttons = this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').filter(function(){
// ignore buttons when the corresponding panel is not in the DOM
return $('#' + $(this).data('toggle-panel'))[0];
});
$buttons.css({display: 'block'}).click(function () {
toggle_panel($(this).data('toggle-panel')); toggle_panel($(this).data('toggle-panel'));
}); });
@ -30,18 +35,14 @@ Header.prototype.init_rx_photo = function() {
Header.prototype.close_rx_photo = function() { Header.prototype.close_rx_photo = function() {
this.rx_photo_state = 0; this.rx_photo_state = 0;
this.el.find("#webrx-rx-photo-desc").animate({opacity: 0}); this.el.find('#openwebrx-description-container').removeClass('expanded');
this.el.find("#webrx-rx-photo-title").animate({opacity: 0});
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'});
this.el.find("#openwebrx-rx-details-arrow-down").show(); this.el.find("#openwebrx-rx-details-arrow-down").show();
this.el.find("#openwebrx-rx-details-arrow-up").hide(); this.el.find("#openwebrx-rx-details-arrow-up").hide();
} }
Header.prototype.open_rx_photo = function() { Header.prototype.open_rx_photo = function() {
this.rx_photo_state = 1; this.rx_photo_state = 1;
this.el.find("#webrx-rx-photo-desc").animate({opacity: 1}); this.el.find('#openwebrx-description-container').addClass('expanded');
this.el.find("#webrx-rx-photo-title").animate({opacity: 1});
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 350}, {duration: 1000, easing: 'easeOutCubic'});
this.el.find("#openwebrx-rx-details-arrow-down").hide(); this.el.find("#openwebrx-rx-details-arrow-down").hide();
this.el.find("#openwebrx-rx-details-arrow-up").show(); this.el.find("#openwebrx-rx-details-arrow-up").show();
} }

View File

@ -78,14 +78,14 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) {
return $('<div/>').text(input).html() return $('<div/>').text(input).html()
}; };
if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'FST4W'].indexOf(msg['mode']) >= 0) { if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4'].indexOf(msg['mode']) >= 0) {
matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/);
if (matches && matches[2] !== 'RR73') { if (matches && matches[2] !== 'RR73') {
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>'; linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
} else { } else {
linkedmsg = html_escape(linkedmsg); linkedmsg = html_escape(linkedmsg);
} }
} else if (msg['mode'] === 'WSPR') { } else if (['WSPR', 'FST4W'].indexOf(msg['mode']) >= 0) {
matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/); matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/);
if (matches) { if (matches) {
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + html_escape(matches[3]); linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + html_escape(matches[3]);

205
htdocs/lib/MetaPanel.js Normal file
View File

@ -0,0 +1,205 @@
function MetaPanel(el) {
this.el = el;
this.modes = [];
}
MetaPanel.prototype.update = function(data) {
};
MetaPanel.prototype.isSupported = function(data) {
return this.modes.includes(data.protocol);
};
MetaPanel.prototype.clear = function() {
this.el.find(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
};
function DmrMetaSlot(el) {
this.el = $(el);
this.clear();
}
DmrMetaSlot.prototype.update = function(data) {
this.el[data['sync'] ? "addClass" : "removeClass"]("sync");
if (data['sync'] && data['sync'] === "voice") {
this.setId(data['additional'] && data['additional']['callsign'] || data['source']);
this.setName(data['additional'] && data['additional']['fname']);
if (data['type'] === "group") {
this.setTalkgroup(data['target']);
}
if (data['type'] === "direct") {
this.setDirect(data['target']);
}
this.el.addClass("active");
} else {
this.clear();
}
};
DmrMetaSlot.prototype.setId = function(id) {
if (this.id === id) return;
this.id = id;
this.el.find('.openwebrx-dmr-id').text(id || '');
}
DmrMetaSlot.prototype.setName = function(name) {
if (this.name === name) return;
this.name = name;
this.el.find('.openwebrx-dmr-name').text(name || '');
};
DmrMetaSlot.prototype.setTalkgroup = function(talkgroup) {
if (this.talkgroup === talkgroup && this.targetMode === 'talkgroup') return;
this.talkgroup = talkgroup;
this.targetMode = 'talkgroup';
var text = '';
if (talkgroup && talkgroup != '') {
text = 'Talkgroup: ' + talkgroup;
}
this.el.find('.openwebrx-dmr-target').text(text);
this.el.find(".openwebrx-meta-user-image").addClass("group");
};
DmrMetaSlot.prototype.setDirect = function(call) {
if (this.call === call && this.targetMode === 'direct') return;
this.call = call;
this.targetMode = 'direct';
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() {
this.setId();
this.setName();
this.setTalkgroup();
this.setDirect();
this.el.removeClass("active");
};
function DmrMetaPanel(el) {
MetaPanel.call(this, el);
this.modes = ['DMR'];
this.slots = this.el.find('.openwebrx-meta-slot').toArray().map(function(el){
return new DmrMetaSlot(el);
});
}
DmrMetaPanel.prototype = new MetaPanel();
DmrMetaPanel.prototype.update = function(data) {
if (!this.isSupported(data)) return;
if (data['slot']) {
var slot = this.slots[data['slot']];
slot.update(data);
} else {
this.clear();
}
}
DmrMetaPanel.prototype.clear = function() {
MetaPanel.prototype.clear.call(this);
this.el.find(".openwebrx-dmr-timeslot-panel").removeClass("muted");
this.slots.forEach(function(slot) {
slot.clear();
});
};
function YsfMetaPanel(el) {
MetaPanel.call(this, el);
this.modes = ['YSF'];
this.clear();
}
YsfMetaPanel.prototype = new MetaPanel();
YsfMetaPanel.prototype.update = function(data) {
if (!this.isSupported(data)) return;
this.setMode(data['mode']);
if (data['mode'] && data['mode'] !== "") {
this.setSource(data['source']);
this.setLocation(data['lat'], data['lon'], data['source']);
this.setUp(data['up']);
this.setDown(data['down']);
this.el.find(".openwebrx-meta-slot").addClass("active");
} else {
this.clear();
}
};
YsfMetaPanel.prototype.clear = function() {
MetaPanel.prototype.clear.call(this);
this.setMode();
this.setSource();
this.setLocation();
this.setUp();
this.setDown();
};
YsfMetaPanel.prototype.setMode = function(mode) {
if (this.mode === mode) return;
this.mode = mode;
var text = '';
if (mode && mode != '') {
text = 'Mode: ' + mode;
}
this.el.find('.openwebrx-ysf-mode').text(text);
};
YsfMetaPanel.prototype.setSource = function(source) {
if (this.source === source) return;
this.source = source;
this.el.find('.openwebrx-ysf-source .callsign').text(source || '');
};
YsfMetaPanel.prototype.setLocation = function(lat, lon, callsign) {
var hasLocation = lat && lon && callsign && callsign != '';
if (hasLocation === this.hasLocation && this.callsign === callsign) return;
this.hasLocation = hasLocation; this.callsign = callsign;
var html = '';
if (hasLocation) {
html = '<a class="openwebrx-maps-pin" href="map?callsign=' + callsign + '" target="_blank"></a>';
}
this.el.find('.openwebrx-ysf-source .location').html(html);
};
YsfMetaPanel.prototype.setUp = function(up) {
if (this.up === up) return;
this.up = up;
var text = '';
if (up && up != '') {
text = 'Up: ' + up;
}
this.el.find('.openwebrx-ysf-up').text(text);
};
YsfMetaPanel.prototype.setDown = function(down) {
if (this.down === down) return;
this.down = down;
var text = '';
if (down && down != '') {
text = 'Down: ' + down;
}
this.el.find('.openwebrx-ysf-down').text(text);
}
MetaPanel.types = {
dmr: DmrMetaPanel,
ysf: YsfMetaPanel
};
$.fn.metaPanel = function() {
return this.map(function() {
var $self = $(this);
if (!$self.data('metapanel')) {
var matches = /^openwebrx-panel-metadata-([a-z]+)$/.exec($self.prop('id'));
var constructor = matches && MetaPanel.types[matches[1]] || MetaPanel;
$self.data('metapanel', new constructor($self));
}
return $self.data('metapanel');
});
};

View File

@ -2,6 +2,9 @@
<html> <html>
<head> <head>
<title>OpenWebRX Map</title> <title>OpenWebRX Map</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#222" />
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<script src="compiled/map.js"></script> <script src="compiled/map.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>

View File

@ -32,26 +32,20 @@ var fft_codec;
var waterfall_setup_done = 0; var waterfall_setup_done = 0;
var secondary_fft_size; var secondary_fft_size;
function e(what) {
return document.getElementById(what);
}
function updateVolume() { function updateVolume() {
audioEngine.setVolume(parseFloat(e("openwebrx-panel-volume").value) / 100); audioEngine.setVolume(parseFloat($("#openwebrx-panel-volume").val()) / 100);
} }
function toggleMute() { function toggleMute() {
if (mute) { var $muteButton = $('.openwebrx-mute-button');
mute = false; var $volumePanel = $('#openwebrx-panel-volume');
e("openwebrx-mute-on").id = "openwebrx-mute-off"; if ($muteButton.hasClass('muted')) {
e("openwebrx-panel-volume").disabled = false; $muteButton.removeClass('muted');
e("openwebrx-panel-volume").value = volumeBeforeMute; $volumePanel.prop('disabled', false).val(volumeBeforeMute);
} else { } else {
mute = true; $muteButton.addClass('muted');
e("openwebrx-mute-off").id = "openwebrx-mute-on"; volumeBeforeMute = $volumePanel.val();
e("openwebrx-panel-volume").disabled = true; $volumePanel.prop('disabled', true).val(0);
volumeBeforeMute = e("openwebrx-panel-volume").value;
e("openwebrx-panel-volume").value = 0;
} }
updateVolume(); updateVolume();
@ -191,7 +185,7 @@ function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch
var highLevel = waterfall_max_level + 20; var highLevel = waterfall_max_level + 20;
var percent = (logValue - lowLevel) / (highLevel - lowLevel); var percent = (logValue - lowLevel) / (highLevel - lowLevel);
setSmeterRelativeValue(percent); setSmeterRelativeValue(percent);
e("openwebrx-smeter-db").innerHTML = logValue.toFixed(1) + " dB"; $("#openwebrx-smeter-db").html(logValue.toFixed(1) + " dB");
} }
function typeInAnimation(element, timeout, what, onFinish) { function typeInAnimation(element, timeout, what, onFinish) {
@ -244,14 +238,14 @@ var scale_ctx;
var scale_canvas; var scale_canvas;
function scale_setup() { function scale_setup() {
scale_canvas = e("openwebrx-scale-canvas"); scale_canvas = $("#openwebrx-scale-canvas")[0];
scale_ctx = scale_canvas.getContext("2d"); scale_ctx = scale_canvas.getContext("2d");
scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false); scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false);
scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false); scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false);
scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false); scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false);
resize_scale(); resize_scale();
var frequency_container = e("openwebrx-frequency-container"); var frequency_container = $("#openwebrx-frequency-container");
frequency_container.addEventListener("mousemove", frequency_container_mousemove, false); frequency_container.on("mousemove", frequency_container_mousemove, false);
} }
var scale_canvas_drag_params = { var scale_canvas_drag_params = {
@ -784,10 +778,10 @@ function on_ws_recv(evt) {
$('#openwebrx-bar-clients').progressbar().setClients(json['value']); $('#openwebrx-bar-clients').progressbar().setClients(json['value']);
break; break;
case "profiles": case "profiles":
var listbox = e("openwebrx-sdr-profiles-listbox"); var listbox = $("#openwebrx-sdr-profiles-listbox");
listbox.innerHTML = json['value'].map(function (profile) { listbox.html(json['value'].map(function (profile) {
return '<option value="' + profile['id'] + '">' + profile['name'] + "</option>"; return '<option value="' + profile['id'] + '">' + profile['name'] + "</option>";
}).join(""); }).join(""));
if (currentprofile) { if (currentprofile) {
$('#openwebrx-sdr-profiles-listbox').val(currentprofile); $('#openwebrx-sdr-profiles-listbox').val(currentprofile);
} }
@ -796,7 +790,9 @@ function on_ws_recv(evt) {
Modes.setFeatures(json['value']); Modes.setFeatures(json['value']);
break; break;
case "metadata": case "metadata":
update_metadata(json['value']); $('.openwebrx-meta-panel').metaPanel().each(function(){
this.update(json['value']);
});
break; break;
case "js8_message": case "js8_message":
$("#openwebrx-panel-js8-message").js8().pushMessage(json['value']); $("#openwebrx-panel-js8-message").js8().pushMessage(json['value']);
@ -906,75 +902,6 @@ function on_ws_recv(evt) {
} }
} }
function update_metadata(meta) {
var el;
if (meta['protocol']) switch (meta['protocol']) {
case 'DMR':
if (meta['slot']) {
el = $("#openwebrx-panel-metadata-dmr").find(".openwebrx-dmr-timeslot-panel").get(meta['slot']);
var id = "";
var name = "";
var target = "";
var group = false;
$(el)[meta['sync'] ? "addClass" : "removeClass"]("sync");
if (meta['sync'] && meta['sync'] === "voice") {
id = (meta['additional'] && meta['additional']['callsign']) || meta['source'] || "";
name = (meta['additional'] && meta['additional']['fname']) || "";
if (meta['type'] === "group") {
target = "Talkgroup: ";
group = true;
}
if (meta['type'] === "direct") target = "Direct: ";
target += meta['target'] || "";
$(el).addClass("active");
} else {
$(el).removeClass("active");
}
$(el).find(".openwebrx-dmr-id").text(id);
$(el).find(".openwebrx-dmr-name").text(name);
$(el).find(".openwebrx-dmr-target").text(target);
$(el).find(".openwebrx-meta-user-image")[group ? "addClass" : "removeClass"]("group");
} else {
clear_metadata();
}
break;
case 'YSF':
el = $("#openwebrx-panel-metadata-ysf");
var mode = " ";
var source = "";
var up = "";
var down = "";
if (meta['mode'] && meta['mode'] !== "") {
mode = "Mode: " + meta['mode'];
source = meta['source'] || "";
if (meta['lat'] && meta['lon'] && meta['source']) {
source = "<a class=\"openwebrx-maps-pin\" href=\"map?callsign=" + meta['source'] + "\" target=\"_blank\"></a>" + source;
}
up = meta['up'] ? "Up: " + meta['up'] : "";
down = meta['down'] ? "Down: " + meta['down'] : "";
$(el).find(".openwebrx-meta-slot").addClass("active");
} else {
$(el).find(".openwebrx-meta-slot").removeClass("active");
}
$(el).find(".openwebrx-ysf-mode").text(mode);
$(el).find(".openwebrx-ysf-source").html(source);
$(el).find(".openwebrx-ysf-up").text(up);
$(el).find(".openwebrx-ysf-down").text(down);
break;
} else {
clear_metadata();
}
}
function clear_metadata() {
$(".openwebrx-meta-panel .openwebrx-meta-autoclear").text("");
$(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
$(".openwebrx-dmr-timeslot-panel").removeClass("muted");
}
var waterfall_measure_minmax_now = false; var waterfall_measure_minmax_now = false;
var waterfall_measure_minmax_continuous = false; var waterfall_measure_minmax_continuous = false;
@ -1019,7 +946,7 @@ function divlog(what, is_error) {
what = "<span class=\"webrx-error\">" + what + "</span>"; what = "<span class=\"webrx-error\">" + what + "</span>";
toggle_panel("openwebrx-panel-log", true); //show panel if any error is present toggle_panel("openwebrx-panel-log", true); //show panel if any error is present
} }
e("openwebrx-debugdiv").innerHTML += what + "<br />"; $('#openwebrx-debugdiv')[0].innerHTML += what + "<br />";
var nano = $('.nano'); var nano = $('.nano');
nano.nanoScroller(); nano.nanoScroller();
nano.nanoScroller({scroll: 'bottom'}); nano.nanoScroller({scroll: 'bottom'});
@ -1145,14 +1072,14 @@ function add_canvas() {
function init_canvas_container() { function init_canvas_container() {
canvas_container = e("webrx-canvas-container"); canvas_container = $("#webrx-canvas-container")[0];
canvas_container.addEventListener("mouseleave", canvas_container_mouseleave, false); canvas_container.addEventListener("mouseleave", canvas_container_mouseleave, false);
canvas_container.addEventListener("mousemove", canvas_mousemove, false); canvas_container.addEventListener("mousemove", canvas_mousemove, false);
canvas_container.addEventListener("mouseup", canvas_mouseup, false); canvas_container.addEventListener("mouseup", canvas_mouseup, false);
canvas_container.addEventListener("mousedown", canvas_mousedown, false); canvas_container.addEventListener("mousedown", canvas_mousedown, false);
canvas_container.addEventListener("wheel", canvas_mousewheel, false); canvas_container.addEventListener("wheel", canvas_mousewheel, false);
var frequency_container = e("openwebrx-frequency-container"); var frequency_container = $("#openwebrx-frequency-container");
frequency_container.addEventListener("wheel", canvas_mousewheel, false); frequency_container.on("wheel", canvas_mousewheel, false);
add_canvas(); add_canvas();
} }
@ -1307,6 +1234,8 @@ function digimodes_init() {
$(e.currentTarget).toggleClass("muted"); $(e.currentTarget).toggleClass("muted");
update_dmr_timeslot_filtering(); update_dmr_timeslot_filtering();
}); });
$('.openwebrx-meta-panel').metaPanel();
} }
function update_dmr_timeslot_filtering() { function update_dmr_timeslot_filtering() {
@ -1354,7 +1283,7 @@ function toggle_panel(what, on) {
item.style.transitionProperty = 'transform'; item.style.transitionProperty = 'transform';
} else { } else {
item.movement = 'expand'; item.movement = 'expand';
item.style.display = 'block'; item.style.display = null;
setTimeout(function(){ setTimeout(function(){
item.style.transitionProperty = 'transform'; item.style.transitionProperty = 'transform';
item.style.transform = 'perspective(600px) rotateX(0deg)'; item.style.transform = 'perspective(600px) rotateX(0deg)';

View File

@ -11,8 +11,9 @@ from owrx.sdr import SdrService
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from owrx.service import Services from owrx.service import Services
from owrx.websocket import WebSocketConnection from owrx.websocket import WebSocketConnection
from owrx.pskreporter import PskReporter from owrx.reporting import ReportingEngine
from owrx.version import openwebrx_version from owrx.version import openwebrx_version
from owrx.audio import DecoderQueue
class ThreadedHttpServer(ThreadingMixIn, HTTPServer): class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
@ -67,4 +68,5 @@ Support and info: https://groups.io/g/openwebrx
except KeyboardInterrupt: except KeyboardInterrupt:
WebSocketConnection.closeAll() WebSocketConnection.closeAll()
Services.stop() Services.stop()
PskReporter.stop() ReportingEngine.stopAll()
DecoderQueue.stopAll()

View File

@ -7,7 +7,7 @@ import subprocess
import os import os
from multiprocessing.connection import Pipe, wait from multiprocessing.connection import Pipe, wait
from datetime import datetime, timedelta from datetime import datetime, timedelta
from queue import Queue, Full from queue import Queue, Full, Empty
import logging import logging
@ -32,22 +32,30 @@ class QueueJob(object):
pass pass
PoisonPill = object()
class QueueWorker(threading.Thread): class QueueWorker(threading.Thread):
def __init__(self, queue): def __init__(self, queue):
self.queue = queue self.queue = queue
self.doRun = True self.doRun = True
super().__init__(daemon=True) super().__init__()
def run(self) -> None: def run(self) -> None:
while self.doRun: while self.doRun:
job = self.queue.get() job = self.queue.get()
try: if job is PoisonPill:
job.run() self.doRun = False
except Exception: # put the poison pill back on the queue for the next worker
logger.exception("failed to decode job") self.queue.put(PoisonPill)
self.queue.onError() else:
finally: try:
job.unlink() job.run()
except Exception:
logger.exception("failed to decode job")
self.queue.onError()
finally:
job.unlink()
self.queue.task_done() self.queue.task_done()
@ -64,6 +72,13 @@ class DecoderQueue(Queue):
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
def stopAll():
with DecoderQueue.creationLock:
if DecoderQueue.sharedInstance is not None:
DecoderQueue.sharedInstance.stop()
DecoderQueue.sharedInstance = None
def __init__(self, maxsize, workers): def __init__(self, maxsize, workers):
super().__init__(maxsize) super().__init__(maxsize)
metrics = Metrics.getSharedInstance() metrics = Metrics.getSharedInstance()
@ -78,6 +93,18 @@ class DecoderQueue(Queue):
metrics.addMetric("decoding.queue.error", self.errorCounter) metrics.addMetric("decoding.queue.error", self.errorCounter)
self.workers = [self.newWorker() for _ in range(0, workers)] self.workers = [self.newWorker() for _ in range(0, workers)]
def stop(self):
logger.debug("shutting down the queue")
try:
# purge all remaining jobs
while not self.empty():
job = self.get()
job.unlink()
except Empty:
pass
# put() PoisonPill to tell workers to shut down
self.put(PoisonPill)
def put(self, item, **kwars): def put(self, item, **kwars):
self.inCounter.inc() self.inCounter.inc()
try: try:
@ -161,11 +188,10 @@ class AudioWriter(object):
self.timer.start() self.timer.start()
def switchFiles(self): def switchFiles(self):
self.switchingLock.acquire() with self.switchingLock:
file = self.wavefile file = self.wavefile
filename = self.wavefilename filename = self.wavefilename
(self.wavefilename, self.wavefile) = self.getWaveFile() (self.wavefilename, self.wavefile) = self.getWaveFile()
self.switchingLock.release()
file.close() file.close()
job = QueueJob(self, filename, self.dsp.get_operating_freq()) job = QueueJob(self, filename, self.dsp.get_operating_freq())
@ -205,9 +231,8 @@ class AudioWriter(object):
self._scheduleNextSwitch() self._scheduleNextSwitch()
def write(self, data): def write(self, data):
self.switchingLock.acquire() with self.switchingLock:
self.wavefile.writeframes(data) self.wavefile.writeframes(data)
self.switchingLock.release()
def stop(self): def stop(self):
self.outputWriter.close() self.outputWriter.close()
@ -229,7 +254,8 @@ class AudioWriter(object):
except Exception: except Exception:
logger.exception("error closing wave file") logger.exception("error closing wave file")
try: try:
os.unlink(self.wavefilename) with self.switchingLock:
os.unlink(self.wavefilename)
except Exception: except Exception:
logger.exception("error removing undecoded file") logger.exception("error removing undecoded file")
self.wavefile = None self.wavefile = None

View File

@ -1,4 +1,3 @@
from owrx.config import Config
from owrx.details import ReceiverDetails from owrx.details import ReceiverDetails
from owrx.dsp import DspManager from owrx.dsp import DspManager
from owrx.cpu import CpuUsageThread from owrx.cpu import CpuUsageThread
@ -110,7 +109,6 @@ class OpenWebRxClient(Client, metaclass=ABCMeta):
class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient): class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
sdr_config_keys = [ sdr_config_keys = [
"waterfall_min_level",
"waterfall_min_level", "waterfall_min_level",
"waterfall_max_level", "waterfall_max_level",
"samp_rate", "samp_rate",

View File

@ -129,6 +129,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
"lib/MessagePanel.js", "lib/MessagePanel.js",
"lib/Js8Threads.js", "lib/Js8Threads.js",
"lib/Modes.js", "lib/Modes.js",
"lib/MetaPanel.js",
], ],
"map.js": [ "map.js": [
"lib/jquery-3.2.1.min.js", "lib/jquery-3.2.1.min.js",

View File

@ -71,6 +71,7 @@ class CpuUsageThread(threading.Thread):
self.shutdown() self.shutdown()
def shutdown(self): def shutdown(self):
CpuUsageThread.sharedInstance = None with CpuUsageThread.creationLock:
CpuUsageThread.sharedInstance = None
self.doRun = False self.doRun = False
self.endEvent.set() self.endEvent.set()

View File

@ -77,6 +77,7 @@ class FeatureDetector(object):
"digital_voice_freedv": ["freedv_rx", "sox"], "digital_voice_freedv": ["freedv_rx", "sox"],
"digital_voice_m17": ["m17_demod", "sox"], "digital_voice_m17": ["m17_demod", "sox"],
"wsjt-x": ["wsjtx", "sox"], "wsjt-x": ["wsjtx", "sox"],
"wsjt-x-2-3": ["wsjtx_2_3", "sox"],
"packet": ["direwolf", "sox"], "packet": ["direwolf", "sox"],
"pocsag": ["digiham", "sox"], "pocsag": ["digiham", "sox"],
"js8call": ["js8", "sox"], "js8call": ["js8", "sox"],
@ -459,6 +460,26 @@ class FeatureDetector(object):
""" """
return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True) return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True)
def _has_wsjtx_version(self, required_version):
wsjt_version_regex = re.compile("^WSJT-X (.*)$")
try:
process = subprocess.Popen(["wsjtx_app_version", "--version"], stdout=subprocess.PIPE)
matches = wsjt_version_regex.match(process.stdout.readline().decode())
if matches is None:
return False
version = LooseVersion(matches.group(1))
process.wait(1)
return version >= required_version
except FileNotFoundError:
return False
def has_wsjtx_2_3(self):
"""
Newer digital modes (e.g. FST4, FST4) require WSJT-X in at least version 2.3.
"""
return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.3"))
def has_js8(self): def has_js8(self):
""" """
To decode JS8, you will need to install [JS8Call](http://js8call.com/) To decode JS8, you will need to install [JS8Call](http://js8call.com/)

View File

@ -4,10 +4,10 @@ import re
from js8py import Js8 from js8py import Js8
from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound
from .map import Map, LocatorLocation from .map import Map, LocatorLocation
from .pskreporter import PskReporter
from .metrics import Metrics, CounterMetric from .metrics import Metrics, CounterMetric
from .config import Config from .config import Config
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from owrx.reporting import ReportingEngine
import logging import logging
@ -102,7 +102,7 @@ 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
) )
PskReporter.getSharedInstance().spot({ ReportingEngine.getSharedInstance().spot({
"callsign": frame.callsign, "callsign": frame.callsign,
"mode": "JS8", "mode": "JS8",
"locator": frame.grid, "locator": frame.grid,

View File

@ -54,7 +54,7 @@ class DigitalMode(Mode):
class Modes(object): class Modes(object):
mappings = [ mappings = [
AnalogMode("nfm", "FM", bandpass=Bandpass(-4000, 4000)), AnalogMode("nfm", "FM", bandpass=Bandpass(-4000, 4000)),
AnalogMode("wfm", "WFM", bandpass=Bandpass(-50000, 50000)), AnalogMode("wfm", "WFM", bandpass=Bandpass(-75000, 75000)),
AnalogMode("am", "AM", bandpass=Bandpass(-4000, 4000)), AnalogMode("am", "AM", bandpass=Bandpass(-4000, 4000)),
AnalogMode("lsb", "LSB", bandpass=Bandpass(-3000, -300)), AnalogMode("lsb", "LSB", bandpass=Bandpass(-3000, -300)),
AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)), AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)),
@ -75,8 +75,8 @@ class Modes(object):
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"], service=True), DigitalMode("fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x-2-3"], service=True),
DigitalMode("fst4w", "FST4W", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], 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("js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True),
DigitalMode( DigitalMode(
"packet", "packet",

View File

@ -9,43 +9,19 @@ from owrx.config import Config
from owrx.version import openwebrx_version from owrx.version import openwebrx_version
from owrx.locator import Locator from owrx.locator import Locator
from owrx.metrics import Metrics, CounterMetric from owrx.metrics import Metrics, CounterMetric
from owrx.reporting import Reporter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class PskReporterDummy(object): class PskReporter(Reporter):
"""
used in place of the PskReporter when reporting is disabled.
does nothing.
"""
def spot(self, spot):
pass
def cancelTimer(self):
pass
class PskReporter(object):
sharedInstance = None
creationLock = threading.Lock()
interval = 300 interval = 300
supportedModes = ["FT8", "FT4", "JT9", "JT65", "FST4", "FST4W", "JS8"]
@staticmethod def getSupportedModes(self):
def getSharedInstance(): return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8"]
with PskReporter.creationLock:
if PskReporter.sharedInstance is None:
if Config.get()["pskreporter_enabled"]:
PskReporter.sharedInstance = PskReporter()
else:
PskReporter.sharedInstance = PskReporterDummy()
return PskReporter.sharedInstance
@staticmethod def stop(self):
def stop(): self.cancelTimer()
if PskReporter.sharedInstance:
PskReporter.sharedInstance.cancelTimer()
def __init__(self): def __init__(self):
self.spots = [] self.spots = []
@ -72,8 +48,6 @@ class PskReporter(object):
return reduce(and_, map(lambda key: s1[key] == s2[key], keys)) return reduce(and_, map(lambda key: s1[key] == s2[key], keys))
def spot(self, spot): def spot(self, spot):
if not spot["mode"] in PskReporter.supportedModes:
return
with self.spotLock: with self.spotLock:
if any(x for x in self.spots if self.spotEquals(spot, x)): if any(x for x in self.spots if self.spotEquals(spot, x)):
# dupe # dupe

56
owrx/reporting.py Normal file
View File

@ -0,0 +1,56 @@
import threading
from abc import ABC, abstractmethod
from owrx.config import Config
class Reporter(ABC):
@abstractmethod
def stop(self):
pass
@abstractmethod
def spot(self, spot):
pass
@abstractmethod
def getSupportedModes(self):
return []
class ReportingEngine(object):
creationLock = threading.Lock()
sharedInstance = None
@staticmethod
def getSharedInstance():
with ReportingEngine.creationLock:
if ReportingEngine.sharedInstance is None:
ReportingEngine.sharedInstance = ReportingEngine()
return ReportingEngine.sharedInstance
@staticmethod
def stopAll():
with ReportingEngine.creationLock:
if ReportingEngine.sharedInstance is not None:
ReportingEngine.sharedInstance.stop()
def __init__(self):
self.reporters = []
config = Config.get()
if "pskreporter_enabled" in config and config["pskreporter_enabled"]:
# inline import due to circular dependencies
from owrx.pskreporter import PskReporter
self.reporters += [PskReporter()]
if "wsprnet_enabled" in config and config["wsprnet_enabled"]:
# inline import due to circular dependencies
from owrx.wsprnet import WsprnetReporter
self.reporters += [WsprnetReporter()]
def stop(self):
for r in self.reporters:
r.stop()
def spot(self, spot):
for r in self.reporters:
if spot["mode"] in r.getSupportedModes():
r.spot(spot)

View File

@ -75,9 +75,38 @@ class SdrSource(ABC):
self.state = SdrSource.STATE_STOPPED self.state = SdrSource.STATE_STOPPED
self.busyState = SdrSource.BUSYSTATE_IDLE self.busyState = SdrSource.BUSYSTATE_IDLE
self.validateProfiles()
if self.isAlwaysOn(): if self.isAlwaysOn():
self.start() self.start()
def validateProfiles(self):
props = PropertyStack()
props.addLayer(1, self.props)
for id, p in self.props["profiles"].items():
props.replaceLayer(0, self._getProfilePropertyLayer(p))
if "center_freq" not in props:
logger.warning("Profile \"%s\" does not specify a center_freq", id)
continue
if "samp_rate" not in props:
logger.warning("Profile \"%s\" does not specify a samp_rate", id)
continue
if "start_freq" in props:
start_freq = props["start_freq"]
srh = props["samp_rate"] / 2
center_freq = props["center_freq"]
if start_freq < center_freq - srh or start_freq > center_freq + srh:
logger.warning("start_freq for profile \"%s\" is out of range", id)
def _getProfilePropertyLayer(self, profile):
layer = PropertyLayer()
for (key, value) in profile.items():
# skip the name, that would overwrite the source name.
if key == "name":
continue
layer[key] = value
return layer
def isAlwaysOn(self): def isAlwaysOn(self):
return "always-on" in self.props and self.props["always-on"] return "always-on" in self.props and self.props["always-on"]
@ -119,12 +148,7 @@ class SdrSource(ABC):
profile = profiles[profile_id] profile = profiles[profile_id]
self.profile_id = profile_id self.profile_id = profile_id
layer = PropertyLayer() layer = self._getProfilePropertyLayer(profile)
for (key, value) in profile.items():
# skip the name, that would overwrite the source name.
if key == "name":
continue
layer[key] = value
self.props.replaceLayer(0, layer) self.props.replaceLayer(0, layer)
def getId(self): def getId(self):

View File

@ -32,3 +32,7 @@ class Resampler(DirectSource):
def activateProfile(self, profile_id=None): def activateProfile(self, profile_id=None):
logger.warning("Resampler does not support setting profiles") logger.warning("Resampler does not support setting profiles")
pass pass
def validateProfiles(self):
# resampler does not support profiles
pass

View File

@ -2,7 +2,7 @@ from datetime import datetime, timezone
from owrx.map import Map, LocatorLocation from owrx.map import Map, LocatorLocation
import re import re
from owrx.metrics import Metrics, CounterMetric from owrx.metrics import Metrics, CounterMetric
from owrx.pskreporter import PskReporter from owrx.reporting import ReportingEngine
from owrx.parser import Parser from owrx.parser import Parser
from owrx.audio import AudioChopperProfile from owrx.audio import AudioChopperProfile
from abc import ABC, ABCMeta, abstractmethod from abc import ABC, ABCMeta, abstractmethod
@ -156,19 +156,24 @@ class WsjtParser(Parser):
return return
mode = profile.getMode() mode = profile.getMode()
if mode == "WSPR": if mode in ["WSPR", "FST4W"]:
decoder = WsprDecoder(profile) messageParser = BeaconMessageParser()
else: else:
decoder = Jt9Decoder(profile) messageParser = QsoMessageParser()
if mode == "WSPR":
decoder = WsprDecoder(profile, messageParser)
else:
decoder = Jt9Decoder(profile, messageParser)
out = decoder.parse(msg, freq) out = decoder.parse(msg, freq)
out["mode"] = mode out["mode"] = mode
out["interval"] = profile.getInterval()
self.pushDecode(mode) self.pushDecode(mode)
if "callsign" in out and "locator" in out: if "callsign" in out and "locator" in out:
Map.getSharedInstance().updateLocation( Map.getSharedInstance().updateLocation(
out["callsign"], LocatorLocation(out["locator"]), mode, self.band out["callsign"], LocatorLocation(out["locator"]), mode, self.band
) )
PskReporter.getSharedInstance().spot(out) ReportingEngine.getSharedInstance().spot(out)
self.handler.write_wsjt_message(out) self.handler.write_wsjt_message(out)
except (ValueError, IndexError): except (ValueError, IndexError):
@ -195,10 +200,9 @@ class WsjtParser(Parser):
class Decoder(ABC): class Decoder(ABC):
locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$") def __init__(self, profile, messageParser):
def __init__(self, profile):
self.profile = profile self.profile = profile
self.messageParser = messageParser
def parse_timestamp(self, instring): def parse_timestamp(self, instring):
dateformat = self.profile.getTimestampFormat() dateformat = self.profile.getTimestampFormat()
@ -215,8 +219,19 @@ class Decoder(ABC):
def parse(self, msg, dial_freq): def parse(self, msg, dial_freq):
pass pass
def parseMessage(self, msg):
m = Decoder.locator_pattern.match(msg) class MessageParser(ABC):
@abstractmethod
def parse(self, msg):
pass
# Used in QSO-style modes (FT8, FT4, FST4)
class QsoMessageParser(MessageParser):
locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$")
def parse(self, msg):
m = QsoMessageParser.locator_pattern.match(msg)
if m is None: if m is None:
return {} return {}
# this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very
@ -226,6 +241,17 @@ class Decoder(ABC):
return {"callsign": m.group(1), "locator": m.group(3)} return {"callsign": m.group(1), "locator": m.group(3)}
# Used in propagation reporting / beacon modes (WSPR / FST4W)
class BeaconMessageParser(MessageParser):
wspr_splitter_pattern = re.compile("([A-Z0-9/]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")
def parse(self, msg):
m = BeaconMessageParser.wspr_splitter_pattern.match(msg)
if m is None:
return {}
return {"callsign": m.group(1), "locator": m.group(2), "dbm": m.group(3)}
class Jt9Decoder(Decoder): class Jt9Decoder(Decoder):
def parse(self, msg, dial_freq): def parse(self, msg, dial_freq):
# ft8 sample # ft8 sample
@ -245,13 +271,11 @@ class Jt9Decoder(Decoder):
"freq": dial_freq + int(msg[9:13]), "freq": dial_freq + int(msg[9:13]),
"msg": wsjt_msg, "msg": wsjt_msg,
} }
result.update(self.parseMessage(wsjt_msg)) result.update(self.messageParser.parse(wsjt_msg))
return result return result
class WsprDecoder(Decoder): class WsprDecoder(Decoder):
wspr_splitter_pattern = re.compile("([A-Z0-9/]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")
def parse(self, msg, dial_freq): def parse(self, msg, dial_freq):
# wspr sample # wspr sample
# '2600 -24 0.4 0.001492 -1 G8AXA JO01 33' # '2600 -24 0.4 0.001492 -1 G8AXA JO01 33'
@ -266,11 +290,5 @@ class WsprDecoder(Decoder):
"drift": int(msg[20:23]), "drift": int(msg[20:23]),
"msg": wsjt_msg, "msg": wsjt_msg,
} }
result.update(self.parseMessage(wsjt_msg)) result.update(self.messageParser.parse(wsjt_msg))
return result return result
def parseMessage(self, msg):
m = WsprDecoder.wspr_splitter_pattern.match(msg)
if m is None:
return {}
return {"callsign": m.group(1), "locator": m.group(2)}

90
owrx/wsprnet.py Normal file
View File

@ -0,0 +1,90 @@
from owrx.reporting import Reporter
from owrx.version import openwebrx_version
from owrx.config import Config
from owrx.locator import Locator
from owrx.metrics import Metrics, CounterMetric
from queue import Queue, Full
from urllib import request, parse
import threading
import logging
from datetime import datetime, timezone
logger = logging.getLogger(__name__)
class Worker(threading.Thread):
def __init__(self, queue: Queue):
self.queue = queue
self.doRun = True
# some constants that we don't expect to change
config = Config.get()
self.callsign = config["wsprnet_callsign"]
self.locator = Locator.fromCoordinates(config["receiver_gps"])
super().__init__(daemon=True)
def run(self):
while self.doRun:
try:
spot = self.queue.get()
self.uploadSpot(spot)
self.queue.task_done()
except Exception:
logger.exception("Exception while uploading WSPRNet spot")
def _getMode(self, spot):
interval = round(spot["interval"] / 60)
# FST4W modes are mapped not to conflict with WSPR modes 2 and 15:
if spot["mode"] != "WSPR" and interval in [2, 15]:
return interval + 1
return interval
def uploadSpot(self, spot):
# function=wspr&date=210114&time=1732&sig=-15&dt=0.5&drift=0&tqrg=7.040019&tcall=DF2UU&tgrid=JN48&dbm=37&version=2.3.0-rc3&rcall=DD5JFK&rgrid=JN58SC&rqrg=7.040047&mode=2
# {'timestamp': 1610655960000, 'db': -23.0, 'dt': 0.3, 'freq': 7040048, 'drift': -1, 'msg': 'LA3JJ JO59 37', 'callsign': 'LA3JJ', 'locator': 'JO59', 'mode': 'WSPR'}
date = datetime.fromtimestamp(spot["timestamp"] / 1000, tz=timezone.utc)
data = parse.urlencode({
"function": "wspr",
"date": date.strftime("%y%m%d"),
"time": date.strftime("%H%M"),
"sig": spot["db"],
"dt": spot["dt"],
# FST4W does not have drift
"drift": spot["drift"] if "drift" in spot else 0,
"tqrg": spot["freq"] / 1E6,
"tcall": spot["callsign"],
"tgrid": spot["locator"],
"dbm": spot["dbm"],
"version": openwebrx_version,
"rcall": self.callsign,
"rgrid": self.locator,
# mode 2 = WSPR 2 minutes
"mode": self._getMode(spot)
}).encode()
request.urlopen("http://wsprnet.org/post/", data)
class WsprnetReporter(Reporter):
def __init__(self):
# max 100 entries
self.queue = Queue(100)
# single worker
Worker(self.queue).start()
# metrics
metrics = Metrics.getSharedInstance()
self.spotCounter = CounterMetric()
metrics.addMetric("wsprnet.spots", self.spotCounter)
def stop(self):
pass
def spot(self, spot):
try:
self.queue.put(spot, block=False)
self.spotCounter.inc()
except Full:
logger.warning("WSPRNet Queue overflow, one spot lost")
def getSupportedModes(self):
return ["WSPR", "FST4W"]