118 Commits

Author SHA1 Message Date
53de54120e only specify device strings when configured 2019-11-24 20:34:51 +01:00
fa097bf57e update readme 2019-11-24 19:36:07 +01:00
917eb4fdf1 update readme 2019-11-24 18:23:45 +01:00
a8df774e50 dual authors 2019-11-24 18:08:54 +01:00
0b98ce1ef2 restructure docker image; add separate temp dir that can be placed in a
tmpfs
2019-11-24 15:30:53 +01:00
c6bbdffea0 update ignore files 2019-11-23 18:21:55 +01:00
481918ab5b better profile switching for the gui 2019-11-23 17:22:20 +01:00
b27caf2405 allow initial_squelch_level to be set per profile 2019-11-23 16:56:29 +01:00
d5b7338531 run black 2019-11-23 01:13:16 +01:00
9246500c95 run black 2019-11-23 01:12:21 +01:00
91669a7fda no agc necessary for wsjt-x decoding 2019-11-23 00:35:33 +01:00
c7eb67129a add information about connectors 2019-11-22 23:34:27 +01:00
98901ac668 add pskreporter dupe check and stats 2019-11-22 17:16:40 +01:00
7dde793f9e let's switch to the connectors per default for now 2019-11-22 15:18:29 +01:00
07de82ae82 secondary chain as array, too 2019-11-22 15:00:36 +01:00
9f710cb70e fix for lfo_offset = None 2019-11-21 17:19:51 +01:00
dab62a04df fix offset switching 2019-11-21 16:07:20 +01:00
de51e266f6 add airspy source; fix offset tuning 2019-11-21 15:31:37 +01:00
5375580104 add device handling for rtl 2019-11-20 11:37:06 +01:00
964d9e873d add iq swapping capability 2019-11-19 14:03:32 +01:00
7e8e644e6c purge manifests after use (won't work as expected otherwise) 2019-11-18 21:26:11 +00:00
6bde623698 add manifest stuff 2019-11-18 14:42:05 +01:00
5ba89035b4 add connectors to docker 2019-11-18 14:15:59 +01:00
a9b99fa0ff introduce connector source for sdrplay 2019-11-17 20:52:16 +01:00
6619a1b4a6 the ServiceHandler is fully passive 2019-11-16 15:40:12 +01:00
a36f106c72 add source "busy state" to improve background scheduling 2019-11-15 23:05:52 +01:00
097f8a2b82 refactor event system 2019-11-15 22:13:00 +01:00
bcbb911b24 restore airspy feature test 2019-11-15 19:36:07 +01:00
f18efb2344 use Popen for feature detection to be able to take control of the
working directory
2019-11-14 22:13:02 +01:00
497d98363f fix bookmark edit / delete flyout 2019-11-14 15:31:44 +01:00
367bf666fc listen for frequency changes in the scheduler, too 2019-11-13 19:50:00 +01:00
7489a3bb9d try to improve memory footprint by rebuilding map dictionary in
intervals
2019-11-13 18:01:01 +01:00
2a6c7863b1 improve control socket handling 2019-11-12 15:57:10 +01:00
bf27f51049 let's leave some footsteps 2019-11-12 13:43:39 +01:00
6ba74a0c30 add ppm 2019-11-11 20:35:50 +01:00
ada94f69c3 new modificitions for owrx_connector support 2019-11-11 18:07:14 +01:00
dc5ac081ce fix some javascript code style issues 2019-11-07 10:56:39 +01:00
8a46922e77 panels disappear behind the header 2019-11-01 22:22:46 +01:00
5fdffb5e0c fix scrolling for canvas background and bookmarks. i hope that's all
now.
2019-11-01 19:48:08 +01:00
9f6a4891ed fix styles (broken by debugging) 2019-11-01 18:53:16 +01:00
41d23c66a4 prevent events from being blocked by the panels 2019-11-01 18:47:33 +01:00
9163f3d30e improve autoplay interface 2019-11-01 16:58:36 +01:00
d49fff65e4 switch to different csdr branch 2019-11-01 15:18:39 +01:00
95253e40bd organize timers and threads to get proper shutdown 2019-10-31 22:24:31 +01:00
af1a99c130 prevent deadlocks by shutting down services in correct order 2019-10-31 19:13:33 +01:00
1638fde181 fix gradient (without gradient) 2019-10-28 20:54:31 +01:00
52ea2e88e9 update readme 2019-10-27 17:45:17 +01:00
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
40 changed files with 3541 additions and 3154 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
.git
.gitignore
.idea
**/*.pyc
**/*.swp

5
.gitignore vendored
View File

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

View File

@ -19,6 +19,24 @@ It has the following features:
- [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN) - [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN)
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9) - [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9)
**News (2019-11-24 by DD5JFK)**
- There is now a new way to interface with SDR hardware, [owrx_connectors](https://github.com/jketterl/owrx_connector).
They talk directly to the hardware (no rtl_sdr / rx_sdr necessary) and offer I/Q data on a socket, just like nmux
did before. They additionally offer a control socket that allows openwebrx to control the SDR parameters directly,
without the need for repeated restarts. This allows for quicker profile changes, and also reduces the risk of your
SDR hardware from failing during the switchover. See `config_webrx.py` for further information and instructions.
- Offset tuning using the `lfo_offset` has been reworked in a way that `center_freq` has to be set to the frequency you
actually want to listen to. If you're using an `lfo_offset` already, you will probably need to change its sign.
- `initial_squelch_level` can now be set on each profile.
- As usual, plenty of fixes and improvements.
**News (2019-10-27 by DD5JFK)**
- Part of the frontend code has been reworked
- Audio buffer minimums have been completely stripped. As a result, you should get better latency. Unfortunately, this also means there will be some skipping when audio starts.
- Now also supports AudioWorklets (for those browser that have it). The Raspberry Pi image has been updated to include https due to the SecureContext requirement.
- Mousewheel controls for the receiver sliders
- Error handling for failed SDR devices
**News (2019-09-29 by DD5FJK)** **News (2019-09-29 by DD5FJK)**
- One of the most-requested features is finally coming to OpenWebRX: Bookmarks (sometimes also referred to as labels). There's two kinds of bookmarks available: - One of the most-requested features is finally coming to OpenWebRX: Bookmarks (sometimes also referred to as labels). There's two kinds of bookmarks available:
- Serverside bookmarks that are set up by the receiver administrator. Check the file `bookmarks.json` for examples! - Serverside bookmarks that are set up by the receiver administrator. Check the file `bookmarks.json` for examples!
@ -78,13 +96,13 @@ It has the following features:
### Raspberry Pi SD Card Images ### Raspberry Pi SD Card Images
Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-09-29-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+. Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-10-27-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+.
This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply. This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply.
Please note: I have not updated this to include the Raspberry Pi 4 yet. (It seems to be impossible to build Rasbpian Buster images on x86 hardware right now. Stay tuned!) Please note: I have not updated this to include the Raspberry Pi 4 yet. (It seems to be impossible to build Rasbpian Buster images on x86 hardware right now. Stay tuned!)
Once you have booted a Raspberry with the SD Card, it will appear in your network with the hostname "openwebrx", which should make it available as http://openwebrx:8073/ on most networks. This may vary depending on your specific setup. Once you have booted a Raspberry with the SD Card, it will appear in your network with the hostname "openwebrx", which should make it available as https://openwebrx:8073/ on most networks. This may vary depending on your specific setup.
For Digital voice, the minimum requirement right now seems to be a Rasbperry Pi 3B+. I would like to work on optimizing this for lower specs, but at this point I am not sure how much can be done. For Digital voice, the minimum requirement right now seems to be a Rasbperry Pi 3B+. I would like to work on optimizing this for lower specs, but at this point I am not sure how much can be done.
@ -94,19 +112,23 @@ For those familiar with docker, I am providing [recent builds and Releases for b
### Manual Installation ### Manual Installation
OpenWebRX currently requires Linux and python 3 to run. OpenWebRX currently requires Linux and python >= 3.6 to run.
First you will need to install the dependencies: First you will need to install the dependencies:
- [csdr](https://github.com/simonyiszk/csdr) - [csdr](https://github.com/simonyiszk/csdr)
- [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr) - [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr)
Optional Dependencies if you want to be able to listen do digital voice: Optional dependency for improved hardware access (to become mandatory at some point):
- [owrx_connector](https://github.com/jketterl/owrx_connector)
Optional dependencies if you want to be able to listen do digital voice:
- [digiham](https://github.com/jketterl/digiham) - [digiham](https://github.com/jketterl/digiham)
- [dsd](https://github.com/f4exb/dsdcc) - [dsd](https://github.com/f4exb/dsdcc)
Optional Dependency if you want to decode WSJT-X modes: Optional dependency if you want to decode WSJT-X modes:
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) - [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html)
@ -123,7 +145,7 @@ Please note that the server is also listening on the following ports (on localho
Now the next step is to customize the parameters of your server in `config_webrx.py`. Now the next step is to customize the parameters of your server in `config_webrx.py`.
Actually, if you do something cool with OpenWebRX, please drop me a mail: Actually, if you do something cool with OpenWebRX, please drop me a mail:
*Andras Retzler, HA7ILM &lt;randras@sdr.hu&gt;* *Jakob Ketterl, DD5JFK &lt;dd5jfk@darc.de&gt;*
## Usage tips ## Usage tips

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

@ -6,6 +6,7 @@ config_webrx: configuration options for OpenWebRX
This file is part of OpenWebRX, This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI. an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as
@ -41,13 +42,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 />
@ -70,8 +67,8 @@ server_hostname = "localhost"
fft_fps = 9 fft_fps = 9
fft_size = 4096 # Should be power of 2 fft_size = 4096 # Should be power of 2
fft_voverlap_factor = ( fft_voverlap_factor = (
0.3 0.3 # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
) # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. )
audio_compression = "adpcm" # valid values: "adpcm", "none" audio_compression = "adpcm" # valid values: "adpcm", "none"
fft_compression = "adpcm" # valid values: "adpcm", "none" fft_compression = "adpcm" # valid values: "adpcm", "none"
@ -102,14 +99,27 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
################################################################################################# #################################################################################################
# Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf", "airspy" # Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf", "airspy"
#
# NEW: There is now custom connector software available, that is tailored for the use with
# openwebrx. The connectors allow the SDR to be reprogrammed while running, which allows for
# quicker profile changes. It also reduces the risk of a USB disconnect that can happen when the
# SDR software is restarted, since the connector will run continuously.
# Check out the connector repository here: https://github.com/jketterl/owrx_connector
#
# The following connectors are available (simply use them as the "type" in the config below):
# "rtl_sdr_connector", "sdrplay_connector", "airspy_connector"
#
# NOTE: These connectors will become the default as soon as they have become mature; the existing
# receiver types will then automatically be migrated to connectors. At that point, the old "nmux"
# method will start to be phased out.
sdrs = { sdrs = {
"rtlsdr": { "rtlsdr": {
"name": "RTL-SDR USB Stick", "name": "RTL-SDR USB Stick",
"type": "rtl_sdr", "type": "rtl_sdr_connector",
"ppm": 0, "ppm": 0,
# you can change this if you use an upconverter. formula is: # you can change this if you use an upconverter. formula is:
# shown_center_freq = center_freq + lfo_offset # center_freq + lfo_offset = actual frequency on the sdr
# "lfo_offset": 0, # "lfo_offset": 0,
"profiles": { "profiles": {
"70cm": { "70cm": {
@ -132,7 +142,7 @@ sdrs = {
}, },
"sdrplay": { "sdrplay": {
"name": "SDRPlay RSP2", "name": "SDRPlay RSP2",
"type": "sdrplay", "type": "sdrplay_connector",
"ppm": 0, "ppm": 0,
"profiles": { "profiles": {
"20m": { "20m": {
@ -181,17 +191,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,

78
csdr.py
View File

@ -4,6 +4,7 @@ OpenWebRX csdr plugin: do the signal processing with csdr
This file is part of OpenWebRX, This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI. an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as
@ -24,6 +25,7 @@ import subprocess
import os import os
import signal import signal
import threading import threading
import math
from functools import partial from functools import partial
from owrx.kiss import KissClient, DirewolfConfig from owrx.kiss import KissClient, DirewolfConfig
@ -84,7 +86,7 @@ class dsp(object):
self.csdr_dynamic_bufsize = False self.csdr_dynamic_bufsize = False
self.csdr_print_bufsizes = False self.csdr_print_bufsizes = False
self.csdr_through = False self.csdr_through = False
self.squelch_level = 0 self.squelch_level = -150
self.fft_averages = 50 self.fft_averages = 50
self.iqtee = False self.iqtee = False
self.iqtee2 = False self.iqtee2 = False
@ -213,35 +215,35 @@ class dsp(object):
return chain return chain
def secondary_chain(self, which): def secondary_chain(self, which):
secondary_chain_base = "cat {input_pipe} | " chain = ["cat {input_pipe}"]
if which == "fft": if which == "fft":
return ( chain += [
secondary_chain_base "csdr realpart_cf",
+ "csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " "csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size}",
+ (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression == "adpcm" else "") "csdr logpower_cf -70",
) ]
if self.fft_compression == "adpcm":
chain += ["csdr compress_fft_adpcm_f_u8 {secondary_fft_size}"]
return chain
elif which == "bpsk31": elif which == "bpsk31":
return ( return chain + [
secondary_chain_base "csdr shift_addition_cc --fifo {secondary_shift_pipe}",
+ "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}",
+ "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " "csdr simple_agc_cc 0.001 0.5",
+ "csdr simple_agc_cc 0.001 0.5 | " "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q",
+ "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8",
+ "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8",
+ "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" ]
)
elif self.isWsjtMode(which): elif self.isWsjtMode(which):
chain = secondary_chain_base + "csdr realpart_cf | " chain += ["csdr realpart_cf"]
if self.last_decimation != 1.0: if self.last_decimation != 1.0:
chain += "csdr fractional_decimator_ff {last_decimation} | " chain += ["csdr fractional_decimator_ff {last_decimation}"]
chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" return chain + ["csdr limit_ff", "csdr convert_f_s16"]
return chain
elif which == "packet": elif which == "packet":
chain = secondary_chain_base + "csdr fmdemod_quadri_cf | " chain += ["csdr fmdemod_quadri_cf"]
if self.last_decimation != 1.0: if self.last_decimation != 1.0:
chain += "csdr fractional_decimator_ff {last_decimation} | " chain += ["csdr fractional_decimator_ff {last_decimation}"]
chain += "csdr convert_f_s16 | direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h - 1>&2" return chain + ["csdr convert_f_s16", "direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h - 1>&2"]
return chain
def set_secondary_demodulator(self, what): def set_secondary_demodulator(self, what):
if self.get_secondary_demodulator() == what: if self.get_secondary_demodulator() == what:
@ -281,7 +283,7 @@ class dsp(object):
if not self.secondary_demodulator: if not self.secondary_demodulator:
return return
logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate()) logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate())
secondary_command_demod = self.secondary_chain(self.secondary_demodulator) secondary_command_demod = " | ".join(self.secondary_chain(self.secondary_demodulator))
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod) self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod)
self.try_create_configs(secondary_command_demod) self.try_create_configs(secondary_command_demod)
@ -304,7 +306,7 @@ class dsp(object):
if self.csdr_print_bufsizes: if self.csdr_print_bufsizes:
my_env["CSDR_PRINT_BUFSIZES"] = "1" my_env["CSDR_PRINT_BUFSIZES"] = "1"
if self.output.supports_type("secondary_fft"): if self.output.supports_type("secondary_fft"):
secondary_command_fft = self.secondary_chain("fft") secondary_command_fft = " | ".join(self.secondary_chain("fft"))
secondary_command_fft = secondary_command_fft.format( secondary_command_fft = secondary_command_fft.format(
input_pipe=self.iqtee_pipe, input_pipe=self.iqtee_pipe,
secondary_fft_input_size=self.secondary_fft_size, secondary_fft_input_size=self.secondary_fft_size,
@ -391,6 +393,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 +409,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 +466,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:
@ -508,13 +522,16 @@ class dsp(object):
def get_bpf(self): def get_bpf(self):
return [self.low_cut, self.high_cut] return [self.low_cut, self.high_cut]
def convertToLinear(self, db):
return float(math.pow(10, db / 10))
def set_squelch_level(self, squelch_level): def set_squelch_level(self, squelch_level):
self.squelch_level = squelch_level self.squelch_level = squelch_level
# no squelch required on digital voice modes # no squelch required on digital voice modes
actual_squelch = 0 if self.isDigitalVoice() or self.isPacket() else self.squelch_level actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() else self.squelch_level
if self.running: if self.running:
self.modification_lock.acquire() self.modification_lock.acquire()
self.squelch_pipe_file.write("%g\n" % (float(actual_squelch))) self.squelch_pipe_file.write("%g\n" % (self.convertToLinear(actual_squelch)))
self.squelch_pipe_file.flush() self.squelch_pipe_file.flush()
self.modification_lock.release() self.modification_lock.release()
@ -647,7 +664,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

@ -3,15 +3,22 @@ FROM $BASE_IMAGE
RUN apk add --no-cache bash RUN apk add --no-cache bash
RUN ln -s /usr/local/lib /usr/local/lib64
ADD docker/scripts/direwolf-1.5.patch / ADD docker/scripts/direwolf-1.5.patch /
ADD docker/scripts/install-dependencies.sh / ADD docker/scripts/install-dependencies.sh /
RUN /install-dependencies.sh RUN /install-dependencies.sh
ADD . /openwebrx ADD . /opt/openwebrx
WORKDIR /openwebrx WORKDIR /opt/openwebrx
RUN mkdir -p /tmp/openwebrx && \
mv "/opt/openwebrx/config_webrx.py" "/opt/openwebrx/config_webrx.py.orig" && \
sed 's/temporary_directory = "\/tmp"/temporary_directory = "\/tmp\/openwebrx"/' < "/opt/openwebrx/config_webrx.py.orig" > "/opt/openwebrx/config_webrx.py" && \
rm "/opt/openwebrx/config_webrx.py.orig"
VOLUME /config VOLUME /config
ENTRYPOINT [ "/openwebrx/docker/scripts/run.sh" ] ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ]
EXPOSE 8073 EXPOSE 8073

View File

@ -9,3 +9,6 @@ RUN /install-dependencies-hackrf.sh
RUN /install-dependencies-soapysdr.sh RUN /install-dependencies-soapysdr.sh
RUN /install-dependencies-sdrplay.sh RUN /install-dependencies-sdrplay.sh
RUN /install-dependencies-airspy.sh RUN /install-dependencies-airspy.sh
ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh

View File

@ -4,3 +4,5 @@ FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-rtlsdr.sh / ADD docker/scripts/install-dependencies-rtlsdr.sh /
RUN /install-dependencies-rtlsdr.sh RUN /install-dependencies-rtlsdr.sh
ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh

View File

@ -5,3 +5,5 @@ ADD docker/scripts/install-dependencies-sdrplay.sh /
ADD docker/scripts/install-lib.*.patch / ADD docker/scripts/install-lib.*.patch /
RUN /install-dependencies-sdrplay.sh RUN /install-dependencies-sdrplay.sh
ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh

View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
BUILD_PACKAGES="git cmake make gcc g++ musl-dev"
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/jketterl/owrx_connector.git
cmakebuild owrx_connector
apk del .build-deps

View File

@ -23,7 +23,7 @@ apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://git.code.sf.net/p/itpp/git itpp git clone https://git.code.sf.net/p/itpp/git itpp
cmakebuild itpp cmakebuild itpp
git clone https://github.com/jketterl/csdr.git -b 48khz_filter git clone https://github.com/jketterl/csdr.git -b docker_fixes
cd csdr cd csdr
make make
make install make install
@ -32,10 +32,6 @@ rm -rf csdr
git clone https://github.com/szechyjs/mbelib.git git clone https://github.com/szechyjs/mbelib.git
cmakebuild mbelib cmakebuild mbelib
if [ -d "/usr/local/lib64" ]; then
# no idea why it's put into there now. alpine does not handle it correctly, so move it.
mv /usr/local/lib64/libmbe* /usr/local/lib
fi
git clone https://github.com/jketterl/digiham.git git clone https://github.com/jketterl/digiham.git
cmakebuild digiham cmakebuild digiham

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

@ -3,6 +3,7 @@
This file is part of OpenWebRX, This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI. an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as
@ -39,6 +40,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 +57,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 +76,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 +86,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 +152,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
@ -163,6 +171,7 @@ input[type=range]:focus::-ms-fill-upper
background-repeat: repeat-x; background-repeat: repeat-x;
background-size: cover; background-size: cover;
background-color: #444; background-color: #444;
z-index: 1001;
} }
#openwebrx-bookmarks-container #openwebrx-bookmarks-container
@ -243,19 +252,28 @@ input[type=range]:focus::-ms-fill-upper
border-top-color: #0FF; border-top-color: #0FF;
} }
#webrx-canvas-container #openwebrx-bookmarks-container .bookmark[data-source=dial_frequencies] {
{ background-color: #0F0;
/*background-image:url('../gfx/openwebrx-blank-background-1.jpg');*/ }
position: relative;
height: 2000px; #openwebrx-bookmarks-container .bookmark[data-source=dial_frequencies]:after {
overflow-y: scroll; border-top-color: #0F0;
overflow-x: hidden; }
/*background-color: #646464;*/
/*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/ #webrx-canvas-background {
flex-grow: 1;
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;
}
#webrx-canvas-container
{
position: relative;
overflow: hidden;
cursor: crosshair; cursor: crosshair;
height: 100%;
} }
#webrx-canvas-container canvas #webrx-canvas-container canvas
@ -264,29 +282,17 @@ input[type=range]:focus::-ms-fill-upper
border-style: none; border-style: none;
image-rendering: crisp-edges; image-rendering: crisp-edges;
image-rendering: -webkit-optimize-contrast; image-rendering: -webkit-optimize-contrast;
/*transition: left 200ms, width 200ms;*/ width: 100%;
height: 200px;
} }
#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 +303,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 +363,37 @@ 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;
height: 0;
overflow: visible;
}
#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 +444,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 +551,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 +612,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;
} }
@ -652,7 +654,7 @@ img.openwebrx-mirror-img
font-family: 'expletus-sans-medium'; font-family: 'expletus-sans-medium';
} }
#openwebrx-big-grey #openwebrx-autoplay-overlay
{ {
position: fixed; position: fixed;
width: 100%; width: 100%;
@ -664,9 +666,6 @@ img.openwebrx-mirror-img
left: 0; left: 0;
top: 0; top: 0;
z-index: 1001; z-index: 1001;
display: none;
vertical-align: middle;
text-align: center;
color: white; color: white;
font-weight: bold; font-weight: bold;
font-size: 20pt; font-size: 20pt;
@ -674,11 +673,19 @@ img.openwebrx-mirror-img
transition: opacity 0.3s linear; transition: opacity 0.3s linear;
} }
#openwebrx-big-grey img #openwebrx-autoplay-overlay img
{ {
width: 150px; width: 150px;
} }
#openwebrx-autoplay-overlay .overlay-content {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
#openwebrx-digimode-canvas-container #openwebrx-digimode-canvas-container
{ {
/*margin: -10px -10px 10px -10px;*/ /*margin: -10px -10px 10px -10px;*/
@ -725,6 +732,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 +1029,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 +1038,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

@ -4,6 +4,7 @@
This file is part of OpenWebRX, This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI. an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as
@ -27,6 +28,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 +39,110 @@
<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>
<div id="openwebrx-mathbox-container"> </div> </div>
<div id="openwebrx-mathbox-container"> </div>
<div id="webrx-canvas-background">
<div id="webrx-canvas-container"> <div id="webrx-canvas-container">
<div id="openwebrx-phantom-canvas"></div>
<!-- add canvas here by javascript --> <!-- add canvas here by javascript -->
</div> </div>
<div id="openwebrx-panels-container"> </div>
<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 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</div>
<div>Author contact: <a href="http://blog.sdr.hu/about" target="_blank">András Retzler, HA7ILM</a></div>
<div>Author contact: <a href="http://www.justjakob.de/" target="_blank">Jakob Ketterl, DD5JFK</a></div>
<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 id="openwebrx-panels-container-right">
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" style="width: 259px;">
<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 +150,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 +188,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,93 +215,13 @@
</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-autoplay-overlay" style="display:none;">
<div id="openwebrx-play-button-text"> <div class="overlay-content">
<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 <div>Start OpenWebRX</div>
</div> </div>
</div> </div>
<div id="openwebrx-dialog-bookmark" class="openwebrx-dialog" style="display:none;"> <div id="openwebrx-dialog-bookmark" class="openwebrx-dialog" style="display:none;">

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

@ -9,6 +9,7 @@ from socketserver import ThreadingMixIn
from owrx.sdrhu import SdrHuUpdater from owrx.sdrhu import SdrHuUpdater
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
import logging import logging
@ -27,6 +28,7 @@ OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE fil
_________________________________________________________________________________________________ _________________________________________________________________________________________________
Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu> Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>
Author contact info: Jakob Ketterl, DD5JFK <dd5jfk@darc.de>
""" """
) )
@ -60,3 +62,5 @@ if __name__ == "__main__":
main() main()
except KeyboardInterrupt: except KeyboardInterrupt:
WebSocketConnection.closeAll() WebSocketConnection.closeAll()
Services.stop()
PskReporter.stop()

View File

@ -26,6 +26,7 @@ class Bookmark(object):
class Bookmarks(object): class Bookmarks(object):
sharedInstance = None sharedInstance = None
@staticmethod @staticmethod
def getSharedInstance(): def getSharedInstance():
if Bookmarks.sharedInstance is None: if Bookmarks.sharedInstance is None:

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
@ -56,7 +57,6 @@ class OpenWebRxReceiverClient(Client):
"waterfall_min_level", "waterfall_min_level",
"waterfall_max_level", "waterfall_max_level",
"waterfall_auto_level_margin", "waterfall_auto_level_margin",
"lfo_offset",
"samp_rate", "samp_rate",
"fft_size", "fft_size",
"fft_fps", "fft_fps",
@ -64,12 +64,13 @@ 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",
"mathbox_waterfall_history_length", "mathbox_waterfall_history_length",
"mathbox_waterfall_frequency_resolution", "mathbox_waterfall_frequency_resolution",
"initial_squelch_level",
"profile_id",
] ]
def __init__(self, conn): def __init__(self, conn):
@ -89,13 +90,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 +142,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 +158,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 +171,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 to support multiple sdrs
config["sdr_id"] = self.sdr.getId()
self.write_config(config) self.write_config(config)
cf = configProps["center_freq"] cf = configProps["center_freq"]
@ -177,6 +187,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 +244,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 +277,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

@ -1,10 +1,11 @@
import os
import subprocess import subprocess
from functools import reduce from functools import reduce
from operator import and_ from operator import and_
import re import re
from distutils.version import LooseVersion from distutils.version import LooseVersion
import inspect import inspect
from owrx.config import PropertyManager
import shlex
import logging import logging
@ -19,9 +20,12 @@ class FeatureDetector(object):
features = { features = {
"core": ["csdr", "nmux", "nc"], "core": ["csdr", "nmux", "nc"],
"rtl_sdr": ["rtl_sdr"], "rtl_sdr": ["rtl_sdr"],
"rtl_sdr_connector": ["rtl_connector"],
"sdrplay": ["rx_tools"], "sdrplay": ["rx_tools"],
"sdrplay_connector": ["soapy_connector"],
"hackrf": ["hackrf_transfer"], "hackrf": ["hackrf_transfer"],
"airspy": ["airspy_rx"], "airspy": ["airspy_rx"],
"airspy_connector": ["soapy_connector"],
"digital_voice_digiham": ["digiham", "sox"], "digital_voice_digiham": ["digiham", "sox"],
"digital_voice_dsd": ["dsd", "sox", "digiham"], "digital_voice_dsd": ["dsd", "sox", "digiham"],
"wsjt-x": ["wsjtx", "sox"], "wsjt-x": ["wsjtx", "sox"],
@ -83,7 +87,13 @@ class FeatureDetector(object):
return inspect.getdoc(self._get_requirement_method(requirement)) return inspect.getdoc(self._get_requirement_method(requirement))
def command_is_runnable(self, command): def command_is_runnable(self, command):
return os.system("{0} 2>/dev/null >/dev/null".format(command)) != 32512 tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"]
cmd = shlex.split(command)
try:
process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=tmp_dir)
return process.wait() != 32512
except FileNotFoundError:
return False
def has_csdr(self): def has_csdr(self):
""" """
@ -144,9 +154,6 @@ class FeatureDetector(object):
# TODO also check if it has the stdout feature # TODO also check if it has the stdout feature
return self.command_is_runnable("hackrf_transfer --help") return self.command_is_runnable("hackrf_transfer --help")
def command_exists(self, command):
return os.system("which {0}".format(command)) == 0
def has_digiham(self): def has_digiham(self):
""" """
To use digital voice modes, the digiham package is required. You can find the package and installation To use digital voice modes, the digiham package is required. You can find the package and installation
@ -163,7 +170,10 @@ class FeatureDetector(object):
def check_digiham_version(command): def check_digiham_version(command):
try: try:
process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
version = LooseVersion(digiham_version_regex.match(process.stdout.readline().decode()).group(1)) matches = digiham_version_regex.match(process.stdout.readline().decode())
if matches is None:
return False
version = LooseVersion(matches.group(1))
process.wait(1) process.wait(1)
return version >= required_version return version >= required_version
except FileNotFoundError: except FileNotFoundError:
@ -185,6 +195,40 @@ class FeatureDetector(object):
True, True,
) )
def _check_connector(self, command):
required_version = LooseVersion("0.1")
owrx_connector_version_regex = re.compile("^owrx-connector version (.*)$")
try:
process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
matches = owrx_connector_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_rtl_connector(self):
"""
The owrx_connector package offers direct interfacing between your hardware and openwebrx. It allows quicker
frequency switching, uses less CPU and can even provide more stability in some cases.
You can get it here: https://github.com/jketterl/owrx_connector
"""
return self._check_connector("rtl_connector")
def has_soapy_connector(self):
"""
The owrx_connector package offers direct interfacing between your hardware and openwebrx. It allows quicker
frequency switching, uses less CPU and can even provide more stability in some cases.
You can get it here: https://github.com/jketterl/owrx_connector
"""
return self._check_connector("soapy_connector")
def has_dsd(self): def has_dsd(self):
""" """
The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version
@ -208,7 +252,7 @@ class FeatureDetector(object):
""" """
In order to use an Airspy Receiver, you need to install the airspy_rx receiver software. In order to use an Airspy Receiver, you need to install the airspy_rx receiver software.
""" """
return self.command_is_runnable("airspy_rx --help 2> /dev/null") return self.command_is_runnable("airspy_rx --help")
def has_wsjtx(self): def has_wsjtx(self):
""" """

View File

@ -25,13 +25,23 @@ class Map(object):
def __init__(self): def __init__(self):
self.clients = [] self.clients = []
self.positions = {} self.positions = {}
self.positionsLock = threading.Lock()
def removeLoop(): def removeLoop():
loops = 0
while True: while True:
try: try:
self.removeOldPositions() self.removeOldPositions()
except Exception: except Exception:
logger.exception("error while removing old map positions") logger.exception("error while removing old map positions")
loops += 1
# rebuild the positions dictionary every once in a while, it consumes lots of memory otherwise
if loops == 60:
try:
self.rebuildPositions()
except Exception:
logger.exception("error while rebuilding positions")
loops = 0
time.sleep(60) time.sleep(60)
threading.Thread(target=removeLoop, daemon=True).start() threading.Thread(target=removeLoop, daemon=True).start()
@ -64,7 +74,8 @@ class Map(object):
def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None): def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None):
ts = datetime.now() ts = datetime.now()
self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band} with self.positionsLock:
self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band}
self.broadcast( self.broadcast(
[ [
{ {
@ -80,13 +91,15 @@ class Map(object):
def touchLocation(self, callsign): def touchLocation(self, callsign):
# not implemented on the client side yet, so do not use! # not implemented on the client side yet, so do not use!
ts = datetime.now() ts = datetime.now()
if callsign in self.positions: with self.positionsLock:
self.positions[callsign]["updated"] = ts if callsign in self.positions:
self.positions[callsign]["updated"] = ts
self.broadcast([{"callsign": callsign, "lastseen": ts.timestamp() * 1000}]) self.broadcast([{"callsign": callsign, "lastseen": ts.timestamp() * 1000}])
def removeLocation(self, callsign): def removeLocation(self, callsign):
self.positions.pop(callsign, None) with self.positionsLock:
# TODO broadcast removal to clients del self.positions[callsign]
# TODO broadcast removal to clients
def removeOldPositions(self): def removeOldPositions(self):
pm = PropertyManager.getSharedInstance() pm = PropertyManager.getSharedInstance()
@ -97,6 +110,11 @@ class Map(object):
for callsign in to_be_removed: for callsign in to_be_removed:
self.removeLocation(callsign) self.removeLocation(callsign)
def rebuildPositions(self):
with self.positionsLock:
p = {key: value for key, value in self.positions.items()}
self.positions = p
class LatLngLocation(Location): class LatLngLocation(Location):
def __init__(self, lat: float, lon: float): def __init__(self, lat: float, lon: float):

View File

@ -62,7 +62,7 @@ class DmrMetaEnricher(object):
cache = DmrCache.getSharedInstance() cache = DmrCache.getSharedInstance()
if not cache.isValid(id): if not cache.isValid(id):
if not id in self.threads: if not id in self.threads:
self.threads[id] = threading.Thread(target=self.downloadRadioIdData, args=[id]) self.threads[id] = threading.Thread(target=self.downloadRadioIdData, args=[id], daemon=True)
self.threads[id].start() self.threads[id].start()
return None return None
data = cache.get(id) data = cache.get(id)

View File

@ -3,10 +3,12 @@ import threading
import time import time
import random import random
import socket import socket
from sched import scheduler from functools import reduce
from operator import and_
from owrx.config import PropertyManager from owrx.config import PropertyManager
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
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -20,6 +22,9 @@ class PskReporterDummy(object):
def spot(self, spot): def spot(self, spot):
pass pass
def cancelTimer(self):
pass
class PskReporter(object): class PskReporter(object):
sharedInstance = None sharedInstance = None
@ -37,24 +42,46 @@ class PskReporter(object):
PskReporter.sharedInstance = PskReporterDummy() PskReporter.sharedInstance = PskReporterDummy()
return PskReporter.sharedInstance return PskReporter.sharedInstance
@staticmethod
def stop():
if PskReporter.sharedInstance:
PskReporter.sharedInstance.cancelTimer()
def __init__(self): def __init__(self):
self.spots = [] self.spots = []
self.spotLock = threading.Lock() self.spotLock = threading.Lock()
self.uploader = Uploader() self.uploader = Uploader()
self.scheduler = scheduler(time.time, time.sleep) self.timer = None
self.scheduleNextUpload() metrics = Metrics.getSharedInstance()
threading.Thread(target=self.scheduler.run).start() self.dupeCounter = CounterMetric()
metrics.addMetric("pskreporter.duplicates", self.dupeCounter)
self.spotCounter = CounterMetric()
metrics.addMetric("pskreporter.spots", self.spotCounter)
def scheduleNextUpload(self): def scheduleNextUpload(self):
if self.timer:
return
delay = PskReporter.interval + random.uniform(0, 30) delay = PskReporter.interval + random.uniform(0, 30)
logger.debug("scheduling next pskreporter upload in %f seconds", delay) logger.debug("scheduling next pskreporter upload in %f seconds", delay)
self.scheduler.enter(delay, 1, self.upload) self.timer = threading.Timer(delay, self.upload)
self.timer.start()
def spotEquals(self, s1, s2):
keys = ["callsign", "timestamp", "locator", "mode", "msg"]
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: if not spot["mode"] in PskReporter.supportedModes:
return return
with self.spotLock: with self.spotLock:
self.spots.append(spot) if any(x for x in self.spots if self.spotEquals(spot, x)):
# dupe
self.dupeCounter.inc()
else:
self.spotCounter.inc()
self.spots.append(spot)
self.scheduleNextUpload()
def upload(self): def upload(self):
try: try:
@ -67,8 +94,13 @@ class PskReporter(object):
except Exception: except Exception:
logger.exception("Failed to upload spots") logger.exception("Failed to upload spots")
self.timer = None
self.scheduleNextUpload() self.scheduleNextUpload()
def cancelTimer(self):
if self.timer:
self.timer.cancel()
class Uploader(object): class Uploader(object):
receieverDelimiter = [0x99, 0x92] receieverDelimiter = [0x99, 0x92]

View File

@ -1,13 +1,14 @@
import threading import threading
import socket from owrx.socket import getAvailablePort
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from owrx.source import SdrService from owrx.source import SdrService, SdrSource
from owrx.bands import Bandplan from owrx.bands import Bandplan
from csdr import dsp, output from csdr import dsp, output
from owrx.wsjt import WsjtParser 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
@ -109,34 +110,47 @@ class ServiceScheduler(object):
def __init__(self, source, schedule): def __init__(self, source, schedule):
self.source = source self.source = source
self.schedule = Schedule.parse(schedule) self.schedule = Schedule.parse(schedule)
self.active = False
self.source.addClient(self) self.source.addClient(self)
self.selectionTimer = None self.selectionTimer = None
self.source.getProps().collect("center_freq", "samp_rate").wire(self.onFrequencyChange)
self.scheduleSelection() self.scheduleSelection()
def shutdown(self):
self.cancelTimer()
self.source.removeClient(self)
def scheduleSelection(self, time=None): def scheduleSelection(self, time=None):
seconds = 10 seconds = 10
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 isActive(self): def cancelTimer(self):
return self.active if self.selectionTimer:
self.selectionTimer.cancel()
def onSdrAvailable(self): def getClientClass(self):
pass return SdrSource.CLIENT_BACKGROUND
def onSdrUnavailable(self): def onStateChange(self, state):
if state == SdrSource.STATE_STOPPING:
self.scheduleSelection()
elif state == SdrSource.STATE_FAILED:
self.cancelTimer()
def onBusyStateChange(self, state):
if state == SdrSource.BUSYSTATE_IDLE:
self.scheduleSelection()
def onFrequencyChange(self, name, value):
self.scheduleSelection() self.scheduleSelection()
def selectProfile(self): def selectProfile(self):
self.active = False if self.source.hasClients(SdrSource.CLIENT_USER):
if self.source.hasActiveClients(): logger.debug("source has active users; not touching")
logger.debug("source has active clients; not touching")
return return
logger.debug("source seems to be idle, selecting profile for background services") logger.debug("source seems to be idle, selecting profile for background services")
entry = self.schedule.getCurrentEntry() entry = self.schedule.getCurrentEntry()
@ -152,7 +166,6 @@ class ServiceScheduler(object):
self.scheduleSelection(entry.getScheduledEnd()) self.scheduleSelection(entry.getScheduledEnd())
try: try:
self.active = True
self.source.activateProfile(entry.getProfile()) self.source.activateProfile(entry.getProfile())
self.source.start() self.source.start()
except KeyError: except KeyError:
@ -170,21 +183,51 @@ class ServiceHandler(object):
props.collect("center_freq", "samp_rate").wire(self.onFrequencyChange) props.collect("center_freq", "samp_rate").wire(self.onFrequencyChange)
if self.source.isAvailable(): if self.source.isAvailable():
self.scheduleServiceStartup() self.scheduleServiceStartup()
self.scheduler = None
if "schedule" in props: if "schedule" in props:
ServiceScheduler(self.source, props["schedule"]) self.scheduler = ServiceScheduler(self.source, props["schedule"])
def isActive(self): def getClientClass(self):
return False return SdrSource.CLIENT_INACTIVE
def onSdrAvailable(self): def onStateChange(self, state):
self.scheduleServiceStartup() if state == SdrSource.STATE_RUNNING:
self.scheduleServiceStartup()
elif state == SdrSource.STATE_STOPPING:
logger.debug("sdr source becoming unavailable; stopping services.")
self.stopServices()
elif state == SdrSource.STATE_FAILED:
logger.debug("sdr source failed; stopping services.")
self.stopServices()
def onSdrUnavailable(self): def onBusyStateChange(self, state):
logger.debug("sdr source becoming unavailable; stopping services.") pass
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 shutdown(self):
self.stopServices()
self.source.removeClient(self)
if self.scheduler:
self.scheduler.shutdown()
def stopServices(self): def stopServices(self):
with self.lock: with self.lock:
@ -206,14 +249,6 @@ class ServiceHandler(object):
self.startupTimer = threading.Timer(10, self.updateServices) self.startupTimer = threading.Timer(10, self.updateServices)
self.startupTimer.start() self.startupTimer.start()
def getAvailablePort(self):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("", 0))
s.listen(1)
port = s.getsockname()[1]
s.close()
return port
def updateServices(self): def updateServices(self):
logger.debug("re-scheduling services due to sdr changes") logger.debug("re-scheduling services due to sdr changes")
self.stopServices() self.stopServices()
@ -238,23 +273,30 @@ 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, getAvailablePort(), self.source)
resampler.start()
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))
# resampler goes in after the services since it must not be shutdown as long as the services are still running
self.services.append(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 +327,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 +378,20 @@ 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.shutdown()
Services.handlers = []
class Service(object): class Service(object):

10
owrx/socket.py Normal file
View File

@ -0,0 +1,10 @@
import socket
def getAvailablePort():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("", 0))
s.listen(1)
port = s.getsockname()[1]
s.close()
return port

View File

@ -5,6 +5,7 @@ from owrx.meta import MetaParser
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
from owrx.aprs import AprsParser from owrx.aprs import AprsParser
from owrx.metrics import Metrics, DirectMetric from owrx.metrics import Metrics, DirectMetric
from owrx.socket import getAvailablePort
import threading import threading
import csdr import csdr
import time import time
@ -75,10 +76,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,28 +95,33 @@ 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): STATE_STOPPED = 0
STATE_STARTING = 1
STATE_RUNNING = 2
STATE_STOPPING = 3
STATE_TUNING = 4
STATE_FAILED = 5
BUSYSTATE_IDLE = 0
BUSYSTATE_BUSY = 1
CLIENT_INACTIVE = 0
CLIENT_BACKGROUND = 1
CLIENT_USER = 2
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(*self.getEventNames()).defaults(PropertyManager.getSharedInstance())
"samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain" self.wireEvents()
).defaults(PropertyManager.getSharedInstance())
def restart(name, value):
logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value))
self.stop()
self.start()
self.rtlProps.wire(restart)
self.port = port self.port = port
self.monitor = None self.monitor = None
self.clients = [] self.clients = []
@ -118,6 +129,31 @@ 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
self.state = SdrSource.STATE_STOPPED
self.busyState = SdrSource.BUSYSTATE_IDLE
def getEventNames(self):
return [
"samp_rate",
"nmux_memory",
"center_freq",
"ppm",
"rf_gain",
"lna_gain",
"rf_amp",
"antenna",
"if_gain",
"lfo_offset",
]
def wireEvents(self):
def restart(name, value):
logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value))
self.stop()
self.start()
self.rtlProps.wire(restart)
# override this in subclasses # override this in subclasses
def getCommand(self): def getCommand(self):
@ -131,14 +167,24 @@ 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]
self.props["profile_id"] = 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.
if key == "name": if key == "name":
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"]
@ -151,6 +197,17 @@ class SdrSource(object):
def getPort(self): def getPort(self):
return self.port return self.port
def useNmux(self):
return True
def getCommandValues(self):
dict = self.rtlProps.collect(*self.getEventNames()).__dict__()
if "lfo_offset" in dict and dict["lfo_offset"] is not None:
dict["tuner_freq"] = dict["center_freq"] + dict["lfo_offset"]
else:
dict["tuner_freq"] = dict["center_freq"]
return dict
def start(self): def start(self):
self.modificationLock.acquire() self.modificationLock.acquire()
if self.monitor: if self.monitor:
@ -159,33 +216,31 @@ class SdrSource(object):
props = self.rtlProps props = self.rtlProps
start_sdr_command = self.getCommand().format( cmd = self.getCommand().format(**self.getCommandValues())
**props.collect(
"samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain"
).__dict__()
)
format_conversion = self.getFormatConversion() format_conversion = self.getFormatConversion()
if format_conversion is not None: if format_conversion is not None:
start_sdr_command += " | " + format_conversion cmd += " | " + format_conversion
nmux_bufcnt = nmux_bufsize = 0 if self.useNmux():
while nmux_bufsize < props["samp_rate"] / 4: nmux_bufcnt = nmux_bufsize = 0
nmux_bufsize += 4096 while nmux_bufsize < props["samp_rate"] / 4:
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: nmux_bufsize += 4096
nmux_bufcnt += 1 while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6:
if nmux_bufcnt == 0 or nmux_bufsize == 0: nmux_bufcnt += 1
logger.error( if nmux_bufcnt == 0 or nmux_bufsize == 0:
"Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py" logger.error(
"Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
)
self.modificationLock.release()
return
logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt))
cmd = cmd + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (
nmux_bufsize,
nmux_bufcnt,
self.port,
) )
self.modificationLock.release()
return
logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt))
cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (
nmux_bufsize,
nmux_bufcnt,
self.port,
)
self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp)
logger.info("Started rtl source: " + cmd) logger.info("Started rtl source: " + cmd)
@ -213,20 +268,26 @@ class SdrSource(object):
except: except:
time.sleep(0.1) time.sleep(0.1)
if not available:
self.failed = True
self.postStart()
self.modificationLock.release() self.modificationLock.release()
if not available: self.setState(SdrSource.STATE_FAILED if self.failed else SdrSource.STATE_RUNNING)
raise SdrSourceException("rtl source failed to start up")
for c in self.clients: def postStart(self):
c.onSdrAvailable() pass
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: self.setState(SdrSource.STATE_STOPPING)
c.onSdrUnavailable()
self.modificationLock.acquire() self.modificationLock.acquire()
@ -241,24 +302,33 @@ class SdrSource(object):
self.sleepOnRestart() self.sleepOnRestart()
self.modificationLock.release() self.modificationLock.release()
self.setState(SdrSource.STATE_STOPPED)
def sleepOnRestart(self): def sleepOnRestart(self):
pass pass
def hasActiveClients(self): def hasClients(self, *args):
activeClients = [c for c in self.clients if c.isActive()] clients = [c for c in self.clients if c.getClientClass() in args]
return len(activeClients) > 0 return len(clients) > 0
def addClient(self, c): def addClient(self, c):
self.clients.append(c) self.clients.append(c)
if self.hasActiveClients(): hasUsers = self.hasClients(SdrSource.CLIENT_USER)
hasBackgroundTasks = self.hasClients(SdrSource.CLIENT_BACKGROUND)
if hasUsers or hasBackgroundTasks:
self.start() self.start()
self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE)
def removeClient(self, c): def removeClient(self, c):
try: try:
self.clients.remove(c) self.clients.remove(c)
except ValueError: except ValueError:
pass pass
if not self.hasActiveClients():
hasUsers = self.hasClients(SdrSource.CLIENT_USER)
hasBackgroundTasks = self.hasClients(SdrSource.CLIENT_BACKGROUND)
self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE)
if not hasUsers and not hasBackgroundTasks:
self.stop() self.stop()
def addSpectrumClient(self, c): def addSpectrumClient(self, c):
@ -280,6 +350,20 @@ class SdrSource(object):
for c in self.spectrumClients: for c in self.spectrumClients:
c.write_spectrum_data(data) c.write_spectrum_data(data)
def setState(self, state):
if state == self.state:
return
self.state = state
for c in self.clients:
c.onStateChange(state)
def setBusyState(self, state):
if state == self.busyState:
return
self.busyState = state
for c in self.clients:
c.onBusyStateChange(state)
class Resampler(SdrSource): class Resampler(SdrSource):
def __init__(self, props, port, sdr): def __init__(self, props, port, sdr):
@ -291,14 +375,19 @@ 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()
return return
self.setState(SdrSource.STATE_STARTING)
props = self.rtlProps props = self.rtlProps
resampler_command = [ resampler_command = [
@ -353,21 +442,140 @@ 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: self.setState(SdrSource.STATE_FAILED if self.failed else SdrSource.STATE_RUNNING)
raise SdrSourceException("resampler source failed to start up")
for c in self.clients:
c.onSdrAvailable()
def activateProfile(self, profile_id=None): def activateProfile(self, profile_id=None):
pass pass
class ConnectorSource(SdrSource):
def __init__(self, id, props, port):
super().__init__(id, props, port)
self.controlSocket = None
self.controlPort = getAvailablePort()
def sendControlMessage(self, prop, value):
logger.debug("sending property change over control socket: {0} changed to {1}".format(prop, value))
self.controlSocket.sendall("{prop}:{value}\n".format(prop=prop, value=value).encode())
def wireEvents(self):
def reconfigure(prop, value):
if self.monitor is None:
return
if (
(prop == "center_freq" or prop == "lfo_offset")
and "lfo_offset" in self.rtlProps
and self.rtlProps["lfo_offset"] is not None
):
freq = self.rtlProps["center_freq"] + self.rtlProps["lfo_offset"]
self.sendControlMessage("center_freq", freq)
else:
self.sendControlMessage(prop, value)
self.rtlProps.wire(reconfigure)
def postStart(self):
logger.debug("opening control socket...")
self.controlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.controlSocket.connect(("localhost", self.controlPort))
def stop(self):
super().stop()
if self.controlSocket:
self.controlSocket.close()
self.controlSocket = None
def getFormatConversion(self):
return None
def useNmux(self):
return False
class RtlSdrConnectorSource(ConnectorSource):
def getEventNames(self):
return [
"samp_rate",
"center_freq",
"ppm",
"rf_gain",
"device",
"iqswap",
"lfo_offset",
]
def getCommand(self):
cmd = (
"rtl_connector -p {port} -c {controlPort}".format(port=self.port, controlPort=self.controlPort)
+ " -s {samp_rate} -f {tuner_freq} -g {rf_gain} -P {ppm}"
)
if "device" in self.rtlProps and self.rtlProps["device"] is not None:
cmd += ' -d "{device}"'
if self.rtlProps["iqswap"]:
cmd += " -i"
return cmd
class SdrplayConnectorSource(ConnectorSource):
def getEventNames(self):
return [
"samp_rate",
"center_freq",
"ppm",
"rf_gain",
"antenna",
"device",
"iqswap",
"lfo_offset",
]
def getCommand(self):
cmd = (
"soapy_connector -p {port} -c {controlPort}".format(port=self.port, controlPort=self.controlPort)
+ ' -s {samp_rate} -f {tuner_freq} -g "{rf_gain}" -P {ppm} -a "{antenna}"'
)
if "device" in self.rtlProps and self.rtlProps["device"] is not None:
cmd += ' -d "{device}"'
if self.rtlProps["iqswap"]:
cmd += " -i"
return cmd
class AirspyConnectorSource(ConnectorSource):
def getEventNames(self):
return [
"samp_rate",
"center_freq",
"ppm",
"rf_gain",
"device",
"iqswap",
"lfo_offset",
"bias_tee",
]
def getCommand(self):
cmd = (
"soapy_connector -p {port} -c {controlPort}".format(port=self.port, controlPort=self.controlPort)
+ ' -s {samp_rate} -f {tuner_freq} -g "{rf_gain}" -P {ppm}'
)
if "device" in self.rtlProps and self.rtlProps["device"] is not None:
cmd += ' -d "{device}"'
if self.rtlProps["iqswap"]:
cmd += " -i"
if self.rtlProps["bias_tee"]:
cmd += " -t biastee=true"
return cmd
class RtlSdrSource(SdrSource): class RtlSdrSource(SdrSource):
def getCommand(self): def getCommand(self):
return "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -" return "rtl_sdr -s {samp_rate} -f {tuner_freq} -p {ppm} -g {rf_gain} -"
def getFormatConversion(self): def getFormatConversion(self):
return "csdr convert_u8_f" return "csdr convert_u8_f"
@ -375,7 +583,7 @@ class RtlSdrSource(SdrSource):
class HackrfSource(SdrSource): class HackrfSource(SdrSource):
def getCommand(self): def getCommand(self):
return "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-" return "hackrf_transfer -s {samp_rate} -f {tuner_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-"
def getFormatConversion(self): def getFormatConversion(self):
return "csdr convert_s8_f" return "csdr convert_s8_f"
@ -383,7 +591,7 @@ class HackrfSource(SdrSource):
class SdrplaySource(SdrSource): class SdrplaySource(SdrSource):
def getCommand(self): def getCommand(self):
command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}" command = "rx_sdr -F CF32 -s {samp_rate} -f {tuner_freq} -p {ppm}"
gainMap = {"rf_gain": "RFGR", "if_gain": "IFGR"} gainMap = {"rf_gain": "RFGR", "if_gain": "IFGR"}
gains = [ gains = [
"{0}={{{1}}}".format(gainMap[name], name) "{0}={{{1}}}".format(gainMap[name], name)
@ -403,7 +611,7 @@ class SdrplaySource(SdrSource):
class AirspySource(SdrSource): class AirspySource(SdrSource):
def getCommand(self): def getCommand(self):
frequency = self.props["center_freq"] / 1e6 frequency = self.props["tuner_freq"] / 1e6
command = "airspy_rx" command = "airspy_rx"
command += " -f{0}".format(frequency) command += " -f{0}".format(frequency)
command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}" command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}"
@ -484,14 +692,17 @@ class SpectrumThread(csdr.output):
c.cancel() c.cancel()
self.subscriptions = [] self.subscriptions = []
def isActive(self): def getClientClass(self):
return True return SdrSource.CLIENT_USER
def onSdrAvailable(self): def onStateChange(self, state):
self.dsp.start() if state in [SdrSource.STATE_STOPPING, SdrSource.STATE_FAILED]:
self.dsp.stop()
elif state == SdrSource.STATE_RUNNING:
self.dsp.start()
def onSdrUnavailable(self): def onBusyStateChange(self, state):
self.dsp.stop() pass
class DspManager(csdr.output): class DspManager(csdr.output):
@ -616,16 +827,23 @@ class DspManager(csdr.output):
def setProperty(self, prop, value): def setProperty(self, prop, value):
self.localProps.getProperty(prop).setValue(value) self.localProps.getProperty(prop).setValue(value)
def isActive(self): def getClientClass(self):
return True return SdrSource.CLIENT_USER
def onSdrAvailable(self): def onStateChange(self, state):
logger.debug("received onSdrAvailable, attempting DspSource restart") if state == SdrSource.STATE_RUNNING:
self.dsp.start() logger.debug("received STATE_RUNNING, attempting DspSource restart")
self.dsp.start()
elif state == SdrSource.STATE_STOPPING:
logger.debug("received STATE_STOPPING, shutting down DspSource")
self.dsp.stop()
elif state == SdrSource.STATE_FAILED:
logger.debug("received STATE_FAILED, shutting down DspSource")
self.dsp.stop()
self.handler.handleSdrFailure("sdr device failed")
def onSdrUnavailable(self): def onBusyStateChange(self, state):
logger.debug("received onSdrUnavailable, shutting down DspSource") pass
self.dsp.stop()
class CpuUsageThread(threading.Thread): class CpuUsageThread(threading.Thread):

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.
@ -111,7 +120,6 @@ class WebSocketConnection(object):
self._sendBytes(data_to_send) self._sendBytes(data_to_send)
def _sendBytes(self, data_to_send): def _sendBytes(self, data_to_send):
def chunks(l, n): def chunks(l, n):
"""Yield successive n-sized chunks from l.""" """Yield successive n-sized chunks from l."""
for i in range(0, len(l), n): for i in range(0, len(l), n):
@ -140,6 +148,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 +177,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 +217,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 +245,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

View File

@ -1,8 +1,6 @@
import threading import threading
import wave import wave
from datetime import datetime, timedelta, date, timezone from datetime import datetime, timedelta, timezone
import time
import sched
import subprocess import subprocess
import os import os
from multiprocessing.connection import Pipe from multiprocessing.connection import Pipe
@ -93,8 +91,7 @@ class WsjtChopper(threading.Thread):
self.tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"] self.tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"]
(self.wavefilename, self.wavefile) = self.getWaveFile() (self.wavefilename, self.wavefile) = self.getWaveFile()
self.switchingLock = threading.Lock() self.switchingLock = threading.Lock()
self.scheduler = sched.scheduler(time.time, time.sleep) self.timer = None
self.schedulerLock = threading.Lock()
(self.outputReader, self.outputWriter) = Pipe() (self.outputReader, self.outputWriter) = Pipe()
self.doRun = True self.doRun = True
super().__init__() super().__init__()
@ -110,27 +107,23 @@ class WsjtChopper(threading.Thread):
return filename, wavefile return filename, wavefile
def getNextDecodingTime(self): def getNextDecodingTime(self):
t = datetime.now() t = datetime.utcnow()
zeroed = t.replace(minute=0, second=0, microsecond=0) zeroed = t.replace(minute=0, second=0, microsecond=0)
delta = t - zeroed delta = t - zeroed
seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval
t = zeroed + timedelta(seconds=seconds) t = zeroed + timedelta(seconds=seconds)
logger.debug("scheduling: {0}".format(t)) logger.debug("scheduling: {0}".format(t))
return t.timestamp() return t
def startScheduler(self): def cancelTimer(self):
self._scheduleNextSwitch() if self.timer:
threading.Thread(target=self.scheduler.run).start() self.timer.cancel()
def emptyScheduler(self):
with self.schedulerLock:
for event in self.scheduler.queue:
self.scheduler.cancel(event)
def _scheduleNextSwitch(self): def _scheduleNextSwitch(self):
with self.schedulerLock: if self.doRun:
if self.doRun: delta = self.getNextDecodingTime() - datetime.utcnow()
self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles) self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
self.timer.start()
def switchFiles(self): def switchFiles(self):
self.switchingLock.acquire() self.switchingLock.acquire()
@ -169,7 +162,7 @@ class WsjtChopper(threading.Thread):
def run(self) -> None: def run(self) -> None:
logger.debug("WSJT chopper starting up") logger.debug("WSJT chopper starting up")
self.startScheduler() self._scheduleNextSwitch()
while self.doRun: while self.doRun:
data = self.source.read(256) data = self.source.read(256)
if data is None or (isinstance(data, bytes) and len(data) == 0): if data is None or (isinstance(data, bytes) and len(data) == 0):
@ -182,7 +175,7 @@ class WsjtChopper(threading.Thread):
logger.debug("WSJT chopper shutting down") logger.debug("WSJT chopper shutting down")
self.outputReader.close() self.outputReader.close()
self.outputWriter.close() self.outputWriter.close()
self.emptyScheduler() self.cancelTimer()
try: try:
os.unlink(self.wavefilename) os.unlink(self.wavefilename)
except Exception: except Exception:

View File

@ -3,6 +3,14 @@ set -euxo pipefail
ARCH=$(uname -m) ARCH=$(uname -m)
ALL_ARCHS="x86_64 armv7l"
for image in openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-full openwebrx; do for image in openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-full openwebrx; do
docker push jketterl/$image:$ARCH docker push jketterl/$image:$ARCH
IMAGE_LIST=""
for a in $ALL_ARCHS; do
IMAGE_LIST="$IMAGE_LIST jketterl/$image:$a"
done
docker manifest create --amend jketterl/$image:latest $IMAGE_LIST
docker manifest push --purge jketterl/$image:latest
done done

View File

@ -4,6 +4,7 @@
This file is part of OpenWebRX, This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI. an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as