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
|
- 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
|
||||||
|
@ -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.
|
||||||
|
@ -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
2
debian/changelog
vendored
@ -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
2
debian/control
vendored
@ -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
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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()">
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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
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>
|
<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>
|
||||||
|
@ -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)';
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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()
|
||||||
|
@ -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/)
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
|
@ -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
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.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):
|
||||||
|
@ -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
|
||||||
|
58
owrx/wsjt.py
58
owrx/wsjt.py
@ -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
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