71 Commits

Author SHA1 Message Date
d4d8699fc5 squelch bar for firefox, too 2019-10-27 16:06:06 +01:00
e8d60e2dc0 animate the squelch slider background 2019-10-27 16:04:00 +01:00
944e9df7cc fix slider mousewheel action 2019-10-27 15:09:34 +01:00
cd2da582c4 fix slider background for firefox 2019-10-27 14:58:46 +01:00
1e28fc5018 fix broken widths on digital meta panels 2019-10-27 13:18:00 +01:00
a24cb3e04a shutdown services properly 2019-10-27 12:16:17 +01:00
13f27a76ff use new way of measuring for network speed, too 2019-10-26 22:44:54 +02:00
39120d9413 implement new way of measuring stats that allows arbitrary timeranges 2019-10-26 22:32:25 +02:00
fe08228204 rework panel code to use less javascript and more css for positioning 2019-10-26 21:32:00 +02:00
c7eb5c430c perform binary decoding on the server side 2019-10-25 21:09:31 +02:00
70e2a99274 custom easing to restore the original fadeout 2019-10-25 21:09:31 +02:00
52b945cd64 optimize 2019-10-25 16:52:10 +02:00
07a8e6bf92 add a title to show what the bookmark button does on hover 2019-10-24 20:06:24 +02:00
afa322a83b mousewheel control for the sliders <3 2019-10-24 20:00:30 +02:00
d3ac44c526 replace custom animations with jquery 2019-10-24 19:35:55 +02:00
5bbee1e1d7 fix some more minor javascript issues 2019-10-23 11:27:05 +02:00
58da0e8a60 remove debugging code 2019-10-22 22:38:08 +02:00
713b6119d0 refactor progressbars into objects 2019-10-22 22:35:54 +02:00
ebf2804d63 rename 2019-10-22 21:30:48 +02:00
3b77753829 ignore IDE files 2019-10-21 22:09:18 +02:00
eb29d0ac99 protect websocket handling from any exceptions 2019-10-21 22:08:37 +02:00
6cdec05cde remove unused variables 2019-10-21 01:16:19 +02:00
7ef0ef0d7c don't split ringbuffer blocks in the output; this means up to 3ms stay
in the buffer.
2019-10-20 23:48:49 +02:00
dd7d262bd3 fixing some issues with the IDE 2019-10-20 23:38:58 +02:00
13d7686258 refactor all the audio stuff into classes and a separate file 2019-10-20 18:53:23 +02:00
91b8c55de9 optimize 2019-10-20 13:28:25 +02:00
00c5467a89 implement a ringbuffer in the audioworklet to optimize runtimes 2019-10-19 18:09:50 +02:00
cc32e28b36 use the raw object name 2019-10-19 13:09:41 +02:00
72329a8a2a use a GainNode for volume control instead of custom code, thus improving
the feedback
2019-10-19 12:58:09 +02:00
a102ee181a show wht method is being used in the log; fix console errors; 2019-10-19 12:39:42 +02:00
778591d460 an attempt to implement audioworklets was made. works mostly, but skips
samples
2019-10-19 01:19:19 +02:00
6bc928b5b6 fine-tune audio buffering 2019-10-18 21:34:00 +02:00
0b2c457030 kill client-side early rebuffering, improving the latency 2019-10-18 21:13:48 +02:00
93d4e629d1 more bookmarks 2019-10-17 19:28:05 +02:00
d53d3b7a51 clean up javascript as good as possible with the help of the IDE 2019-10-16 17:11:09 +02:00
72062c8570 let's apply some formatting 2019-10-16 13:17:47 +02:00
de90219406 dynamically calculate audio block size (improving latency) 2019-10-15 19:50:24 +02:00
de179d070d this is not theoretical any more 2019-10-13 18:28:58 +02:00
f45857f79b don't use the resampler if the optimization says so 2019-10-13 18:25:32 +02:00
eda556ef03 prevent start-up of services if requirements are not fulfilled.
closes #4
2019-10-13 17:51:00 +02:00
ea67340cab display message when sdr unavailable 2019-10-13 14:17:32 +02:00
5b61f8c7a3 show message in log 2019-10-12 20:48:36 +02:00
70d8fe82b3 send failure message to client 2019-10-12 20:46:32 +02:00
fce8c294d3 first work at detecting failed sdr devices 2019-10-12 20:19:34 +02:00
8541f79ebc remove dial button 2019-10-12 17:34:49 +02:00
ec4fd401cb update dropdown, too 2019-10-12 17:26:57 +02:00
98217b1745 dial frequencies as bookmarks 2019-10-12 17:14:28 +02:00
378c574eed even more bookmarks 2019-10-12 17:02:39 +02:00
e5193f3460 remove old code 2019-10-12 17:02:29 +02:00
60e90575ac refactor bookmarks into a self-contained javascript 2019-10-12 17:02:04 +02:00
78ffa6f184 remove ids 2019-10-11 12:15:01 +02:00
f9f50e734f improved websocket handling 2019-10-11 12:08:43 +02:00
2e75bac90c more bookmarks 2019-10-11 12:08:19 +02:00
8c2f081cb0 scale the background for large monitors 2019-10-06 14:22:49 +02:00
6adbc6c291 Merge pull request #16 from d9394/develop
explicitly specify encoding since the default is platform-dependent
2019-10-06 11:01:22 +02:00
db663fe134 Update controllers.py
fix a bug with reading template file
2019-10-06 16:05:30 +08:00
2e394dc2cb remove waterfall queueing 2019-10-05 20:38:58 +02:00
b80fd9c023 update profile dropdown box on changes 2019-10-04 22:01:07 +02:00
3e25f1ec42 fix dialog flexbox layout (especially for firefox) 2019-10-04 00:56:46 +02:00
351f63f0b8 improve receiver button alignment 2019-10-04 00:17:40 +02:00
9f90d01dc6 simplify icon display 2019-10-03 23:55:04 +02:00
71d815cf08 trim config 2019-10-03 23:35:36 +02:00
a168136102 remove from config, too 2019-10-03 18:11:25 +02:00
e9f9bbb9c0 replace receiver_qra setting with locator calculation 2019-10-03 18:10:46 +02:00
3e8e2182a8 fix many, many problems with the frontend frequency displays, scroll and
drag handling, closes #13
2019-10-03 17:24:28 +02:00
2025ccb366 catch more generic OSError 2019-10-03 00:58:27 +02:00
6ae934e461 initialize demodulator with configured start values, fixes #9 2019-10-03 00:36:26 +02:00
7431e4d7c0 restart dsp chain on output_rate change, fixes #8 2019-10-03 00:14:05 +02:00
eb0f54e79d reset status values properly on reconnect 2019-10-02 23:48:13 +02:00
08e9520019 reduce png size by using indexed colors 2019-10-02 18:13:33 +02:00
630a542ed6 better websocket header handling 2019-10-02 11:28:41 +02:00
23 changed files with 2949 additions and 2969 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.pyc *.pyc
*.swp *.swp
tags tags
.idea

View File

@ -1,65 +1,222 @@
[{ [
"name": "DB0ZU", {
"frequency": 145725000, "name": "DB0ZU",
"modulation": "nfm" "frequency": 145725000,
},{ "modulation": "nfm"
"name": "DB0ZM", },
"frequency": 145750000, {
"modulation": "nfm" "name": "DB0ZM",
},{ "frequency": 145750000,
"name": "DM0ULR", "modulation": "nfm"
"frequency": 145787500, },
"modulation": "nfm" {
},{ "name": "DM0ULR",
"name": "DB0EL", "frequency": 145787500,
"frequency": 439275000, "modulation": "nfm"
"modulation": "nfm" },
},{ {
"name": "DB0NJ", "name": "DB0EL",
"frequency": 438775000, "frequency": 439275000,
"modulation": "nfm" "modulation": "nfm"
},{ },
"name": "DB0NJ", {
"frequency": 439437500, "name": "DB0NJ",
"modulation": "dmr" "frequency": 438775000,
},{ "modulation": "nfm"
"name": "DB0UFO", },
"frequency": 438312500, {
"modulation": "dmr" "name": "DB0NJ",
},{ "frequency": 439437500,
"name": "DB0PV", "modulation": "dmr"
"frequency": 438525000, },
"modulation": "ysf" {
},{ "name": "DB0UFO",
"name": "DB0BZA", "frequency": 438312500,
"frequency": 438412500, "modulation": "dmr"
"modulation": "ysf" },
},{ {
"name": "DB0OSH", "name": "DB0PV",
"frequency": 438250000, "frequency": 438525000,
"modulation": "ysf" "modulation": "ysf"
},{ },
"name": "DB0ULR", {
"frequency": 439325000, "name": "DB0BZA",
"modulation": "nfm" "frequency": 438412500,
},{ "modulation": "ysf"
"name": "DB0ZU", },
"frequency": 438850000, {
"modulation": "nfm" "name": "DB0OSH",
},{ "frequency": 438250000,
"name": "DB0ISW", "modulation": "ysf"
"frequency": 438650000, },
"modulation": "nfm" {
},{ "name": "DB0ULR",
"name": "Radio DARC", "frequency": 439325000,
"frequency": 6070000, "modulation": "nfm"
"modulation": "am" },
},{ {
"name": "DB0TVM", "name": "DB0ZU",
"frequency": 439575000, "frequency": 438850000,
"modulation": "dstar" "modulation": "nfm"
},{ },
"name": "DB0TVM", {
"frequency": 439800000, "name": "DB0ISW",
"modulation": "dmr" "frequency": 438650000,
}] "modulation": "nfm"
},
{
"name": "Radio DARC",
"frequency": 6070000,
"modulation": "am"
},
{
"name": "DB0TVM",
"frequency": 439575000,
"modulation": "dstar"
},
{
"name": "DB0TVM",
"frequency": 439800000,
"modulation": "dmr"
},
{
"name": "DB0TR",
"frequency": 438700000,
"modulation": "nfm"
},
{
"name": "DB0PME",
"frequency": 439825000,
"modulation": "dmr"
},
{
"name": "DB0HKN",
"frequency": 438300000,
"modulation": "dmr"
},
{
"name": "OE2XHM",
"frequency": 438825000,
"modulation": "nfm"
},
{
"name": "DM0WW",
"frequency": 438962500,
"modulation": "dmr"
},
{
"name": "OE7XXR",
"frequency": 438200000,
"modulation": "dstar"
},
{
"name": "OE2XZR",
"frequency": 439000000,
"modulation": "dstar"
},
{
"name": "DB0OAL",
"frequency": 439912500,
"modulation": "dmr"
},
{
"name": "DB0AAT",
"frequency": 439550000,
"modulation": "dmr"
},
{
"name": "DB0FSG",
"frequency": 439937500,
"modulation": "dmr"
},
{
"name": "Pocsag",
"frequency": 439987500,
"modulation": "nfm"
},
{
"name": "DB0ULR",
"frequency": 145575000,
"modulation": "nfm"
},
{
"name": "DB0RDH",
"frequency": 145737500,
"modulation": "dstar"
},
{
"name": "DM0GAP",
"frequency": 145612500,
"modulation": "nfm"
},
{
"name": "DB0XF",
"frequency": 145600000,
"modulation": "nfm"
},
{
"name": "DB0TOL",
"frequency": 145712500,
"modulation": "nfm"
},
{
"name": "DB0TTB",
"frequency": 439587500,
"modulation": "dmr"
},
{
"name": "DB0TRS",
"frequency": 439125000,
"modulation": "nfm"
},
{
"name": "DB0OAL",
"frequency": 438937500,
"modulation": "nfm"
},
{
"name": "DM0ULR",
"frequency": 439337500,
"modulation": "nxdn"
},
{
"name": "DB0MIR",
"frequency": 439300000,
"modulation": "nfm"
},
{
"name": "DB0PM",
"frequency": 439075000,
"modulation": "nfm"
},
{
"name": "DB0CP",
"frequency": 439025000,
"modulation": "nfm"
},
{
"name": "OE7XGR",
"frequency": 438925000,
"modulation": "dmr"
},
{
"name": "DB0TOL",
"frequency": 438725000,
"modulation": "nfm"
},
{
"name": "DB0OAL",
"frequency": 438325000,
"modulation": "dstar"
},
{
"name": "DB0ROL",
"frequency": 439237500,
"modulation": "nfm"
},
{
"name": "DB0ABX",
"frequency": 439137500,
"modulation": "nfm"
}
]

