Merge branch 'develop' into pycsdr
This commit is contained in:
commit
297d6b540d
@ -3,6 +3,7 @@
|
||||
- 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 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:
|
||||
- HPSDR devices (Hermes Lite 2)
|
||||
- BBRF103 / RX666 / RX888 devices supported by libsddc
|
||||
|
@ -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
|
||||
# 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
|
||||
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator
|
||||
pskreporter_enabled = False
|
||||
@ -369,6 +369,12 @@ pskreporter_callsign = "N0CALL"
|
||||
# optional antenna information, uncomment to enable
|
||||
#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 ===
|
||||
# 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.
|
||||
|
@ -556,12 +556,12 @@ class dsp(object):
|
||||
# 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
|
||||
# to compensate here.
|
||||
# the factor of 5 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).
|
||||
# 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 +/- 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
|
||||
# after the demodulator.
|
||||
if self.get_demodulator() == "wfm":
|
||||
correction = 5
|
||||
correction = 6
|
||||
while input_rate / (decimation + 1) >= output_rate * correction:
|
||||
decimation += 1
|
||||
fraction = float(input_rate / decimation) / output_rate
|
||||
@ -933,6 +933,3 @@ class dsp(object):
|
||||
return
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def __del__(self):
|
||||
self.stop()
|
||||
|
2
debian/changelog
vendored
2
debian/changelog
vendored
@ -7,6 +7,8 @@ openwebrx (0.21.0) UNRELEASED; urgency=low
|
||||
WSJT-X 2.3)
|
||||
* 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:
|
||||
- HPSDR devices (Hermes Lite 2) (`"type": "hpsdr"`)
|
||||
- BBRF103 / RX666 / RX888 devices supported by libsddc (`"type": "sddc"`)
|
||||
|
2
debian/control
vendored
2
debian/control
vendored
@ -11,6 +11,6 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git
|
||||
Package: openwebrx
|
||||
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}
|
||||
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
|
||||
Open source, multi-user SDR receiver with a web interface
|
@ -25,12 +25,19 @@ ul {
|
||||
padding-inline-start: 25px;
|
||||
}
|
||||
|
||||
/* don't show the filter in it's initial position */
|
||||
.openwebrx-map-legend {
|
||||
display: none;
|
||||
background-color: #fff;
|
||||
padding: 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 {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
|
@ -1,32 +1,36 @@
|
||||
#webrx-top-container
|
||||
{
|
||||
#webrx-top-container {
|
||||
position: relative;
|
||||
z-index:1000;
|
||||
background-color: #575757;
|
||||
}
|
||||
|
||||
#webrx-top-photo
|
||||
{
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
background-image: url(../gfx/openwebrx-top-photo.jpg);
|
||||
background-position-x: center;
|
||||
background-position-y: top;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
|
||||
#webrx-top-photo-clip
|
||||
{
|
||||
min-height: 67px;
|
||||
max-height: 67px;
|
||||
height: 350px;
|
||||
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;
|
||||
}
|
||||
|
||||
#webrx-top-bar
|
||||
{
|
||||
background: rgba(128, 128, 128, 0.15);
|
||||
margin:0;
|
||||
padding:0;
|
||||
@ -37,31 +41,28 @@
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#webrx-tob-container, #webrx-top-container * {
|
||||
.webrx-top-bar-parts > * {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
#webrx-top-container, #webrx-top-container * {
|
||||
line-height: initial;
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
#webrx-top-container img {
|
||||
vertical-align: initial;
|
||||
}
|
||||
|
||||
#webrx-top-logo
|
||||
{
|
||||
#webrx-top-logo {
|
||||
padding: 12px;
|
||||
float: left;
|
||||
/* overwritten by media queries */
|
||||
display: none;
|
||||
}
|
||||
|
||||
#webrx-rx-avatar
|
||||
{
|
||||
#webrx-rx-avatar {
|
||||
background-color: rgba(154, 154, 154, .5);
|
||||
float: left;
|
||||
margin: 7px;
|
||||
|
||||
cursor:pointer;
|
||||
@ -73,49 +74,45 @@
|
||||
}
|
||||
|
||||
#webrx-rx-texts {
|
||||
float: left;
|
||||
padding: 10px;
|
||||
/* minimum layout width */
|
||||
width: 0;
|
||||
/* will be getting wider with flex */
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#webrx-rx-texts div {
|
||||
margin: 0 10px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#webrx-rx-title
|
||||
{
|
||||
white-space:nowrap;
|
||||
overflow: hidden;
|
||||
cursor:pointer;
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
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-weight: bold;
|
||||
}
|
||||
|
||||
#webrx-rx-desc
|
||||
{
|
||||
white-space:nowrap;
|
||||
overflow: hidden;
|
||||
cursor:pointer;
|
||||
#webrx-rx-desc {
|
||||
font-size: 10pt;
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
#webrx-rx-desc a
|
||||
{
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow
|
||||
{
|
||||
#openwebrx-rx-details-arrow {
|
||||
cursor:pointer;
|
||||
position: absolute;
|
||||
left: 470px;
|
||||
top: 55px;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow a
|
||||
{
|
||||
#openwebrx-rx-details-arrow a {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
@ -128,6 +125,11 @@
|
||||
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 {
|
||||
height: 38px;
|
||||
}
|
||||
@ -137,23 +139,19 @@
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons .button:hover
|
||||
{
|
||||
#openwebrx-main-buttons .button:hover {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
#openwebrx-main-buttons
|
||||
{
|
||||
#openwebrx-main-buttons {
|
||||
padding: 5px 15px;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
float: right;
|
||||
margin:0;
|
||||
color: white;
|
||||
text-shadow: 0px 0px 4px #000000;
|
||||
@ -162,23 +160,17 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-title
|
||||
{
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 78px;
|
||||
color: White;
|
||||
#webrx-rx-photo-title {
|
||||
margin: 10px 15px;
|
||||
color: white;
|
||||
font-size: 16pt;
|
||||
text-shadow: 1px 1px 4px #444;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-desc
|
||||
{
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 109px;
|
||||
color: White;
|
||||
#webrx-rx-photo-desc {
|
||||
margin: 10px 15px;
|
||||
color: white;
|
||||
font-size: 10pt;
|
||||
font-weight: bold;
|
||||
text-shadow: 0px 0px 6px #444;
|
||||
@ -186,12 +178,37 @@
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-desc a
|
||||
{
|
||||
#webrx-rx-photo-desc a {
|
||||
color: #5ca8ff;
|
||||
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 {
|
||||
background-position: 0 0;
|
||||
width: 44px;
|
||||
|
@ -573,9 +573,10 @@ img.openwebrx-mirror-img
|
||||
.openwebrx-progressbar-text
|
||||
{
|
||||
position: absolute;
|
||||
left:0px;
|
||||
top:4px;
|
||||
width: inherit;
|
||||
left:50;
|
||||
top:50%;
|
||||
transform: translate(-50%, -50%);
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@ -907,18 +908,30 @@ img.openwebrx-mirror-img
|
||||
border-color: Red;
|
||||
}
|
||||
|
||||
.openwebrx-meta-panel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot {
|
||||
flex: 1;
|
||||
width: 145px;
|
||||
height: 196px;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
|
||||
background-color: #676767;
|
||||
padding: 2px 0;
|
||||
color: #333;
|
||||
|
||||
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 {
|
||||
@ -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;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot .openwebrx-meta-user-image {
|
||||
width:100%;
|
||||
height:133px;
|
||||
flex: 1;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@ -1233,12 +1241,12 @@ img.openwebrx-mirror-img
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
#openwebrx-mute-on .sprite-speaker {
|
||||
background-position: -117px -38px;
|
||||
.openwebrx-mute-button .sprite-speaker {
|
||||
background-position: -103px -38px;
|
||||
}
|
||||
|
||||
#openwebrx-mute-off .sprite-speaker {
|
||||
background-position: -103px -38px;
|
||||
.openwebrx-mute-button.muted .sprite-speaker {
|
||||
background-position: -117px -38px;
|
||||
}
|
||||
|
||||
.sprite-squelch {
|
||||
|
@ -1,26 +1,25 @@
|
||||
<div id="webrx-top-container">
|
||||
<div id="webrx-top-photo-clip">
|
||||
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo" alt="Receiver panorama"/>
|
||||
<div id="webrx-top-bar" class="webrx-top-bar-parts">
|
||||
<a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a>
|
||||
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
|
||||
<div id="webrx-rx-texts">
|
||||
<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 id="webrx-top-bar" class="webrx-top-bar-parts">
|
||||
<a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a>
|
||||
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
|
||||
<div id="webrx-rx-texts">
|
||||
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
|
||||
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></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-desc"></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>
|
||||
|
@ -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-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-meta-frame">
|
||||
<div class="openwebrx-meta-slot">
|
||||
<div class="openwebrx-ysf-mode openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-ysf-source openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-ysf-up openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-ysf-down openwebrx-meta-autoclear"></div>
|
||||
</div>
|
||||
<div class="openwebrx-meta-slot">
|
||||
<div class="openwebrx-ysf-mode"></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-up"></div>
|
||||
<div class="openwebrx-ysf-down"></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-meta-frame">
|
||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||
<div class="openwebrx-dmr-slot">Timeslot 1</div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
|
||||
</div>
|
||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||
<div class="openwebrx-dmr-slot">Timeslot 2</div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
|
||||
</div>
|
||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||
<div class="openwebrx-dmr-slot">Timeslot 1</div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-dmr-id"></div>
|
||||
<div class="openwebrx-dmr-name"></div>
|
||||
<div class="openwebrx-dmr-target"></div>
|
||||
</div>
|
||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||
<div class="openwebrx-dmr-slot">Timeslot 2</div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-dmr-id"></div>
|
||||
<div class="openwebrx-dmr-name"></div>
|
||||
<div class="openwebrx-dmr-target"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" style="width: 619px;">
|
||||
@ -132,7 +128,7 @@
|
||||
</div>
|
||||
<div class="openwebrx-modes openwebrx-panel-line"></div>
|
||||
<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()">
|
||||
<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()">
|
||||
|
@ -14,7 +14,19 @@ function AudioEngine(maxBufferLength, audioReporter) {
|
||||
this.onStartCallbacks = [];
|
||||
|
||||
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;
|
||||
this.audioContext.onstatechange = function() {
|
||||
if (me.audioContext.state !== 'running') return;
|
||||
|
@ -8,7 +8,7 @@ Filter.prototype.getLimits = function() {
|
||||
if (this.demodulator.get_secondary_demod() === 'pocsag') {
|
||||
max_bw = 12500;
|
||||
} else if (this.demodulator.get_modulation() === 'wfm') {
|
||||
max_bw = 80000;
|
||||
max_bw = 100000;
|
||||
} else if (this.demodulator.get_modulation() === 'drm') {
|
||||
max_bw = 100000;
|
||||
} 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) {
|
||||
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);
|
||||
if (this.offset_frequency === to_what) {
|
||||
return;
|
||||
|
@ -165,10 +165,13 @@ DemodulatorPanel.prototype.updatePanels = function() {
|
||||
|
||||
modulation = this.getDemodulator().get_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);
|
||||
});
|
||||
clear_metadata();
|
||||
metaPanels.metaPanel().each(function() {
|
||||
this.clear();
|
||||
});
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.getDemodulator = function() {
|
||||
@ -181,7 +184,7 @@ DemodulatorPanel.prototype.collectParams = function() {
|
||||
squelch_level: -150,
|
||||
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() {
|
||||
@ -287,7 +290,7 @@ DemodulatorPanel.prototype.validateHash = function(params) {
|
||||
var self = this;
|
||||
params = Object.keys(params).filter(function(key) {
|
||||
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;
|
||||
}).reduce(function(p, key) {
|
||||
@ -303,6 +306,17 @@ DemodulatorPanel.prototype.validateHash = function(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() {
|
||||
var demod = this.getDemodulator();
|
||||
if (!demod) return;
|
||||
|
@ -1,7 +1,12 @@
|
||||
function Header(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'));
|
||||
});
|
||||
|
||||
@ -30,18 +35,14 @@ Header.prototype.init_rx_photo = function() {
|
||||
|
||||
Header.prototype.close_rx_photo = function() {
|
||||
this.rx_photo_state = 0;
|
||||
this.el.find("#webrx-rx-photo-desc").animate({opacity: 0});
|
||||
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-description-container').removeClass('expanded');
|
||||
this.el.find("#openwebrx-rx-details-arrow-down").show();
|
||||
this.el.find("#openwebrx-rx-details-arrow-up").hide();
|
||||
}
|
||||
|
||||
Header.prototype.open_rx_photo = function() {
|
||||
this.rx_photo_state = 1;
|
||||
this.el.find("#webrx-rx-photo-desc").animate({opacity: 1});
|
||||
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-description-container').addClass('expanded');
|
||||
this.el.find("#openwebrx-rx-details-arrow-down").hide();
|
||||
this.el.find("#openwebrx-rx-details-arrow-up").show();
|
||||
}
|
||||
|
@ -78,14 +78,14 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) {
|
||||
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})$/);
|
||||
if (matches && matches[2] !== 'RR73') {
|
||||
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
|
||||
} else {
|
||||
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]+)/);
|
||||
if (matches) {
|
||||
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
205
htdocs/lib/MetaPanel.js
Normal 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');
|
||||
});
|
||||
};
|
@ -2,6 +2,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<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" />
|
||||
<script src="compiled/map.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||
|
@ -32,26 +32,20 @@ var fft_codec;
|
||||
var waterfall_setup_done = 0;
|
||||
var secondary_fft_size;
|
||||
|
||||
function e(what) {
|
||||
return document.getElementById(what);
|
||||
}
|
||||
|
||||
function updateVolume() {
|
||||
audioEngine.setVolume(parseFloat(e("openwebrx-panel-volume").value) / 100);
|
||||
audioEngine.setVolume(parseFloat($("#openwebrx-panel-volume").val()) / 100);
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (mute) {
|
||||
mute = false;
|
||||
e("openwebrx-mute-on").id = "openwebrx-mute-off";
|
||||
e("openwebrx-panel-volume").disabled = false;
|
||||
e("openwebrx-panel-volume").value = volumeBeforeMute;
|
||||
var $muteButton = $('.openwebrx-mute-button');
|
||||
var $volumePanel = $('#openwebrx-panel-volume');
|
||||
if ($muteButton.hasClass('muted')) {
|
||||
$muteButton.removeClass('muted');
|
||||
$volumePanel.prop('disabled', false).val(volumeBeforeMute);
|
||||
} else {
|
||||
mute = true;
|
||||
e("openwebrx-mute-off").id = "openwebrx-mute-on";
|
||||
e("openwebrx-panel-volume").disabled = true;
|
||||
volumeBeforeMute = e("openwebrx-panel-volume").value;
|
||||
e("openwebrx-panel-volume").value = 0;
|
||||
$muteButton.addClass('muted');
|
||||
volumeBeforeMute = $volumePanel.val();
|
||||
$volumePanel.prop('disabled', true).val(0);
|
||||
}
|
||||
|
||||
updateVolume();
|
||||
@ -191,7 +185,7 @@ function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch
|
||||
var highLevel = waterfall_max_level + 20;
|
||||
var percent = (logValue - lowLevel) / (highLevel - lowLevel);
|
||||
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) {
|
||||
@ -244,14 +238,14 @@ var scale_ctx;
|
||||
var scale_canvas;
|
||||
|
||||
function scale_setup() {
|
||||
scale_canvas = e("openwebrx-scale-canvas");
|
||||
scale_canvas = $("#openwebrx-scale-canvas")[0];
|
||||
scale_ctx = scale_canvas.getContext("2d");
|
||||
scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false);
|
||||
scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false);
|
||||
scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false);
|
||||
resize_scale();
|
||||
var frequency_container = e("openwebrx-frequency-container");
|
||||
frequency_container.addEventListener("mousemove", frequency_container_mousemove, false);
|
||||
var frequency_container = $("#openwebrx-frequency-container");
|
||||
frequency_container.on("mousemove", frequency_container_mousemove, false);
|
||||
}
|
||||
|
||||
var scale_canvas_drag_params = {
|
||||
@ -784,10 +778,10 @@ function on_ws_recv(evt) {
|
||||
$('#openwebrx-bar-clients').progressbar().setClients(json['value']);
|
||||
break;
|
||||
case "profiles":
|
||||
var listbox = e("openwebrx-sdr-profiles-listbox");
|
||||
listbox.innerHTML = json['value'].map(function (profile) {
|
||||
var listbox = $("#openwebrx-sdr-profiles-listbox");
|
||||
listbox.html(json['value'].map(function (profile) {
|
||||
return '<option value="' + profile['id'] + '">' + profile['name'] + "</option>";
|
||||
}).join("");
|
||||
}).join(""));
|
||||
if (currentprofile) {
|
||||
$('#openwebrx-sdr-profiles-listbox').val(currentprofile);
|
||||
}
|
||||
@ -796,7 +790,9 @@ function on_ws_recv(evt) {
|
||||
Modes.setFeatures(json['value']);
|
||||
break;
|
||||
case "metadata":
|
||||
update_metadata(json['value']);
|
||||
$('.openwebrx-meta-panel').metaPanel().each(function(){
|
||||
this.update(json['value']);
|
||||
});
|
||||
break;
|
||||
case "js8_message":
|
||||
$("#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_continuous = false;
|
||||
|
||||
@ -1019,7 +946,7 @@ function divlog(what, is_error) {
|
||||
what = "<span class=\"webrx-error\">" + what + "</span>";
|
||||
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');
|
||||
nano.nanoScroller();
|
||||
nano.nanoScroller({scroll: 'bottom'});
|
||||
@ -1145,14 +1072,14 @@ function add_canvas() {
|
||||
|
||||
|
||||
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("mousemove", canvas_mousemove, false);
|
||||
canvas_container.addEventListener("mouseup", canvas_mouseup, false);
|
||||
canvas_container.addEventListener("mousedown", canvas_mousedown, false);
|
||||
canvas_container.addEventListener("wheel", canvas_mousewheel, false);
|
||||
var frequency_container = e("openwebrx-frequency-container");
|
||||
frequency_container.addEventListener("wheel", canvas_mousewheel, false);
|
||||
var frequency_container = $("#openwebrx-frequency-container");
|
||||
frequency_container.on("wheel", canvas_mousewheel, false);
|
||||
add_canvas();
|
||||
}
|
||||
|
||||
@ -1307,6 +1234,8 @@ function digimodes_init() {
|
||||
$(e.currentTarget).toggleClass("muted");
|
||||
update_dmr_timeslot_filtering();
|
||||
});
|
||||
|
||||
$('.openwebrx-meta-panel').metaPanel();
|
||||
}
|
||||
|
||||
function update_dmr_timeslot_filtering() {
|
||||
@ -1354,7 +1283,7 @@ function toggle_panel(what, on) {
|
||||
item.style.transitionProperty = 'transform';
|
||||
} else {
|
||||
item.movement = 'expand';
|
||||
item.style.display = 'block';
|
||||
item.style.display = null;
|
||||
setTimeout(function(){
|
||||
item.style.transitionProperty = 'transform';
|
||||
item.style.transform = 'perspective(600px) rotateX(0deg)';
|
||||
|
@ -11,8 +11,9 @@ from owrx.sdr import SdrService
|
||||
from socketserver import ThreadingMixIn
|
||||
from owrx.service import Services
|
||||
from owrx.websocket import WebSocketConnection
|
||||
from owrx.pskreporter import PskReporter
|
||||
from owrx.reporting import ReportingEngine
|
||||
from owrx.version import openwebrx_version
|
||||
from owrx.audio import DecoderQueue
|
||||
|
||||
|
||||
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
|
||||
@ -67,4 +68,5 @@ Support and info: https://groups.io/g/openwebrx
|
||||
except KeyboardInterrupt:
|
||||
WebSocketConnection.closeAll()
|
||||
Services.stop()
|
||||
PskReporter.stop()
|
||||
ReportingEngine.stopAll()
|
||||
DecoderQueue.stopAll()
|
||||
|
@ -7,7 +7,7 @@ import subprocess
|
||||
import os
|
||||
from multiprocessing.connection import Pipe, wait
|
||||
from datetime import datetime, timedelta
|
||||
from queue import Queue, Full
|
||||
from queue import Queue, Full, Empty
|
||||
|
||||
|
||||
import logging
|
||||
@ -32,22 +32,30 @@ class QueueJob(object):
|
||||
pass
|
||||
|
||||
|
||||
PoisonPill = object()
|
||||
|
||||
|
||||
class QueueWorker(threading.Thread):
|
||||
def __init__(self, queue):
|
||||
self.queue = queue
|
||||
self.doRun = True
|
||||
super().__init__(daemon=True)
|
||||
super().__init__()
|
||||
|
||||
def run(self) -> None:
|
||||
while self.doRun:
|
||||
job = self.queue.get()
|
||||
try:
|
||||
job.run()
|
||||
except Exception:
|
||||
logger.exception("failed to decode job")
|
||||
self.queue.onError()
|
||||
finally:
|
||||
job.unlink()
|
||||
if job is PoisonPill:
|
||||
self.doRun = False
|
||||
# put the poison pill back on the queue for the next worker
|
||||
self.queue.put(PoisonPill)
|
||||
else:
|
||||
try:
|
||||
job.run()
|
||||
except Exception:
|
||||
logger.exception("failed to decode job")
|
||||
self.queue.onError()
|
||||
finally:
|
||||
job.unlink()
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
@ -64,6 +72,13 @@ class DecoderQueue(Queue):
|
||||
DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"])
|
||||
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):
|
||||
super().__init__(maxsize)
|
||||
metrics = Metrics.getSharedInstance()
|
||||
@ -78,6 +93,18 @@ class DecoderQueue(Queue):
|
||||
metrics.addMetric("decoding.queue.error", self.errorCounter)
|
||||
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):
|
||||
self.inCounter.inc()
|
||||
try:
|
||||
@ -161,11 +188,10 @@ class AudioWriter(object):
|
||||
self.timer.start()
|
||||
|
||||
def switchFiles(self):
|
||||
self.switchingLock.acquire()
|
||||
file = self.wavefile
|
||||
filename = self.wavefilename
|
||||
(self.wavefilename, self.wavefile) = self.getWaveFile()
|
||||
self.switchingLock.release()
|
||||
with self.switchingLock:
|
||||
file = self.wavefile
|
||||
filename = self.wavefilename
|
||||
(self.wavefilename, self.wavefile) = self.getWaveFile()
|
||||
|
||||
file.close()
|
||||
job = QueueJob(self, filename, self.dsp.get_operating_freq())
|
||||
@ -205,9 +231,8 @@ class AudioWriter(object):
|
||||
self._scheduleNextSwitch()
|
||||
|
||||
def write(self, data):
|
||||
self.switchingLock.acquire()
|
||||
self.wavefile.writeframes(data)
|
||||
self.switchingLock.release()
|
||||
with self.switchingLock:
|
||||
self.wavefile.writeframes(data)
|
||||
|
||||
def stop(self):
|
||||
self.outputWriter.close()
|
||||
@ -229,7 +254,8 @@ class AudioWriter(object):
|
||||
except Exception:
|
||||
logger.exception("error closing wave file")
|
||||
try:
|
||||
os.unlink(self.wavefilename)
|
||||
with self.switchingLock:
|
||||
os.unlink(self.wavefilename)
|
||||
except Exception:
|
||||
logger.exception("error removing undecoded file")
|
||||
self.wavefile = None
|
||||
|
@ -1,4 +1,3 @@
|
||||
from owrx.config import Config
|
||||
from owrx.details import ReceiverDetails
|
||||
from owrx.dsp import DspManager
|
||||
from owrx.cpu import CpuUsageThread
|
||||
@ -110,7 +109,6 @@ class OpenWebRxClient(Client, metaclass=ABCMeta):
|
||||
|
||||
class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
|
||||
sdr_config_keys = [
|
||||
"waterfall_min_level",
|
||||
"waterfall_min_level",
|
||||
"waterfall_max_level",
|
||||
"samp_rate",
|
||||
|
@ -129,6 +129,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
|
||||
"lib/MessagePanel.js",
|
||||
"lib/Js8Threads.js",
|
||||
"lib/Modes.js",
|
||||
"lib/MetaPanel.js",
|
||||
],
|
||||
"map.js": [
|
||||
"lib/jquery-3.2.1.min.js",
|
||||
|
@ -71,6 +71,7 @@ class CpuUsageThread(threading.Thread):
|
||||
self.shutdown()
|
||||
|
||||
def shutdown(self):
|
||||
CpuUsageThread.sharedInstance = None
|
||||
with CpuUsageThread.creationLock:
|
||||
CpuUsageThread.sharedInstance = None
|
||||
self.doRun = False
|
||||
self.endEvent.set()
|
||||
|
@ -77,6 +77,7 @@ class FeatureDetector(object):
|
||||
"digital_voice_freedv": ["freedv_rx", "sox"],
|
||||
"digital_voice_m17": ["m17_demod", "sox"],
|
||||
"wsjt-x": ["wsjtx", "sox"],
|
||||
"wsjt-x-2-3": ["wsjtx_2_3", "sox"],
|
||||
"packet": ["direwolf", "sox"],
|
||||
"pocsag": ["digiham", "sox"],
|
||||
"js8call": ["js8", "sox"],
|
||||
@ -459,6 +460,26 @@ class FeatureDetector(object):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
To decode JS8, you will need to install [JS8Call](http://js8call.com/)
|
||||
|
@ -4,10 +4,10 @@ import re
|
||||
from js8py import Js8
|
||||
from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound
|
||||
from .map import Map, LocatorLocation
|
||||
from .pskreporter import PskReporter
|
||||
from .metrics import Metrics, CounterMetric
|
||||
from .config import Config
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from owrx.reporting import ReportingEngine
|
||||
|
||||
import logging
|
||||
|
||||
@ -102,7 +102,7 @@ class Js8Parser(Parser):
|
||||
Map.getSharedInstance().updateLocation(
|
||||
frame.callsign, LocatorLocation(frame.grid), "JS8", self.band
|
||||
)
|
||||
PskReporter.getSharedInstance().spot({
|
||||
ReportingEngine.getSharedInstance().spot({
|
||||
"callsign": frame.callsign,
|
||||
"mode": "JS8",
|
||||
"locator": frame.grid,
|
||||
|
@ -54,7 +54,7 @@ class DigitalMode(Mode):
|
||||
class Modes(object):
|
||||
mappings = [
|
||||
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("lsb", "LSB", bandpass=Bandpass(-3000, -300)),
|
||||
AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)),
|
||||
@ -75,8 +75,8 @@ class Modes(object):
|
||||
DigitalMode(
|
||||
"wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True
|
||||
),
|
||||
DigitalMode("fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
|
||||
DigitalMode("fst4w", "FST4W", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True),
|
||||
DigitalMode("fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x-2-3"], service=True),
|
||||
DigitalMode("fst4w", "FST4W", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"], service=True),
|
||||
DigitalMode("js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True),
|
||||
DigitalMode(
|
||||
"packet",
|
||||
|
@ -9,43 +9,19 @@ from owrx.config import Config
|
||||
from owrx.version import openwebrx_version
|
||||
from owrx.locator import Locator
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
from owrx.reporting import Reporter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PskReporterDummy(object):
|
||||
"""
|
||||
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()
|
||||
class PskReporter(Reporter):
|
||||
interval = 300
|
||||
supportedModes = ["FT8", "FT4", "JT9", "JT65", "FST4", "FST4W", "JS8"]
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
with PskReporter.creationLock:
|
||||
if PskReporter.sharedInstance is None:
|
||||
if Config.get()["pskreporter_enabled"]:
|
||||
PskReporter.sharedInstance = PskReporter()
|
||||
else:
|
||||
PskReporter.sharedInstance = PskReporterDummy()
|
||||
return PskReporter.sharedInstance
|
||||
def getSupportedModes(self):
|
||||
return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8"]
|
||||
|
||||
@staticmethod
|
||||
def stop():
|
||||
if PskReporter.sharedInstance:
|
||||
PskReporter.sharedInstance.cancelTimer()
|
||||
def stop(self):
|
||||
self.cancelTimer()
|
||||
|
||||
def __init__(self):
|
||||
self.spots = []
|
||||
@ -72,8 +48,6 @@ class PskReporter(object):
|
||||
return reduce(and_, map(lambda key: s1[key] == s2[key], keys))
|
||||
|
||||
def spot(self, spot):
|
||||
if not spot["mode"] in PskReporter.supportedModes:
|
||||
return
|
||||
with self.spotLock:
|
||||
if any(x for x in self.spots if self.spotEquals(spot, x)):
|
||||
# dupe
|
||||
|
56
owrx/reporting.py
Normal file
56
owrx/reporting.py
Normal 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)
|
@ -75,9 +75,38 @@ class SdrSource(ABC):
|
||||
self.state = SdrSource.STATE_STOPPED
|
||||
self.busyState = SdrSource.BUSYSTATE_IDLE
|
||||
|
||||
self.validateProfiles()
|
||||
|
||||
if self.isAlwaysOn():
|
||||
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):
|
||||
return "always-on" in self.props and self.props["always-on"]
|
||||
|
||||
@ -119,12 +148,7 @@ class SdrSource(ABC):
|
||||
profile = profiles[profile_id]
|
||||
self.profile_id = profile_id
|
||||
|
||||
layer = PropertyLayer()
|
||||
for (key, value) in profile.items():
|
||||
# skip the name, that would overwrite the source name.
|
||||
if key == "name":
|
||||
continue
|
||||
layer[key] = value
|
||||
layer = self._getProfilePropertyLayer(profile)
|
||||
self.props.replaceLayer(0, layer)
|
||||
|
||||
def getId(self):
|
||||
|
@ -32,3 +32,7 @@ class Resampler(DirectSource):
|
||||
def activateProfile(self, profile_id=None):
|
||||
logger.warning("Resampler does not support setting profiles")
|
||||
pass
|
||||
|
||||
def validateProfiles(self):
|
||||
# resampler does not support profiles
|
||||
pass
|
||||
|
58
owrx/wsjt.py
58
owrx/wsjt.py
@ -2,7 +2,7 @@ from datetime import datetime, timezone
|
||||
from owrx.map import Map, LocatorLocation
|
||||
import re
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
from owrx.pskreporter import PskReporter
|
||||
from owrx.reporting import ReportingEngine
|
||||
from owrx.parser import Parser
|
||||
from owrx.audio import AudioChopperProfile
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
@ -156,19 +156,24 @@ class WsjtParser(Parser):
|
||||
return
|
||||
|
||||
mode = profile.getMode()
|
||||
if mode == "WSPR":
|
||||
decoder = WsprDecoder(profile)
|
||||
if mode in ["WSPR", "FST4W"]:
|
||||
messageParser = BeaconMessageParser()
|
||||
else:
|
||||
decoder = Jt9Decoder(profile)
|
||||
messageParser = QsoMessageParser()
|
||||
if mode == "WSPR":
|
||||
decoder = WsprDecoder(profile, messageParser)
|
||||
else:
|
||||
decoder = Jt9Decoder(profile, messageParser)
|
||||
out = decoder.parse(msg, freq)
|
||||
out["mode"] = mode
|
||||
out["interval"] = profile.getInterval()
|
||||
|
||||
self.pushDecode(mode)
|
||||
if "callsign" in out and "locator" in out:
|
||||
Map.getSharedInstance().updateLocation(
|
||||
out["callsign"], LocatorLocation(out["locator"]), mode, self.band
|
||||
)
|
||||
PskReporter.getSharedInstance().spot(out)
|
||||
ReportingEngine.getSharedInstance().spot(out)
|
||||
|
||||
self.handler.write_wsjt_message(out)
|
||||
except (ValueError, IndexError):
|
||||
@ -195,10 +200,9 @@ class WsjtParser(Parser):
|
||||
|
||||
|
||||
class Decoder(ABC):
|
||||
locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$")
|
||||
|
||||
def __init__(self, profile):
|
||||
def __init__(self, profile, messageParser):
|
||||
self.profile = profile
|
||||
self.messageParser = messageParser
|
||||
|
||||
def parse_timestamp(self, instring):
|
||||
dateformat = self.profile.getTimestampFormat()
|
||||
@ -215,8 +219,19 @@ class Decoder(ABC):
|
||||
def parse(self, msg, dial_freq):
|
||||
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:
|
||||
return {}
|
||||
# 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)}
|
||||
|
||||
|
||||
# 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):
|
||||
def parse(self, msg, dial_freq):
|
||||
# ft8 sample
|
||||
@ -245,13 +271,11 @@ class Jt9Decoder(Decoder):
|
||||
"freq": dial_freq + int(msg[9:13]),
|
||||
"msg": wsjt_msg,
|
||||
}
|
||||
result.update(self.parseMessage(wsjt_msg))
|
||||
result.update(self.messageParser.parse(wsjt_msg))
|
||||
return result
|
||||
|
||||
|
||||
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):
|
||||
# wspr sample
|
||||
# '2600 -24 0.4 0.001492 -1 G8AXA JO01 33'
|
||||
@ -266,11 +290,5 @@ class WsprDecoder(Decoder):
|
||||
"drift": int(msg[20:23]),
|
||||
"msg": wsjt_msg,
|
||||
}
|
||||
result.update(self.parseMessage(wsjt_msg))
|
||||
result.update(self.messageParser.parse(wsjt_msg))
|
||||
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
90
owrx/wsprnet.py
Normal 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"]
|
Loading…
Reference in New Issue
Block a user