View File

@ -41,13 +41,9 @@ max_clients = 20
# ==== Web GUI configuration ==== # ==== Web GUI configuration ====
receiver_name = "[Callsign]" receiver_name = "[Callsign]"
receiver_location = "Budapest, Hungary" receiver_location = "Budapest, Hungary"
receiver_qra = "JN97ML"
receiver_asl = 200 receiver_asl = 200
receiver_ant = "Longwire"
receiver_device = "RTL-SDR"
receiver_admin = "example@example.com" receiver_admin = "example@example.com"
receiver_gps = (47.000000, 19.000000) receiver_gps = (47.000000, 19.000000)
photo_height = 350
photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory" photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
photo_desc = """ photo_desc = """
You can add your own background photo and receiver information.<br /> You can add your own background photo and receiver information.<br />
@ -181,17 +177,10 @@ sdrs = {
}, },
}, },
}, },
# this one is just here to test feature detection
"test": {"type": "test"},
} }
# ==== Misc settings ==== # ==== Misc settings ====
client_audio_buffer_size = 5
# increasing client_audio_buffer_size will:
# - also increase the latency
# - decrease the chance of audio underruns
iq_port_range = [ iq_port_range = [
4950, 4950,
4960, 4960,

17
csdr.py
View File

@ -391,6 +391,15 @@ class dsp(object):
def set_audio_compression(self, what): def set_audio_compression(self, what):
self.audio_compression = what self.audio_compression = what
def get_audio_bytes_to_read(self):
# desired latency: 5ms
# uncompressed audio has 16 bits = 2 bytes per sample
base = self.output_rate * 0.005 * 2
# adpcm compresses the bitstream by 4
if self.audio_compression == "adpcm":
base = base / 4
return int(base)
def set_fft_compression(self, what): def set_fft_compression(self, what):
self.fft_compression = what self.fft_compression = what
@ -398,7 +407,7 @@ class dsp(object):
if self.fft_compression == "none": if self.fft_compression == "none":
return self.fft_size * 4 return self.fft_size * 4
if self.fft_compression == "adpcm": if self.fft_compression == "adpcm":
return (self.fft_size / 2) + (10 / 2) return int((self.fft_size / 2) + (10 / 2))
def get_secondary_fft_bytes_to_read(self): def get_secondary_fft_bytes_to_read(self):
if self.fft_compression == "none": if self.fft_compression == "none":
@ -455,8 +464,11 @@ class dsp(object):
return demodulator == "packet" return demodulator == "packet"
def set_output_rate(self, output_rate): def set_output_rate(self, output_rate):
if self.output_rate == output_rate:
return
self.output_rate = output_rate self.output_rate = output_rate
self.calculate_decimation() self.calculate_decimation()
self.restart()
def set_demodulator(self, demodulator): def set_demodulator(self, demodulator):
if self.demodulator == demodulator: if self.demodulator == demodulator:
@ -647,7 +659,8 @@ class dsp(object):
self.output.send_output( self.output.send_output(
"audio", "audio",
partial( partial(
self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256 self.process.stdout.read,
self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(),
), ),
) )

View File

@ -53,21 +53,13 @@
padding: 15px; padding: 15px;
} }
#webrx-rx-avatar-background
{
cursor:pointer;
background-image: url(../gfx/openwebrx-avatar-background.png);
background-origin: content-box;
background-repeat: no-repeat;
float: left;
width: 54px;
height: 54px;
padding: 7px;
box-sizing: content-box;
}
#webrx-rx-avatar #webrx-rx-avatar
{ {
background-color: rgba(154, 154, 154, .5);
border-radius: 7px;
float: left;
margin: 7px;
cursor:pointer; cursor:pointer;
width: 46px; width: 46px;
height: 46px; height: 46px;

View File

@ -39,6 +39,8 @@ input[type=range]
{ {
-webkit-appearance: none; -webkit-appearance: none;
margin: 0 0; margin: 0 0;
background: transparent;
--track-background: #B6B6B6;
} }
input[type=range]:focus input[type=range]:focus
{ {
@ -54,6 +56,7 @@ input[type=range]::-webkit-slider-runnable-track
background: #B6B6B6; background: #B6B6B6;
/*border-radius: 11px;*/ /*border-radius: 11px;*/
border: 1px solid #8A8A8A; border: 1px solid #8A8A8A;
background: var(--track-background);
} }
input[type=range]::-webkit-slider-thumb input[type=range]::-webkit-slider-thumb
@ -72,6 +75,7 @@ input[type=range]::-webkit-slider-thumb
input[type=range]:focus::-webkit-slider-runnable-track input[type=range]:focus::-webkit-slider-runnable-track
{ {
background: #B6B6B6; background: #B6B6B6;
background: var(--track-background);
} }
input[type=range]::-moz-range-track input[type=range]::-moz-range-track
@ -81,6 +85,7 @@ input[type=range]::-moz-range-track
animate: 0.2s; animate: 0.2s;
box-shadow: 0px 0px 0px #000000; box-shadow: 0px 0px 0px #000000;
background: #B6B6B6; background: #B6B6B6;
background: var(--track-background);
border-radius: 11px; border-radius: 11px;
border: 1px solid #8A8A8A; border: 1px solid #8A8A8A;
} }
@ -146,8 +151,10 @@ input[type=range]:focus::-ms-fill-upper
#webrx-page-container #webrx-page-container
{ {
min-height:100%; height: 100%;
position:relative; position: relative;
display: flex;
flex-direction: column;
} }
#openwebrx-scale-container #openwebrx-scale-container
@ -243,18 +250,23 @@ input[type=range]:focus::-ms-fill-upper
border-top-color: #0FF; border-top-color: #0FF;
} }
#openwebrx-bookmarks-container .bookmark[data-source=dial_frequencies] {
background-color: #0F0;
}
#openwebrx-bookmarks-container .bookmark[data-source=dial_frequencies]:after {
border-top-color: #0F0;
}
#webrx-canvas-container #webrx-canvas-container
{ {
/*background-image:url('../gfx/openwebrx-blank-background-1.jpg');*/ flex-grow: 1;
position: relative; position: relative;
height: 2000px; overflow: hidden;
overflow-y: scroll;
overflow-x: hidden;
/*background-color: #646464;*/
/*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/
background-image: url('../gfx/openwebrx-background-cool-blue.png'); background-image: url('../gfx/openwebrx-background-cool-blue.png');
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: #1e5f7f; background-color: #1e5f7f;
background-size: cover;
cursor: crosshair; cursor: crosshair;
} }
@ -269,24 +281,11 @@ input[type=range]:focus::-ms-fill-upper
#openwebrx-mathbox-container #openwebrx-mathbox-container
{ {
flex-grow: 1;
overflow: none; overflow: none;
display: none; display: none;
} }
#openwebrx-phantom-canvas
{
position: absolute;
width: 0px;
height: 0px;
}
/*#openwebrx-canvas-gradient-background
{
overflow: hidden;
width: 100%;
height: 396px;
}*/
#openwebrx-log-scroll #openwebrx-log-scroll
{ {
/*overflow-y:auto;*/ /*overflow-y:auto;*/
@ -297,32 +296,12 @@ input[type=range]:focus::-ms-fill-upper
.nano .nano-pane { background: #444; } .nano .nano-pane { background: #444; }
.nano .nano-slider { background: #eee !important; } .nano .nano-slider { background: #eee !important; }
#webrx-main-container
{
position: relative;
width: 100%;
margin: 0;
padding: 0;
}
.webrx-error .webrx-error
{ {
font-weight: bold; font-weight: bold;
color: #ff6262; color: #ff6262;
} }
#openwebrx-problems span
{
background: #ff6262;
padding: 3px;
font-size: 8pt;
color: white;
font-weight: bold;
border-radius: 4px;
-moz-border-radius: 4px;
margin: 0px 2px 0px 2px;
}
/*#webrx-freq-show /*#webrx-freq-show
{ {
visibility: hidden; visibility: hidden;
@ -377,18 +356,35 @@ input[type=range]:focus::-ms-fill-upper
margin-bottom: 5px; margin-bottom: 5px;
} }
#openwebrx-panels-container-left,
#openwebrx-panels-container-right {
position: absolute;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
#openwebrx-panels-container-left {
left: 0;
align-items: flex-start;
}
#openwebrx-panels-container-right {
right: 0;
align-items: flex-end;
}
.openwebrx-panel .openwebrx-panel
{ {
transform: perspective( 600px ) rotateX( 90deg ); transform: perspective( 600px ) rotateX( 90deg );
visibility: hidden;
background-color: #575757; background-color: #575757;
padding: 10px; padding: 10px;
color: white; color: white;
position: fixed;
font-size: 10pt; font-size: 10pt;
border-radius: 15px; border-radius: 15px;
-moz-border-radius: 15px; -moz-border-radius: 15px;
margin: 5.9px;
} }
.openwebrx-panel a .openwebrx-panel a
@ -439,26 +435,18 @@ input[type=range]:focus::-ms-fill-upper
color: #FFFF50; color: #FFFF50;
} }
.openwebrx-button:last-child {
margin-right: 0;
}
.openwebrx-demodulator-button .openwebrx-demodulator-button
{ {
width: 38px; width: 38px;
height: 19px; height: 19px;
font-size: 12pt; font-size: 12pt;
text-align: center; text-align: center;
} flex: 1;
margin-right: 5px;
.openwebrx-dial-button svg {
width: 19px;
height: 19px;
vertical-align: bottom;
}
.openwebrx-dial-button #ph_dial {
fill: #888;
}
.openwebrx-dial-button.available #ph_dial {
fill: #FFF;
} }
.openwebrx-square-button img .openwebrx-square-button img
@ -554,7 +542,7 @@ img.openwebrx-mirror-img
#openwebrx-panel-status #openwebrx-panel-status
{ {
margin: 0px; margin: 0 0 0 5.9px;
padding: 0px; padding: 0px;
background-color:rgba(0, 0, 0, 0); background-color:rgba(0, 0, 0, 0);
} }
@ -615,6 +603,11 @@ img.openwebrx-mirror-img
padding-top: 5px; padding-top: 5px;
} }
.openwebrx-panel-flex-line {
display: flex;
flex-direction: row;
}
.openwebrx-panel-line:first-child { .openwebrx-panel-line:first-child {
padding-top: 0; padding-top: 0;
} }
@ -725,6 +718,7 @@ img.openwebrx-mirror-img
width: 173px; width: 173px;
height: 27px; height: 27px;
padding-left:3px; padding-left:3px;
flex: 4;
} }
#openwebrx-sdr-profiles-listbox { #openwebrx-sdr-profiles-listbox {
@ -1021,7 +1015,8 @@ img.openwebrx-mirror-img
.openwebrx-dialog label { .openwebrx-dialog label {
display: inline-block; display: inline-block;
flex: 1 0 20px; flex-grow: 0;
width: 70px;
padding-right: 20px; padding-right: 20px;
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
@ -1029,7 +1024,7 @@ img.openwebrx-mirror-img
.openwebrx-dialog .form-field input, .openwebrx-dialog .form-field input,
.openwebrx-dialog .form-field select { .openwebrx-dialog .form-field select {
flex: 2 0 20px; flex-grow: 1;
height: 27px; height: 27px;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -4,9 +4,7 @@
<div id="webrx-top-bar" class="webrx-top-bar-parts"> <div id="webrx-top-bar" class="webrx-top-bar-parts">
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a> <a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a> <a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
<div id="webrx-rx-avatar-background"> <img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png"/>
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png"/>
</div>
<div id="webrx-rx-texts"> <div id="webrx-rx-texts">
<div id="webrx-rx-title" 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 id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>

View File

@ -27,6 +27,10 @@
<script src="static/openwebrx.js"></script> <script src="static/openwebrx.js"></script>
<script src="static/lib/jquery-3.2.1.min.js"></script> <script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/jquery.nanoscroller.js"></script> <script src="static/lib/jquery.nanoscroller.js"></script>
<script src="static/lib/BookmarkBar.js"></script>
<script src="static/lib/AudioEngine.js"></script>
<script src="static/lib/ProgressBar.js"></script>
<script src="static/lib/Measurement.js"></script>
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" /> <link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" /> <link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
<meta charset="utf-8"> <meta charset="utf-8">
@ -34,26 +38,107 @@
<body onload="openwebrx_init();"> <body onload="openwebrx_init();">
<div id="webrx-page-container"> <div id="webrx-page-container">
${header} ${header}
<div id="webrx-main-container"> <div id="openwebrx-frequency-container">
<div id="openwebrx-frequency-container"> <div id="openwebrx-bookmarks-container"></div>
<div id="openwebrx-bookmarks-container"></div> <div id="openwebrx-scale-container">
<div id="openwebrx-scale-container"> <canvas id="openwebrx-scale-canvas" width="0" height="0"></canvas>
<canvas id="openwebrx-scale-canvas" width="0" height="0"></canvas> </div>
</div>
<div id="openwebrx-mathbox-container"> </div>
<div id="webrx-canvas-container">
<!-- add canvas here by javascript -->
</div>
<div id="openwebrx-panels-container">
<div id="openwebrx-panels-container-left">
<div class="openwebrx-panel" data-panel-name="client-under-devel" style="width: 245px; background-color: Red;">
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
<br />We're working on the code right now, so the application might fail.
</div>
<div class="openwebrx-panel" id="openwebrx-panel-digimodes" style="display: none; width: 619px;" data-panel-name="digimodes">
<div id="openwebrx-digimode-canvas-container">
<div id="openwebrx-digimode-select-channel"></div>
</div>
<div id="openwebrx-digimode-content-container">
<div class="gradient"></div>
<div id="openwebrx-digimode-content">
<span id="openwebrx-cursor-blink"></span>
</div>
</div>
</div>
<table class="openwebrx-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message">
<thead><tr>
<th>UTC</th>
<th class="decimal">dB</th>
<th class="decimal">DT</th>
<th class="decimal freq">Freq</th>
<th class="message">Message</th>
</tr></thead>
<tbody></tbody>
</table>
<table class="openwebrx-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message">
<thead><tr>
<th>UTC</th>
<th class="callsign">Callsign</th>
<th class="coord">Coord</th>
<th class="message">Comment</th>
</tr></thead>
<tbody></tbody>
</table>
<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>
</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>
</div>
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" style="width: 619px;">
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
<div class="nano-content">
<div id="openwebrx-client-log-title">OpenWebRX client log</strong></div>
<span id="openwebrx-client-1">Author: </span><a href="http://blog.sdr.hu/about" target="_blank">András Retzler, HA7ILM</a><br />You can support OpenWebRX development via <a href="http://blog.sdr.hu/support" target="_blank">PayPal!</a><br/>
<div id="openwebrx-debugdiv"></div>
</div>
</div>
</div>
<div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" style="width: 615px;" data-panel-transparent="true">
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer"> <span class="openwebrx-progressbar-text">Audio buffer [0 ms]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output"> <span class="openwebrx-progressbar-text">Audio output [0 sps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed"> <span class="openwebrx-progressbar-text">Audio stream [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed"> <span class="openwebrx-progressbar-text">Network usage [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div>
</div> </div>
</div> </div>
<div id="openwebrx-mathbox-container"> </div> <div id="openwebrx-panels-container-right">
<div id="webrx-canvas-container"> <div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" style="width: 259px;">
<div id="openwebrx-phantom-canvas"></div>
<!-- add canvas here by javascript -->
</div>
<div id="openwebrx-panels-container">
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" data-panel-pos="right" data-panel-order="0" data-panel-size="259,115">
<div class="openwebrx-panel-line frequencies-container"> <div class="openwebrx-panel-line frequencies-container">
<div class="frequencies"> <div class="frequencies">
<div id="webrx-actual-freq">---.--- MHz</div> <div id="webrx-actual-freq">---.--- MHz</div>
<div id="webrx-mouse-freq">---.--- MHz</div> <div id="webrx-mouse-freq">---.--- MHz</div>
</div> </div>
<div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;"> <div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark...">
<img src="static/gfx/openwebrx-bookmark.png"> <img src="static/gfx/openwebrx-bookmark.png">
</div> </div>
</div> </div>
@ -61,31 +146,33 @@
<select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();"> <select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();">
</select> </select>
</div> </div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line openwebrx-panel-flex-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm" <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
onclick="demodulator_analog_replace('nfm');">FM</div> onclick="demodulator_analog_replace('nfm');">FM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-am" <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-am"
onclick="demodulator_analog_replace('am');">AM</div> onclick="demodulator_analog_replace('am');">AM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-lsb" <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-lsb"
onclick="demodulator_analog_replace('lsb');">LSB</div> onclick="demodulator_analog_replace('lsb');">LSB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-usb" <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-usb"
onclick="demodulator_analog_replace('usb');">USB</div> onclick="demodulator_analog_replace('usb');">USB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw" <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
onclick="demodulator_analog_replace('cw');">CW</div> onclick="demodulator_analog_replace('cw');">CW</div>
</div>
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dmr" <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dmr"
style="display:none;" data-feature="digital_voice_digiham" style="display:none;" data-feature="digital_voice_digiham"
onclick="demodulator_analog_replace('dmr');">DMR</div> onclick="demodulator_analog_replace('dmr');">DMR</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dstar" <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dstar"
style="display:none;" data-feature="digital_voice_dsd" style="display:none;" data-feature="digital_voice_dsd"
onclick="demodulator_analog_replace('dstar');">DStar</div> onclick="demodulator_analog_replace('dstar');">DStar</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nxdn" <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nxdn"
style="display:none;" data-feature="digital_voice_dsd" style="display:none;" data-feature="digital_voice_dsd"
onclick="demodulator_analog_replace('nxdn');">NXDN</div> onclick="demodulator_analog_replace('nxdn');">NXDN</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-ysf" <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-ysf"
style="display:none;" data-feature="digital_voice_digiham" style="display:none;" data-feature="digital_voice_digiham"
onclick="demodulator_analog_replace('ysf');">YSF</div> onclick="demodulator_analog_replace('ysf');">YSF</div>
</div> </div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line openwebrx-panel-flex-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dig" onclick="demodulator_digital_replace_last();">DIG</div> <div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dig" onclick="demodulator_digital_replace_last();">DIG</div>
<select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();"> <select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();">
<option value="none"></option> <option value="none"></option>
@ -97,13 +184,6 @@
<option value="ft4" data-feature="wsjt-x">FT4</option> <option value="ft4" data-feature="wsjt-x">FT4</option>
<option value="packet" data-feature="packet">Packet</option> <option value="packet" data-feature="packet">Packet</option>
</select> </select>
<div id="openwebrx-secondary-demod-dial-button" class="openwebrx-button openwebrx-dial-button" onclick="dial_button_click();">
<svg version="1.1" id="Layer_1" x="0px" y="0px" width="246px" height="246px" viewBox="0 0 246 246" xmlns="http://www.w3.org/2000/svg">
<g id="ph_dial_1_" transform="matrix(1, 0, 0, 1, -45.398312, -50.931698)">
<path id="ph_dial" d="M238.875,190.125c3.853,7.148,34.267,4.219,50.242,2.145c0.891-5.977,1.508-12.043,1.508-18.27 c0-67.723-54.901-122.625-122.625-122.625c-67.723,0-122.625,54.902-122.625,122.625c0,67.723,54.902,122.625,122.625,122.625 c51.06,0,94.797-31.227,113.25-75.609c-13.969-9.668-41.625-18.891-41.625-18.891c-5.25,0-10.5-3-12.75-8.25 S233.625,180.375,238.875,190.125z M220.465,175.313c0,28.478-23.086,51.563-51.563,51.563c-28.478,0-51.563-23.086-51.563-51.563 c0-28.477,23.086-51.563,51.563-51.563C197.379,123.75,220.465,146.836,220.465,175.313z M185.25,64.125 c10.563,0,19.125,8.563,19.125,19.125s-8.563,19.125-19.125,19.125c-10.562,0-19.125-8.563-19.125-19.125 S174.688,64.125,185.25,64.125z M142.875,69C153.438,69,162,77.563,162,88.125s-8.563,19.125-19.125,19.125 c-10.562,0-19.125-8.563-19.125-19.125S132.313,69,142.875,69z M106.5,91.875c10.563,0,19.125,8.563,19.125,19.125 s-8.563,19.125-19.125,19.125c-10.562,0-19.125-8.562-19.125-19.125S95.938,91.875,106.5,91.875z M81.375,126.75 c10.563,0,19.125,8.563,19.125,19.125S91.938,165,81.375,165c-10.563,0-19.125-8.563-19.125-19.125S70.813,126.75,81.375,126.75z M58.125,188.625c0-10.559,8.563-19.125,19.125-19.125c10.563,0,19.125,8.566,19.125,19.125S87.813,207.75,77.25,207.75 C66.687,207.75,58.125,199.184,58.125,188.625z M75.75,229.875c0-10.559,8.563-19.125,19.125-19.125 c10.563,0,19.125,8.566,19.125,19.125S105.438,249,94.875,249C84.312,249,75.75,240.434,75.75,229.875z M126.375,276 c-10.563,0-19.125-8.566-19.125-19.125s8.563-19.125,19.125-19.125c10.563,0,19.125,8.566,19.125,19.125S136.938,276,126.375,276z M168,288c-10.563,0-19.125-8.566-19.125-19.125S157.438,249.75,168,249.75c10.563,0,19.125,8.566,19.125,19.125 S178.563,288,168,288z M210.375,276c-10.563,0-19.125-8.566-19.125-19.125s8.563-19.125,19.125-19.125 c10.563,0,19.125,8.566,19.125,19.125S220.938,276,210.375,276z M243.375,210.75c10.563,0,19.125,8.566,19.125,19.125 S253.938,249,243.375,249c-10.563,0-19.125-8.566-19.125-19.125S232.813,210.75,243.375,210.75z"/>
</g>
</svg>
</div>
</div> </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();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div> <div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
@ -131,90 +211,10 @@
</div> </div>
</div> </div>
</div> </div>
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" data-panel-pos="left" data-panel-order="1" data-panel-size="619,137">
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
<div class="nano-content">
<div id="openwebrx-client-log-title">OpenWebRX client log</strong><span id="openwebrx-problems"></span></div>
<span id="openwebrx-client-1">Author: </span><a href="http://blog.sdr.hu/about" target="_blank">András Retzler, HA7ILM</a><br />You can support OpenWebRX development via <a href="http://blog.sdr.hu/support" target="_blank">PayPal!</a><br/>
<div id="openwebrx-debugdiv"></div>
</div>
</div>
</div>
<div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" data-panel-pos="left" data-panel-order="0" data-panel-size="615,50" data-panel-transparent="true">
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer"> <span class="openwebrx-progressbar-text">Audio buffer [0 ms]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output"> <span class="openwebrx-progressbar-text">Audio output [0 sps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed"> <span class="openwebrx-progressbar-text">Audio stream [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed"> <span class="openwebrx-progressbar-text">Network usage [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div>
</div>
<div class="openwebrx-panel" data-panel-name="client-under-devel" data-panel-pos="left" data-panel-order="9" data-panel-size="245,55" style="background-color: Red;">
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
<br />We're working on the code right now, so the application might fail.
</div>
<div class="openwebrx-panel" id="openwebrx-panel-digimodes" data-panel-name="digimodes" data-panel-pos="left" data-panel-order="3" data-panel-size="619,210">
<div id="openwebrx-digimode-canvas-container">
<div id="openwebrx-digimode-select-channel"></div>
</div>
<div id="openwebrx-digimode-content-container">
<div class="gradient"></div>
<div id="openwebrx-digimode-content">
<span id="openwebrx-cursor-blink"></span>
</div>
</div>
</div>
<table class="openwebrx-panel" id="openwebrx-panel-wsjt-message" data-panel-name="wsjt-message" data-panel-pos="left" data-panel-order="2" data-panel-size="619,200">
<thead><tr>
<th>UTC</th>
<th class="decimal">dB</th>
<th class="decimal">DT</th>
<th class="decimal freq">Freq</th>
<th class="message">Message</th>
</tr></thead>
<tbody></tbody>
</table>
<table class="openwebrx-panel" id="openwebrx-panel-packet-message" data-panel-name="aprs-message" data-panel-pos="left" data-panel-order="2" data-panel-size="619,200">
<thead><tr>
<th>UTC</th>
<th class="callsign">Callsign</th>
<th class="coord">Coord</th>
<th class="message">Comment</th>
</tr></thead>
<tbody></tbody>
</table>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" data-panel-name="metadata-ysf" data-panel-pos="left" data-panel-order="2" data-panel-size="145,220">
<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>
</div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" data-panel-name="metadata-dmr" data-panel-pos="left" data-panel-order="2" data-panel-size="300,220">
<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>
</div>
</div> </div>
</div> </div>
</div> </div>
<div id="openwebrx-big-grey" onclick="iosPlayButtonClick();"> <div id="openwebrx-big-grey" onclick="playButtonClick();">
<div id="openwebrx-play-button-text"> <div id="openwebrx-play-button-text">
<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" /> <img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" />
<br /><br />Start OpenWebRX <br /><br />Start OpenWebRX

View File

@ -8,7 +8,7 @@ AprsMarker.prototype.draw = function() {
if (!div || !overlay) return; if (!div || !overlay) return;
if (this.symbol) { if (this.symbol) {
var tableId = this.symbol.table == '/' ? 0 : 1; var tableId = this.symbol.table === '/' ? 0 : 1;
div.style.background = 'url(/aprs-symbols/aprs-symbols-24-' + tableId + '@2x.png)'; div.style.background = 'url(/aprs-symbols/aprs-symbols-24-' + tableId + '@2x.png)';
div.style['background-size'] = '384px 144px'; div.style['background-size'] = '384px 144px';
div.style['background-position-x'] = -(this.symbol.index % 16) * 24 + 'px'; div.style['background-position-x'] = -(this.symbol.index % 16) * 24 + 'px';
@ -25,7 +25,7 @@ AprsMarker.prototype.draw = function() {
div.style.transform = null; div.style.transform = null;
} }
if (this.symbol.table != '/' && this.symbol.table != '\\') { if (this.symbol.table !== '/' && this.symbol.table !== '\\') {
overlay.style.display = 'block'; overlay.style.display = 'block';
overlay.style['background-position-x'] = -(this.symbol.tableindex % 16) * 24 + 'px'; overlay.style['background-position-x'] = -(this.symbol.tableindex % 16) * 24 + 'px';
overlay.style['background-position-y'] = -Math.floor(this.symbol.tableindex / 16) * 24 + 'px'; overlay.style['background-position-y'] = -Math.floor(this.symbol.tableindex / 16) * 24 + 'px';

230
htdocs/lib/AudioEngine.js Normal file
View File

@ -0,0 +1,230 @@
// this controls if the new AudioWorklet API should be used if available.
// the engine will still fall back to the ScriptProcessorNode if this is set to true but not available in the browser.
var useAudioWorklets = true;
function AudioEngine(maxBufferLength, audioReporter) {
this.audioReporter = audioReporter;
this.initStats();
this.resetStats();
var ctx = window.AudioContext || window.webkitAudioContext;
if (!ctx) {
return;
}
this.audioContext = new ctx();
this.allowed = this.audioContext.state === 'running';
this.started = false;
this.audioCodec = new sdrjs.ImaAdpcm();
this.compression = 'none';
this.setupResampling();
this.resampler = new sdrjs.RationalResamplerFF(this.resamplingFactor, 1);
this.maxBufferSize = maxBufferLength * this.getSampleRate();
}
AudioEngine.prototype.start = function(callback) {
var me = this;
if (me.resamplingFactor === 0) return; //if failed to find a valid resampling factor...
if (me.started) {
if (callback) callback(false);
return;
}
me.audioContext.resume().then(function(){
me.allowed = me.audioContext.state === 'running';
if (!me.allowed) {
if (callback) callback(false);
return;
}
me.started = true;
me.gainNode = me.audioContext.createGain();
me.gainNode.connect(me.audioContext.destination);
if (useAudioWorklets && me.audioContext.audioWorklet) {
me.audioContext.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){
me.audioNode = new AudioWorkletNode(me.audioContext, 'openwebrx-audio-processor', {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [1],
processorOptions: {
maxBufferSize: me.maxBufferSize
}
});
me.audioNode.connect(me.gainNode);
me.audioNode.port.addEventListener('message', function(m){
var json = JSON.parse(m.data);
if (typeof(json.buffersize) !== 'undefined') {
me.audioReporter({
buffersize: json.buffersize
});
}
if (typeof(json.samplesProcessed) !== 'undefined') {
me.audioSamples.add(json.samplesProcessed);
}
});
me.audioNode.port.start();
if (callback) callback(true, 'AudioWorklet');
});
} else {
me.audioBuffers = [];
if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does
AudioBuffer.prototype.copyToChannel = function (input, channel) //input is Float32Array
{
var cd = this.getChannelData(channel);
for (var i = 0; i < input.length; i++) cd[i] = input[i];
}
}
var bufferSize;
if (me.audioContext.sampleRate < 44100 * 2)
bufferSize = 4096;
else if (me.audioContext.sampleRate >= 44100 * 2 && me.audioContext.sampleRate < 44100 * 4)
bufferSize = 4096 * 2;
else if (me.audioContext.sampleRate > 44100 * 4)
bufferSize = 4096 * 4;
function audio_onprocess(e) {
var total = 0;
var out = new Float32Array(bufferSize);
while (me.audioBuffers.length) {
var b = me.audioBuffers.shift();
// not enough space to fit all data, so splice and put back in the queue
if (total + b.length > bufferSize) {
var spaceLeft = bufferSize - total;
var tokeep = b.subarray(0, spaceLeft);
out.set(tokeep, total);
var tobuffer = b.subarray(spaceLeft, b.length);
me.audioBuffers.unshift(tobuffer);
total += spaceLeft;
break;
} else {
out.set(b, total);
total += b.length;
}
}
e.outputBuffer.copyToChannel(out, 0);
me.audioSamples.add(total);
}
//on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor
var method = 'createScriptProcessor';
if (me.audioContext.createJavaScriptNode) {
method = 'createJavaScriptNode';
}
me.audioNode = me.audioContext[method](bufferSize, 0, 1);
me.audioNode.onaudioprocess = audio_onprocess;
me.audioNode.connect(me.gainNode);
if (callback) callback(true, 'ScriptProcessorNode');
}
setInterval(me.reportStats.bind(me), 1000);
});
};
AudioEngine.prototype.isAllowed = function() {
return this.allowed;
};
AudioEngine.prototype.reportStats = function() {
if (this.audioNode.port) {
this.audioNode.port.postMessage(JSON.stringify({cmd:'getStats'}));
} else {
this.audioReporter({
buffersize: this.getBuffersize()
});
}
};
AudioEngine.prototype.initStats = function() {
var me = this;
var buildReporter = function(key) {
return function(v){
var report = {};
report[key] = v;
me.audioReporter(report);
}
};
this.audioBytes = new Measurement();
this.audioBytes.report(10000, 1000, buildReporter('audioByteRate'));
this.audioSamples = new Measurement();
this.audioSamples.report(10000, 1000, buildReporter('audioRate'));
};
AudioEngine.prototype.resetStats = function() {
this.audioBytes.reset();
this.audioSamples.reset();
};
AudioEngine.prototype.setupResampling = function() { //both at the server and the client
var output_range_max = 12000;
var output_range_min = 8000;
var targetRate = this.audioContext.sampleRate;
var i = 1;
while (true) {
var audio_server_output_rate = Math.floor(targetRate / i);
if (audio_server_output_rate < output_range_min) {
this.resamplingFactor = 0;
this.outputRate = 0;
divlog('Your audio card sampling rate (' + targetRate + ') is not supported.<br />Please change your operating system default settings in order to fix this.', 1);
break;
} else if (audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) {
this.resamplingFactor = i;
this.outputRate = audio_server_output_rate;
break; //okay, we're done
}
i++;
}
};
AudioEngine.prototype.getOutputRate = function() {
return this.outputRate;
};
AudioEngine.prototype.getSampleRate = function() {
return this.audioContext.sampleRate;
};
AudioEngine.prototype.pushAudio = function(data) {
if (!this.audioNode) return;
this.audioBytes.add(data.byteLength);
var buffer;
if (this.compression === "adpcm") {
//resampling & ADPCM
buffer = this.audioCodec.decode(new Uint8Array(data));
} else {
buffer = new Int16Array(data);
}
buffer = this.resampler.process(sdrjs.ConvertI16_F(buffer));
if (this.audioNode.port) {
// AudioWorklets supported
this.audioNode.port.postMessage(buffer);
} else {
// silently drop excess samples
if (this.getBuffersize() + buffer.length <= this.maxBufferSize) {
this.audioBuffers.push(buffer);
}
}
};
AudioEngine.prototype.setCompression = function(compression) {
this.compression = compression;
};
AudioEngine.prototype.setVolume = function(volume) {
this.gainNode.gain.value = volume;
};
AudioEngine.prototype.getBuffersize = function() {
// only available when using ScriptProcessorNode
if (!this.audioBuffers) return 0;
return this.audioBuffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0);
};

View File

@ -0,0 +1,58 @@
class OwrxAudioProcessor extends AudioWorkletProcessor {
constructor(options){
super(options);
// initialize ringbuffer, make sure it aligns with the expected buffer size of 128
this.bufferSize = Math.round(options.processorOptions.maxBufferSize / 128) * 128;
this.audioBuffer = new Float32Array(this.bufferSize);
this.inPos = 0;
this.outPos = 0;
this.samplesProcessed = 0;
this.port.addEventListener('message', (m) => {
if (typeof(m.data) === 'string') {
const json = JSON.parse(m.data);
if (json.cmd && json.cmd === 'getStats') {
this.reportStats();
}
} else {
// the ringbuffer size is aligned to the output buffer size, which means that the input buffers might
// need to wrap around the end of the ringbuffer, back to the start.
// it is better to have this processing here instead of in the time-critical process function.
if (this.inPos + m.data.length <= this.bufferSize) {
// we have enough space, so just copy data over.
this.audioBuffer.set(m.data, this.inPos);
} else {
// we don't have enough space, so we need to split the data.
const remaining = this.bufferSize - this.inPos;
this.audioBuffer.set(m.data.subarray(0, remaining), this.inPos);
this.audioBuffer.set(m.data.subarray(remaining));
}
this.inPos = (this.inPos + m.data.length) % this.bufferSize;
}
});
this.port.addEventListener('messageerror', console.error);
this.port.start();
}
process(inputs, outputs) {
if (this.remaining() < 128) return true;
outputs[0].forEach((output) => {
output.set(this.audioBuffer.subarray(this.outPos, this.outPos + 128));
});
this.outPos = (this.outPos + 128) % this.bufferSize;
this.samplesProcessed += 128;
return true;
}
remaining() {
const mod = (this.inPos - this.outPos) % this.bufferSize;
if (mod >= 0) return mod;
return mod + this.bufferSize;
}
reportStats() {
this.port.postMessage(JSON.stringify({
buffersize: this.remaining(),
samplesProcessed: this.samplesProcessed
}));
this.samplesProcessed = 0;
}
}
registerProcessor('openwebrx-audio-processor', OwrxAudioProcessor);

177
htdocs/lib/BookmarkBar.js Normal file
View File

@ -0,0 +1,177 @@
function BookmarkBar() {
var me = this;
me.localBookmarks = new BookmarkLocalStorage();
me.$container = $("#openwebrx-bookmarks-container");
me.bookmarks = {};
me.$container.on('click', '.bookmark', function(e){
var $bookmark = $(e.target).closest('.bookmark');
me.$container.find('.bookmark').removeClass('selected');
var b = $bookmark.data();
if (!b || !b.frequency || (!b.modulation && !b.digital_modulation)) return;
demodulator_set_offset_frequency(0, b.frequency - center_freq);
if (b.modulation) {
demodulator_analog_replace(b.modulation);
} else if (b.digital_modulation) {
demodulator_digital_replace(b.digital_modulation);
}
$bookmark.addClass('selected');
});
me.$container.on('click', '.action[data-action=edit]', function(e){
e.stopPropagation();
var $bookmark = $(e.target).closest('.bookmark');
me.showEditDialog($bookmark.data());
});
me.$container.on('click', '.action[data-action=delete]', function(e){
e.stopPropagation();
var $bookmark = $(e.target).closest('.bookmark');
me.localBookmarks.deleteBookmark($bookmark.data());
me.loadLocalBookmarks();
});
var $bookmarkButton = $('#openwebrx-panel-receiver').find('.openwebrx-bookmark-button');
if (typeof(Storage) !== 'undefined') {
$bookmarkButton.show();
} else {
$bookmarkButton.hide();
}
$bookmarkButton.click(function(){
me.showEditDialog();
});
me.$dialog = $("#openwebrx-dialog-bookmark");
me.$dialog.find('.openwebrx-button[data-action=cancel]').click(function(){
me.$dialog.hide();
});
me.$dialog.find('.openwebrx-button[data-action=submit]').click(function(){
me.storeBookmark();
});
me.$dialog.find('form').on('submit', function(e){
e.preventDefault();
me.storeBookmark();
});
}
BookmarkBar.prototype.position = function(){
var range = get_visible_freq_range();
$('#openwebrx-bookmarks-container').find('.bookmark').each(function(){
$(this).css('left', scale_px_from_freq($(this).data('frequency'), range));
});
};
BookmarkBar.prototype.loadLocalBookmarks = function(){
var bwh = bandwidth / 2;
var start = center_freq - bwh;
var end = center_freq + bwh;
var bookmarks = this.localBookmarks.getBookmarks().filter(function(b){
return b.frequency >= start && b.frequency <= end;
});
this.replace_bookmarks(bookmarks, 'local', true);
};
BookmarkBar.prototype.replace_bookmarks = function(bookmarks, source, editable) {
editable = !!editable;
bookmarks = bookmarks.map(function(b){
b.source = source;
b.editable = editable;
return b;
});
this.bookmarks[source] = bookmarks;
this.render();
};
BookmarkBar.prototype.render = function(){
var bookmarks = Object.values(this.bookmarks).reduce(function(l, v){ return l.concat(v); });
bookmarks = bookmarks.sort(function(a, b){ return a.frequency - b.frequency; });
var elements = bookmarks.map(function(b){
var $bookmark = $(
'<div class="bookmark" data-source="' + b.source + '"' + (b.editable?' editable="editable"':'') + '>' +
'<div class="bookmark-actions">' +
'<div class="openwebrx-button action" data-action="edit"><img src="static/gfx/openwebrx-edit.png"></div>' +
'<div class="openwebrx-button action" data-action="delete"><img src="static/gfx/openwebrx-trashcan.png"></div>' +
'</div>' +
'<div class="bookmark-content">' + b.name + '</div>' +
'</div>'
);
$bookmark.data(b);
return $bookmark;
});
this.$container.find('.bookmark').remove();
this.$container.append(elements);
this.position();
};
BookmarkBar.prototype.showEditDialog = function(bookmark) {
var $form = this.$dialog.find("form");
if (!bookmark) {
bookmark = {
name: "",
frequency: center_freq + demodulators[0].offset_frequency,
modulation: demodulators[0].subtype
}
}
['name', 'frequency', 'modulation'].forEach(function(key){
$form.find('#' + key).val(bookmark[key]);
});
this.$dialog.data('id', bookmark.id);
this.$dialog.show();
this.$dialog.find('#name').focus();
};
BookmarkBar.prototype.storeBookmark = function() {
var me = this;
var bookmark = {};
var valid = true;
['name', 'frequency', 'modulation'].forEach(function(key){
var $input = me.$dialog.find('#' + key);
valid = valid && $input[0].checkValidity();
bookmark[key] = $input.val();
});
if (!valid) {
me.$dialog.find("form :submit").click();
return;
}
bookmark.frequency = Number(bookmark.frequency);
var bookmarks = me.localBookmarks.getBookmarks();
bookmark.id = me.$dialog.data('id');
if (!bookmark.id) {
if (bookmarks.length) {
bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; }));
} else {
bookmark.id = 1;
}
}
bookmarks = bookmarks.filter(function(b) { return b.id !== bookmark.id; });
bookmarks.push(bookmark);
me.localBookmarks.setBookmarks(bookmarks);
me.loadLocalBookmarks();
me.$dialog.hide();
};
BookmarkLocalStorage = function(){
};
BookmarkLocalStorage.prototype.getBookmarks = function(){
return JSON.parse(window.localStorage.getItem("bookmarks")) || [];
};
BookmarkLocalStorage.prototype.setBookmarks = function(bookmarks){
window.localStorage.setItem("bookmarks", JSON.stringify(bookmarks));
};
BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
if (data.id) data = data.id;
var bookmarks = this.getBookmarks();
bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
this.setBookmarks(bookmarks);
};

62
htdocs/lib/Measurement.js Normal file
View File

@ -0,0 +1,62 @@
function Measurement() {
this.reset();
};
Measurement.prototype.add = function(v) {
this.value += v;
};
Measurement.prototype.getValue = function() {
return this.value;
};
Measurement.prototype.getElapsed = function() {
return new Date() - this.start;
};
Measurement.prototype.getRate = function() {
return this.getValue() / this.getElapsed();
};
Measurement.prototype.reset = function() {
this.value = 0;
this.start = new Date();
};
Measurement.prototype.report = function(range, interval, callback) {
return new Reporter(this, range, interval, callback);
}
function Reporter(measurement, range, interval, callback) {
this.measurement = measurement;
this.range = range;
this.samples = [];
this.callback = callback;
this.interval = setInterval(this.report.bind(this), interval);
};
Reporter.prototype.sample = function(){
this.samples.push({
timestamp: new Date(),
value: this.measurement.getValue()
});
};
Reporter.prototype.report = function(){
this.sample();
var now = new Date();
var minDate = now.getTime() - this.range;
this.samples = this.samples.filter(function(s) {
return s.timestamp.getTime() > minDate;
});
this.samples.sort(function(a, b) {
return a.timestamp - b.timestamp;
});
var oldest = this.samples[0];
var newest = this.samples[this.samples.length -1];
var elapsed = newest.timestamp - oldest.timestamp;
if (elapsed <= 0) return;
var accumulated = newest.value - oldest.value;
// we want rate per second, but our time is in milliseconds... compensate by 1000
this.callback(accumulated * 1000 / elapsed);
};

113
htdocs/lib/ProgressBar.js Normal file
View File

@ -0,0 +1,113 @@
ProgressBar = function(el) {
this.$el = $(el);
this.$innerText = this.$el.find('.openwebrx-progressbar-text');
this.$innerBar = this.$el.find('.openwebrx-progressbar-bar');
this.$innerBar.css('width', '0%');
};
ProgressBar.prototype.set = function(val, text, over) {
this.setValue(val);
this.setText(text);
this.setOver(over);
};
ProgressBar.prototype.setValue = function(val) {
if (val < 0) val = 0;
if (val > 1) val = 1;
this.$innerBar.stop().animate({width: val * 100 + '%'}, 700);
};
ProgressBar.prototype.setText = function(text) {
this.$innerText.html(text);
};
ProgressBar.prototype.setOver = function(over) {
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
};
AudioBufferProgressBar = function(el, sampleRate) {
ProgressBar.call(this, el);
this.sampleRate = sampleRate;
};
AudioBufferProgressBar.prototype = new ProgressBar();
AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) {
var audio_buffer_value = buffersize / this.sampleRate;
var overrun = audio_buffer_value > audio_buffer_maximal_length_sec;
var underrun = audio_buffer_value === 0;
var text = "buffer";
if (overrun) {
text = "overrun";
}
if (underrun) {
text = "underrun";
}
this.set(audio_buffer_value, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun);
};
NetworkSpeedProgressBar = function(el) {
ProgressBar.call(this, el);
};
NetworkSpeedProgressBar.prototype = new ProgressBar();
NetworkSpeedProgressBar.prototype.setSpeed = function(speed) {
var speedInKilobits = speed * 8 / 1000;
this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false);
};
AudioSpeedProgressBar = function(el) {
ProgressBar.call(this, el);
};
AudioSpeedProgressBar.prototype = new ProgressBar();
AudioSpeedProgressBar.prototype.setSpeed = function(speed) {
this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false);
};
AudioOutputProgressBar = function(el, sampleRate) {
ProgressBar.call(this, el);
this.maxRate = sampleRate * 1.25;
this.minRate = sampleRate * .25;
};
AudioOutputProgressBar.prototype = new ProgressBar();
AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) {
this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate);
};
ClientsProgressBar = function(el) {
ProgressBar.call(this, el);
this.clients = 0;
this.maxClients = 0;
};
ClientsProgressBar.prototype = new ProgressBar();
ClientsProgressBar.prototype.setClients = function(clients) {
this.clients = clients;
this.render();
};
ClientsProgressBar.prototype.setMaxClients = function(maxClients) {
this.maxClients = maxClients;
this.render();
};
ClientsProgressBar.prototype.render = function() {
this.set(this.clients / this.maxClients, "Clients [" + this.clients + "]", this.clients > this.maxClients * 0.85);
};
CpuProgressBar = function(el) {
ProgressBar.call(this, el);
};
CpuProgressBar.prototype = new ProgressBar();
CpuProgressBar.prototype.setUsage = function(usage) {
this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85);
};

File diff suppressed because it is too large Load Diff

View File

@ -60,3 +60,4 @@ if __name__ == "__main__":
main() main()
except KeyboardInterrupt: except KeyboardInterrupt:
WebSocketConnection.closeAll() WebSocketConnection.closeAll()
Services.stop()

View File

@ -5,6 +5,7 @@ from owrx.version import openwebrx_version
from owrx.bands import Bandplan from owrx.bands import Bandplan
from owrx.bookmarks import Bookmarks from owrx.bookmarks import Bookmarks
from owrx.map import Map from owrx.map import Map
from owrx.locator import Locator
from multiprocessing import Queue from multiprocessing import Queue
import json import json
import threading import threading
@ -64,7 +65,6 @@ class OpenWebRxReceiverClient(Client):
"fft_compression", "fft_compression",
"max_clients", "max_clients",
"start_mod", "start_mod",
"client_audio_buffer_size",
"start_freq", "start_freq",
"center_freq", "center_freq",
"mathbox_waterfall_colors", "mathbox_waterfall_colors",
@ -89,13 +89,13 @@ class OpenWebRxReceiverClient(Client):
receiver_keys = [ receiver_keys = [
"receiver_name", "receiver_name",
"receiver_location", "receiver_location",
"receiver_qra",
"receiver_asl", "receiver_asl",
"receiver_gps", "receiver_gps",
"photo_title", "photo_title",
"photo_desc", "photo_desc",
] ]
receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys) receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys)
receiver_details["locator"] = Locator.fromCoordinates(receiver_details["receiver_gps"])
self.write_receiver_details(receiver_details) self.write_receiver_details(receiver_details)
profiles = [ profiles = [
@ -141,6 +141,11 @@ class OpenWebRxReceiverClient(Client):
def setSdr(self, id=None): def setSdr(self, id=None):
next = SdrService.getSource(id) next = SdrService.getSource(id)
if next is None:
self.handleSdrFailure("sdr device failed")
return
if next == self.sdr: if next == self.sdr:
return return
@ -152,6 +157,8 @@ class OpenWebRxReceiverClient(Client):
self.sdr = next self.sdr = next
self.startDsp()
# send initial config # send initial config
configProps = ( configProps = (
self.sdr.getProps() self.sdr.getProps()
@ -163,6 +170,8 @@ class OpenWebRxReceiverClient(Client):
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys) config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
# TODO mathematical properties? hmmmm # TODO mathematical properties? hmmmm
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
# TODO this is a hack that only works because setting the profile always causes plenty of config change
config["profile_id"] = self.sdr.getId() + "|" + self.sdr.getProfileId()
self.write_config(config) self.write_config(config)
cf = configProps["center_freq"] cf = configProps["center_freq"]
@ -177,6 +186,9 @@ class OpenWebRxReceiverClient(Client):
self.sdr.addSpectrumClient(self) self.sdr.addSpectrumClient(self)
def handleSdrFailure(self, message):
self.write_sdr_error(message)
def startDsp(self): def startDsp(self):
if self.dsp is None: if self.dsp is None:
self.dsp = DspManager(self, self.sdr) self.dsp = DspManager(self, self.sdr)
@ -231,7 +243,8 @@ class OpenWebRxReceiverClient(Client):
self.send(bytes([0x03]) + data) self.send(bytes([0x03]) + data)
def write_secondary_demod(self, data): def write_secondary_demod(self, data):
self.send(bytes([0x04]) + data) message = data.decode('ascii')
self.send({"type": "secondary_demod", "value": message})
def write_secondary_dsp_config(self, cfg): def write_secondary_dsp_config(self, cfg):
self.send({"type": "secondary_config", "value": cfg}) self.send({"type": "secondary_config", "value": cfg})
@ -263,6 +276,9 @@ class OpenWebRxReceiverClient(Client):
def write_aprs_data(self, data): def write_aprs_data(self, data):
self.send({"type": "aprs_data", "value": data}) self.send({"type": "aprs_data", "value": data})
def write_sdr_error(self, message):
self.send({"type": "sdr_error", "value": message})
class MapConnection(Client): class MapConnection(Client):
def __init__(self, conn): def __init__(self, conn):

View File

@ -101,7 +101,7 @@ class AprsSymbolsController(AssetsController):
class TemplateController(Controller): class TemplateController(Controller):
def render_template(self, file, **vars): def render_template(self, file, **vars):
f = open("htdocs/" + file, "r") f = open("htdocs/" + file, "r", encoding="utf-8")
template = Template(f.read()) template = Template(f.read())
f.close() f.close()
@ -152,4 +152,4 @@ class WebSocketController(Controller):
def handle_request(self): def handle_request(self):
conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) conn = WebSocketConnection(self.handler, WebSocketMessageHandler())
# enter read loop # enter read loop
conn.read_loop() conn.handle()

View File

@ -8,6 +8,7 @@ from owrx.wsjt import WsjtParser
from owrx.aprs import AprsParser from owrx.aprs import AprsParser
from owrx.config import PropertyManager from owrx.config import PropertyManager
from owrx.source import Resampler from owrx.source import Resampler
from owrx.feature import FeatureDetector
import logging import logging
@ -119,11 +120,14 @@ class ServiceScheduler(object):
if time is not None: if time is not None:
delta = time - datetime.utcnow() delta = time - datetime.utcnow()
seconds = delta.total_seconds() seconds = delta.total_seconds()
if self.selectionTimer: self.cancelTimer()
self.selectionTimer.cancel()
self.selectionTimer = threading.Timer(seconds, self.selectProfile) self.selectionTimer = threading.Timer(seconds, self.selectProfile)
self.selectionTimer.start() self.selectionTimer.start()
def cancelTimer(self):
if self.selectionTimer:
self.selectionTimer.cancel()
def isActive(self): def isActive(self):
return self.active return self.active
@ -133,6 +137,9 @@ class ServiceScheduler(object):
def onSdrUnavailable(self): def onSdrUnavailable(self):
self.scheduleSelection() self.scheduleSelection()
def onSdrFailed(self):
self.cancelTimer()
def selectProfile(self): def selectProfile(self):
self.active = False self.active = False
if self.source.hasActiveClients(): if self.source.hasActiveClients():
@ -183,8 +190,29 @@ class ServiceHandler(object):
logger.debug("sdr source becoming unavailable; stopping services.") logger.debug("sdr source becoming unavailable; stopping services.")
self.stopServices() self.stopServices()
def onSdrFailed(self):
logger.debug("sdr source failed; stopping services.")
self.stopServices()
def isSupported(self, mode): def isSupported(self, mode):
return mode in PropertyManager.getSharedInstance()["services_decoders"] # TODO this should be in a more central place (the frontend also needs this)
requirements = {
'ft8': 'wsjt-x',
'ft4': 'wsjt-x',
'jt65': 'wsjt-x',
'jt9': 'wsjt-x',
'wspr': 'wsjt-x',
'packet': 'packet',
}
fd = FeatureDetector()
# this looks overly complicated... but i'd like modes with no requirements to be always available without
# being listed in the hash above
unavailable = [mode for mode, req in requirements.items() if not fd.is_available(req)]
configured = PropertyManager.getSharedInstance()["services_decoders"]
available = [mode for mode in configured if mode not in unavailable]
return mode in available
def stopServices(self): def stopServices(self):
with self.lock: with self.lock:
@ -238,23 +266,28 @@ class ServiceHandler(object):
with self.lock: with self.lock:
self.services = [] self.services = []
for group in self.optimizeResampling(dials, sr): groups = self.optimizeResampling(dials, sr)
frequencies = sorted([f["frequency"] for f in group]) if groups is None:
min = frequencies[0] for dial in dials:
max = frequencies[-1] self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source))
cf = (min + max) / 2 else:
bw = max - min for group in groups:
logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw)) frequencies = sorted([f["frequency"] for f in group])
resampler_props = PropertyManager() min = frequencies[0]
resampler_props["center_freq"] = cf max = frequencies[-1]
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths cf = (min + max) / 2
resampler_props["samp_rate"] = bw + 24000 bw = max - min
resampler = Resampler(resampler_props, self.getAvailablePort(), self.source) logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw))
resampler.start() resampler_props = PropertyManager()
self.services.append(resampler) resampler_props["center_freq"] = cf
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
resampler_props["samp_rate"] = bw + 24000
resampler = Resampler(resampler_props, self.getAvailablePort(), self.source)
resampler.start()
self.services.append(resampler)
for dial in group: for dial in group:
self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler)) self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler))
def optimizeResampling(self, freqs, bandwidth): def optimizeResampling(self, freqs, bandwidth):
freqs = sorted(freqs, key=lambda f: f["frequency"]) freqs = sorted(freqs, key=lambda f: f["frequency"])
@ -285,14 +318,17 @@ class ServiceHandler(object):
return {"num_splits": num_splits, "total_bandwidth": total_bandwidth, "groups": groups} return {"num_splits": num_splits, "total_bandwidth": total_bandwidth, "groups": groups}
usages = [calculate_usage(i) for i in range(0, len(freqs))] usages = [calculate_usage(i) for i in range(0, len(freqs))]
# this is simulating no resampling. i haven't seen this as the best result yet # another possible outcome might be that it's best not to resample at all. this is a special case.
usages += [{"num_splits": None, "total_bandwidth": bandwidth * len(freqs), "groups": [freqs]}] usages += [{"num_splits": None, "total_bandwidth": bandwidth * len(freqs), "groups": [freqs]}]
results = sorted(usages, key=lambda f: f["total_bandwidth"]) results = sorted(usages, key=lambda f: f["total_bandwidth"])
for r in results: for r in results:
logger.debug("splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"])) logger.debug("splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"]))
return results[0]["groups"] best = results[0]
if best["num_splits"] is None:
return None
return best["groups"]
def setupService(self, mode, frequency, source): def setupService(self, mode, frequency, source):
logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) logger.debug("setting up service {0} on frequency {1}".format(mode, frequency))
@ -333,12 +369,19 @@ class AprsHandler(object):
class Services(object): class Services(object):
handlers = []
@staticmethod @staticmethod
def start(): def start():
if not PropertyManager.getSharedInstance()["services_enabled"]: if not PropertyManager.getSharedInstance()["services_enabled"]:
return return
for source in SdrService.getSources().values(): for source in SdrService.getSources().values():
ServiceHandler(source) Services.handlers.append(ServiceHandler(source))
@staticmethod
def stop():
for handler in Services.handlers:
handler.stopServices()
Services.handlers = []
class Service(object): class Service(object):

View File

@ -75,10 +75,15 @@ class SdrService(object):
@staticmethod @staticmethod
def getSource(id=None): def getSource(id=None):
SdrService.loadProps() SdrService.loadProps()
sources = SdrService.getSources()
if not sources:
return None
if id is None: if id is None:
# TODO: configure default sdr in config? right now it will pick the first one off the list. # TODO: configure default sdr in config? right now it will pick the first one off the list.
id = list(SdrService.sdrProps.keys())[0] id = list(sources.keys())[0]
sources = SdrService.getSources()
if not id in sources:
return None
return sources[id] return sources[id]
@staticmethod @staticmethod
@ -89,17 +94,15 @@ class SdrService(object):
props = SdrService.sdrProps[id] props = SdrService.sdrProps[id]
className = "".join(x for x in props["type"].title() if x.isalnum()) + "Source" className = "".join(x for x in props["type"].title() if x.isalnum()) + "Source"
cls = getattr(sys.modules[__name__], className) cls = getattr(sys.modules[__name__], className)
SdrService.sources[id] = cls(props, SdrService.getNextPort()) SdrService.sources[id] = cls(id, props, SdrService.getNextPort())
return SdrService.sources return {key: s for key, s in SdrService.sources.items() if not s.isFailed()}
class SdrSourceException(Exception):
pass
class SdrSource(object): class SdrSource(object):
def __init__(self, props, port): def __init__(self, id, props, port):
self.id = id
self.props = props self.props = props
self.profile_id = None
self.activateProfile() self.activateProfile()
self.rtlProps = self.props.collect( self.rtlProps = self.props.collect(
"samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain" "samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain"
@ -118,6 +121,7 @@ class SdrSource(object):
self.spectrumThread = None self.spectrumThread = None
self.process = None self.process = None
self.modificationLock = threading.Lock() self.modificationLock = threading.Lock()
self.failed = False
# override this in subclasses # override this in subclasses
def getCommand(self): def getCommand(self):
@ -131,7 +135,10 @@ class SdrSource(object):
profiles = self.props["profiles"] profiles = self.props["profiles"]
if profile_id is None: if profile_id is None:
profile_id = list(profiles.keys())[0] profile_id = list(profiles.keys())[0]
if profile_id == self.profile_id:
return;
logger.debug("activating profile {0}".format(profile_id)) logger.debug("activating profile {0}".format(profile_id))
self.profile_id = profile_id
profile = profiles[profile_id] profile = profiles[profile_id]
for (key, value) in profile.items(): for (key, value) in profile.items():
# skip the name, that would overwrite the source name. # skip the name, that would overwrite the source name.
@ -139,6 +146,12 @@ class SdrSource(object):
continue continue
self.props[key] = value self.props[key] = value
def getId(self):
return self.id
def getProfileId(self):
return self.profile_id
def getProfiles(self): def getProfiles(self):
return self.props["profiles"] return self.props["profiles"]
@ -213,17 +226,23 @@ class SdrSource(object):
except: except:
time.sleep(0.1) time.sleep(0.1)
if not available:
self.failed = True
self.modificationLock.release() self.modificationLock.release()
if not available:
raise SdrSourceException("rtl source failed to start up")
for c in self.clients: for c in self.clients:
c.onSdrAvailable() if self.failed:
c.onSdrFailed()
else:
c.onSdrAvailable()
def isAvailable(self): def isAvailable(self):
return self.monitor is not None return self.monitor is not None
def isFailed(self):
return self.failed
def stop(self): def stop(self):
for c in self.clients: for c in self.clients:
c.onSdrUnavailable() c.onSdrUnavailable()
@ -291,9 +310,12 @@ class Resampler(SdrSource):
props["samp_rate"] = if_samp_rate props["samp_rate"] = if_samp_rate
self.sdr = sdr self.sdr = sdr
super().__init__(props, port) super().__init__(None, props, port)
def start(self): def start(self):
if self.isFailed():
return
self.modificationLock.acquire() self.modificationLock.acquire()
if self.monitor: if self.monitor:
self.modificationLock.release() self.modificationLock.release()
@ -353,13 +375,16 @@ class Resampler(SdrSource):
except: except:
time.sleep(0.1) time.sleep(0.1)
if not available:
self.failed = True
self.modificationLock.release() self.modificationLock.release()
if not available:
raise SdrSourceException("resampler source failed to start up")
for c in self.clients: for c in self.clients:
c.onSdrAvailable() if self.failed:
c.onSdrFailed()
else:
c.onSdrAvailable()
def activateProfile(self, profile_id=None): def activateProfile(self, profile_id=None):
pass pass
@ -493,6 +518,9 @@ class SpectrumThread(csdr.output):
def onSdrUnavailable(self): def onSdrUnavailable(self):
self.dsp.stop() self.dsp.stop()
def onSdrFailed(self):
self.dsp.stop()
class DspManager(csdr.output): class DspManager(csdr.output):
def __init__(self, handler, sdrSource): def __init__(self, handler, sdrSource):
@ -627,6 +655,11 @@ class DspManager(csdr.output):
logger.debug("received onSdrUnavailable, shutting down DspSource") logger.debug("received onSdrUnavailable, shutting down DspSource")
self.dsp.stop() self.dsp.stop()
def onSdrFailed(self):
logger.debug("received onSdrFailed, shutting down DspSource")
self.dsp.stop()
self.handler.handleSdrFailure("sdr device failed")
class CpuUsageThread(threading.Thread): class CpuUsageThread(threading.Thread):
sharedInstance = None sharedInstance = None

View File

@ -16,11 +16,19 @@ OPCODE_PING = 0x09
OPCODE_PONG = 0x0A OPCODE_PONG = 0x0A
class IncompleteRead(Exception): class WebSocketException(Exception):
pass pass
class Drained(Exception): class IncompleteRead(WebSocketException):
pass
class Drained(WebSocketException):
pass
class WebSocketClosed(WebSocketException):
pass pass
@ -42,17 +50,16 @@ class WebSocketConnection(object):
(self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False) (self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False)
self.open = True self.open = True
self.sendLock = threading.Lock() self.sendLock = threading.Lock()
my_headers = self.handler.headers.items()
my_header_keys = list(map(lambda x: x[0], my_headers)) headers = {key.lower(): value for key, value in self.handler.headers.items()}
h_key_exists = lambda x: my_header_keys.count(x) if not "upgrade" in headers:
h_value = lambda x: my_headers[my_header_keys.index(x)][1] raise WebSocketException("Upgrade header not found")
if ( if headers["upgrade"].lower() != "websocket":
(not h_key_exists("Upgrade")) raise WebSocketException("Upgrade header does not contain expected value")
or not (h_value("Upgrade") == "websocket") if not "sec-websocket-key" in headers:
or (not h_key_exists("Sec-WebSocket-Key")) raise WebSocketException("Websocket key not provided")
):
raise WebSocketException ws_key = headers["sec-websocket-key"]
ws_key = h_value("Sec-WebSocket-Key")
shakey = hashlib.sha1() shakey = hashlib.sha1()
shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode()) shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode())
ws_key_toreturn = base64.b64encode(shakey.digest()) ws_key_toreturn = base64.b64encode(shakey.digest())
@ -94,6 +101,8 @@ class WebSocketConnection(object):
return bytes([ws_first_byte, size]) return bytes([ws_first_byte, size])
def send(self, data): def send(self, data):
if not self.open:
raise WebSocketClosed()
# convenience # convenience
if type(data) == dict: if type(data) == dict:
# allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway. # allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway.
@ -140,6 +149,26 @@ class WebSocketConnection(object):
def interrupt(self): def interrupt(self):
self.interruptPipeSend.send(bytes(0x00)) self.interruptPipeSend.send(bytes(0x00))
def handle(self):
WebSocketConnection.connections.append(self)
try:
self.read_loop()
finally:
logger.debug("websocket loop ended; shutting down")
self.messageHandler.handleClose()
self.cancelPing()
logger.debug("websocket loop ended; sending close frame")
header = self.get_header(0, OPCODE_CLOSE)
self._sendBytes(header)
try:
WebSocketConnection.connections.remove(self)
except ValueError:
pass
def read_loop(self): def read_loop(self):
def protected_read(num): def protected_read(num):
data = self.handler.rfile.read(num) data = self.handler.rfile.read(num)
@ -149,10 +178,9 @@ class WebSocketConnection(object):
raise IncompleteRead() raise IncompleteRead()
return data return data
WebSocketConnection.connections.append(self)
self.open = True self.open = True
while self.open: while self.open:
(read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], []) (read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], [], 15)
if self.handler.rfile in read: if self.handler.rfile in read:
available = True available = True
self.resetPing() self.resetPing()
@ -190,25 +218,10 @@ class WebSocketConnection(object):
except IncompleteRead: except IncompleteRead:
logger.warning("incomplete read on websocket; closing connection") logger.warning("incomplete read on websocket; closing connection")
self.open = False self.open = False
except TimeoutError: except OSError:
logger.warning("websocket timed out; closing connection") logger.exception("OSError while reading data; closing connection")
self.open = False self.open = False
logger.debug("websocket loop ended; shutting down")
self.messageHandler.handleClose()
self.cancelPing()
logger.debug("websocket loop ended; sending close frame")
header = self.get_header(0, OPCODE_CLOSE)
self._sendBytes(header)
try:
WebSocketConnection.connections.remove(self)
except ValueError:
pass
def close(self): def close(self):
self.open = False self.open = False
self.interrupt() self.interrupt()
@ -233,7 +246,3 @@ class WebSocketConnection(object):
def sendPong(self): def sendPong(self):
header = self.get_header(0, OPCODE_PONG) header = self.get_header(0, OPCODE_PONG)
self._sendBytes(header) self._sendBytes(header)
class WebSocketException(Exception):
pass