47 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
29 changed files with 685 additions and 278 deletions

5
.dockerignore Normal file
View File

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

4
.gitignore vendored
View File

@ -1,4 +1,4 @@
*.pyc **/*.pyc
*.swp **/*.swp
tags tags
.idea .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 <randras@sdr.hu>* *Jakob Ketterl, DD5JFK <dd5jfk@darc.de>*
## Usage tips ## Usage tips

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
@ -66,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"
@ -98,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": {
@ -128,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": {

61
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,
@ -520,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()

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

@ -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
@ -170,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
@ -258,16 +260,20 @@ input[type=range]:focus::-ms-fill-upper
border-top-color: #0F0; border-top-color: #0F0;
} }
#webrx-canvas-container #webrx-canvas-background {
{
flex-grow: 1; flex-grow: 1;
position: relative;
overflow: hidden;
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; background-size: cover;
}
#webrx-canvas-container
{
position: relative;
overflow: hidden;
cursor: crosshair; cursor: crosshair;
height: 100%;
} }
#webrx-canvas-container canvas #webrx-canvas-container canvas
@ -276,7 +282,8 @@ 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
@ -363,6 +370,8 @@ input[type=range]:focus::-ms-fill-upper
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
height: 0;
overflow: visible;
} }
#openwebrx-panels-container-left { #openwebrx-panels-container-left {
@ -645,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%;
@ -657,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;
@ -667,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;*/

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
@ -45,8 +46,10 @@
</div> </div>
</div> </div>
<div id="openwebrx-mathbox-container"> </div> <div id="openwebrx-mathbox-container"> </div>
<div id="webrx-canvas-container"> <div id="webrx-canvas-background">
<!-- add canvas here by javascript --> <div id="webrx-canvas-container">
<!-- add canvas here by javascript -->
</div>
</div> </div>
<div id="openwebrx-panels-container"> <div id="openwebrx-panels-container">
<div id="openwebrx-panels-container-left"> <div id="openwebrx-panels-container-left">
@ -116,8 +119,9 @@
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" style="width: 619px;"> <div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" style="width: 619px;">
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll"> <div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
<div class="nano-content"> <div class="nano-content">
<div id="openwebrx-client-log-title">OpenWebRX client log</strong></div> <div id="openwebrx-client-log-title">OpenWebRX client log</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>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 id="openwebrx-debugdiv"></div>
</div> </div>
</div> </div>
@ -214,10 +218,10 @@
</div> </div>
</div> </div>
</div> </div>
<div id="openwebrx-big-grey" onclick="playButtonClick();"> <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

@ -1,6 +1,6 @@
function Measurement() { function Measurement() {
this.reset(); this.reset();
}; }
Measurement.prototype.add = function(v) { Measurement.prototype.add = function(v) {
this.value += v; this.value += v;
@ -25,7 +25,7 @@ Measurement.prototype.reset = function() {
Measurement.prototype.report = function(range, interval, callback) { Measurement.prototype.report = function(range, interval, callback) {
return new Reporter(this, range, interval, callback); return new Reporter(this, range, interval, callback);
} };
function Reporter(measurement, range, interval, callback) { function Reporter(measurement, range, interval, callback) {
this.measurement = measurement; this.measurement = measurement;
@ -33,7 +33,7 @@ function Reporter(measurement, range, interval, callback) {
this.samples = []; this.samples = [];
this.callback = callback; this.callback = callback;
this.interval = setInterval(this.report.bind(this), interval); this.interval = setInterval(this.report.bind(this), interval);
}; }
Reporter.prototype.sample = function(){ Reporter.prototype.sample = function(){
this.samples.push({ this.samples.push({

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
@ -140,9 +141,8 @@ function setSquelchToAuto() {
} }
function updateSquelch() { function updateSquelch() {
var sliderValue = parseInt(e("openwebrx-panel-squelch").value); var sliderValue = parseInt($("#openwebrx-panel-squelch").val());
var outputValue = (sliderValue === parseInt(e("openwebrx-panel-squelch").min)) ? 0 : getLinearSmeterValue(sliderValue); ws.send(JSON.stringify({"type": "dspcontrol", "params": {"squelch_level": sliderValue}}));
ws.send(JSON.stringify({"type": "dspcontrol", "params": {"squelch_level": outputValue}}));
} }
var waterfall_min_level; var waterfall_min_level;
@ -196,7 +196,8 @@ function setSquelchSliderBackground(val) {
var relative = (val - min) / (max - min); var relative = (val - min) / (max - min);
// use a brighter color when squelch is open // use a brighter color when squelch is open
var color = val >= sliderPosition ? '#22ff2f' : '#008908'; var color = val >= sliderPosition ? '#22ff2f' : '#008908';
var style = 'linear-gradient(90deg, ' + color + ' ' + relative * 100 + '%, #B6B6B6 ' + (1 - relative) * 100 + '%)'; // we don't use the gradient, but separate the colors discretely using css tricks
var style = 'linear-gradient(90deg, ' + color + ', ' + color + ' ' + relative * 100 + '%, #B6B6B6 ' + relative * 100 + '%)';
$slider.css('--track-background', style); $slider.css('--track-background', style);
} }
@ -562,6 +563,10 @@ function demodulator_set_offset_frequency(which, to_what) {
$("#webrx-actual-freq").html(format_frequency("{x} MHz", center_freq + to_what, 1e6, 4)); $("#webrx-actual-freq").html(format_frequency("{x} MHz", center_freq + to_what, 1e6, 4));
} }
function waterfallWidth() {
return $('body').width();
}
// ======================================================== // ========================================================
// =================== SCALE ROUTINES =================== // =================== SCALE ROUTINES ===================
@ -601,7 +606,7 @@ function scale_canvas_mousedown(evt) {
function scale_offset_freq_from_px(x, visible_range) { function scale_offset_freq_from_px(x, visible_range) {
if (typeof visible_range === "undefined") visible_range = get_visible_freq_range(); if (typeof visible_range === "undefined") visible_range = get_visible_freq_range();
return (visible_range.start + visible_range.bw * (x / canvas_container.clientWidth)) - center_freq; return (visible_range.start + visible_range.bw * (x / waterfallWidth())) - center_freq;
} }
function scale_canvas_mousemove(evt) { function scale_canvas_mousemove(evt) {
@ -642,20 +647,20 @@ function scale_canvas_mouseup(evt) {
} }
function scale_px_from_freq(f, range) { function scale_px_from_freq(f, range) {
return Math.round(((f - range.start) / range.bw) * canvas_container.clientWidth); return Math.round(((f - range.start) / range.bw) * waterfallWidth());
} }
function get_visible_freq_range() { function get_visible_freq_range() {
var out = {}; var out = {};
var fcalc = function (x) { var fcalc = function (x) {
var canvasWidth = canvas_container.clientWidth * zoom_levels[zoom_level]; var canvasWidth = waterfallWidth() * zoom_levels[zoom_level];
return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2); return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2);
}; };
out.start = fcalc(0); out.start = fcalc(0);
out.center = fcalc(canvas_container.clientWidth / 2); out.center = fcalc(waterfallWidth() / 2);
out.end = fcalc(canvas_container.clientWidth); out.end = fcalc(waterfallWidth());
out.bw = out.end - out.start; out.bw = out.end - out.start;
out.hps = out.bw / canvas_container.clientWidth; out.hps = out.bw / waterfallWidth();
return out; return out;
} }
@ -731,7 +736,7 @@ function get_scale_mark_spacing(range) {
var out = {}; var out = {};
var fcalc = function (freq) { var fcalc = function (freq) {
out.numlarge = (range.bw / freq); out.numlarge = (range.bw / freq);
out.large = canvas_container.clientWidth / out.numlarge; //distance between large markers (these have text) out.large = waterfallWidth() / out.numlarge; //distance between large markers (these have text)
out.ratio = 5; //(ratio-1) small markers exist per large marker out.ratio = 5; //(ratio-1) small markers exist per large marker
out.small = out.large / out.ratio; //distance between small markers out.small = out.large / out.ratio; //distance between small markers
if (out.small < scale_min_space_bw_small_markers) return false; if (out.small < scale_min_space_bw_small_markers) return false;
@ -890,8 +895,8 @@ function canvas_mousemove(evt) {
var deltaX = canvas_drag_last_x - evt.pageX; var deltaX = canvas_drag_last_x - evt.pageX;
var dpx = range.hps * deltaX; var dpx = range.hps * deltaX;
if ( if (
!(zoom_center_rel + dpx > (bandwidth / 2 - canvas_container.clientWidth * (1 - zoom_center_where) * range.hps)) && !(zoom_center_rel + dpx > (bandwidth / 2 - waterfallWidth() * (1 - zoom_center_where) * range.hps)) &&
!(zoom_center_rel + dpx < -bandwidth / 2 + canvas_container.clientWidth * zoom_center_where * range.hps) !(zoom_center_rel + dpx < -bandwidth / 2 + waterfallWidth() * zoom_center_where * range.hps)
) { ) {
zoom_center_rel += dpx; zoom_center_rel += dpx;
} }
@ -928,14 +933,18 @@ function canvas_end_drag() {
} }
function zoom_center_where_calc(screenposX) { function zoom_center_where_calc(screenposX) {
//return (screenposX-(window.innerWidth-canvas_container.clientWidth))/canvas_container.clientWidth; return screenposX / waterfallWidth();
return screenposX / canvas_container.clientWidth;
} }
function get_relative_x(evt) { function get_relative_x(evt) {
var relativeX = (evt.offsetX) ? evt.offsetX : evt.layerX; var relativeX = evt.offsetX || evt.layerX;
if ($(evt.target).closest(canvas_container).length) return relativeX; if ($(evt.target).closest(canvas_container).length) return relativeX;
// compensate for the frequency scale, since that is not resized by the browser. // compensate for the frequency scale, since that is not resized by the browser.
var relatives = $(evt.target).closest('#openwebrx-frequency-container').map(function(){
return evt.pageX - this.offsetLeft;
});
if (relatives.length) relativeX = relatives[0];
return relativeX - zoom_offset_px; return relativeX - zoom_offset_px;
} }
@ -992,7 +1001,7 @@ function zoom_set(level) {
if (!(level >= 0 && level <= zoom_levels.length - 1)) return; if (!(level >= 0 && level <= zoom_levels.length - 1)) return;
level = parseInt(level); level = parseInt(level);
zoom_level = level; zoom_level = level;
//zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+canvas_container.clientWidth/2); //zoom to screen center instead of demod envelope //zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+waterfallWidth()/2); //zoom to screen center instead of demod envelope
zoom_center_rel = demodulators[0].offset_frequency; zoom_center_rel = demodulators[0].offset_frequency;
zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack
resize_canvases(true); resize_canvases(true);
@ -1001,13 +1010,12 @@ function zoom_set(level) {
} }
function zoom_calc() { function zoom_calc() {
var winsize = canvas_container.clientWidth; var winsize = waterfallWidth();
var canvases_new_width = winsize * zoom_levels[zoom_level]; var canvases_new_width = winsize * zoom_levels[zoom_level];
zoom_offset_px = -((canvases_new_width * (0.5 + zoom_center_rel / bandwidth)) - (winsize * zoom_center_where)); zoom_offset_px = -((canvases_new_width * (0.5 + zoom_center_rel / bandwidth)) - (winsize * zoom_center_where));
if (zoom_offset_px > 0) zoom_offset_px = 0; if (zoom_offset_px > 0) zoom_offset_px = 0;
if (zoom_offset_px < winsize - canvases_new_width) if (zoom_offset_px < winsize - canvases_new_width)
zoom_offset_px = winsize - canvases_new_width; zoom_offset_px = winsize - canvases_new_width;
//console.log("zoom_calc || zopx:"+zoom_offset_px.toString()+ " maxoff:"+(winsize-canvases_new_width).toString()+" relval:"+(0.5+zoom_center_rel/bandwidth).toString() );
} }
var networkSpeedMeasurement; var networkSpeedMeasurement;
@ -1037,7 +1045,7 @@ function on_ws_recv(evt) {
starting_mod = config['start_mod']; starting_mod = config['start_mod'];
starting_offset_frequency = config['start_offset_freq']; starting_offset_frequency = config['start_offset_freq'];
bandwidth = config['samp_rate']; bandwidth = config['samp_rate'];
center_freq = config['center_freq'] + config['lfo_offset']; center_freq = config['center_freq'];
fft_size = config['fft_size']; fft_size = config['fft_size'];
fft_fps = config['fft_fps']; fft_fps = config['fft_fps'];
var audio_compression = config['audio_compression']; var audio_compression = config['audio_compression'];
@ -1049,6 +1057,9 @@ function on_ws_recv(evt) {
mathbox_waterfall_colors = config['mathbox_waterfall_colors']; mathbox_waterfall_colors = config['mathbox_waterfall_colors'];
mathbox_waterfall_frequency_resolution = config['mathbox_waterfall_frequency_resolution']; mathbox_waterfall_frequency_resolution = config['mathbox_waterfall_frequency_resolution'];
mathbox_waterfall_history_length = config['mathbox_waterfall_history_length']; mathbox_waterfall_history_length = config['mathbox_waterfall_history_length'];
var sql = Number.isInteger(config['initial_squelch_level']) ? config['initial_squelch_level'] : -150;
$("#openwebrx-panel-squelch").val(sql);
updateSquelch();
waterfall_init(); waterfall_init();
initialize_demodulator(); initialize_demodulator();
@ -1056,7 +1067,7 @@ function on_ws_recv(evt) {
waterfall_clear(); waterfall_clear();
currentprofile = config['profile_id']; currentprofile = config['sdr_id'] + '|' + config['profile_id'];
$('#openwebrx-sdr-profiles-listbox').val(currentprofile); $('#openwebrx-sdr-profiles-listbox').val(currentprofile);
break; break;
@ -1421,7 +1432,7 @@ function divlog(what, is_error) {
was_error |= is_error; was_error |= is_error;
if (is_error) { if (is_error) {
what = "<span class=\"webrx-error\">" + what + "</span>"; what = "<span class=\"webrx-error\">" + what + "</span>";
if (e("openwebrx-panel-log").openwebrxHidden) toggle_panel("openwebrx-panel-log"); //show panel if any error is present toggle_panel("openwebrx-panel-log", true); //show panel if any error is present
} }
e("openwebrx-debugdiv").innerHTML += what + "<br />"; e("openwebrx-debugdiv").innerHTML += what + "<br />";
var nano = $('.nano'); var nano = $('.nano');
@ -1473,9 +1484,7 @@ function onAudioStart(success, apiType){
//hide log panel in a second (if user has not hidden it yet) //hide log panel in a second (if user has not hidden it yet)
window.setTimeout(function () { window.setTimeout(function () {
if (typeof e("openwebrx-panel-log").openwebrxHidden === "undefined" && !was_error) { toggle_panel("openwebrx-panel-log", !!was_error);
toggle_panel("openwebrx-panel-log");
}
}, 2000); }, 2000);
//Synchronise volume with slider //Synchronise volume with slider
@ -1573,9 +1582,6 @@ function add_canvas() {
new_canvas.width = fft_size; new_canvas.width = fft_size;
new_canvas.height = canvas_default_height; new_canvas.height = canvas_default_height;
canvas_actual_line = canvas_default_height - 1; canvas_actual_line = canvas_default_height - 1;
new_canvas.style.width = (canvas_container.clientWidth * zoom_levels[zoom_level]).toString() + "px";
new_canvas.style.left = zoom_offset_px.toString() + "px";
new_canvas.style.height = canvas_default_height.toString() + "px";
new_canvas.openwebrx_top = (-canvas_default_height + 1); new_canvas.openwebrx_top = (-canvas_default_height + 1);
new_canvas.style.top = new_canvas.openwebrx_top.toString() + "px"; new_canvas.style.top = new_canvas.openwebrx_top.toString() + "px";
canvas_context = new_canvas.getContext("2d"); canvas_context = new_canvas.getContext("2d");
@ -1615,11 +1621,9 @@ function resize_canvases(zoom) {
if (typeof zoom === "undefined") zoom = false; if (typeof zoom === "undefined") zoom = false;
if (!zoom) mkzoomlevels(); if (!zoom) mkzoomlevels();
zoom_calc(); zoom_calc();
var new_width = (canvas_container.clientWidth * zoom_levels[zoom_level]).toString() + "px"; $('#webrx-canvas-container').css({
var zoom_value = zoom_offset_px.toString() + "px"; width: waterfallWidth() * zoom_levels[zoom_level] + 'px',
canvases.forEach(function (p) { left: zoom_offset_px + "px"
p.style.width = new_width;
p.style.left = zoom_value;
}); });
} }
@ -1917,10 +1921,10 @@ var audioEngine;
function openwebrx_init() { function openwebrx_init() {
audioEngine = new AudioEngine(audio_buffer_maximal_length_sec, audioReporter); audioEngine = new AudioEngine(audio_buffer_maximal_length_sec, audioReporter);
$overlay = $('#openwebrx-autoplay-overlay');
$overlay.on('click', playButtonClick);
if (!audioEngine.isAllowed()) { if (!audioEngine.isAllowed()) {
e("openwebrx-big-grey").style.display = "table-cell"; $overlay.show();
var opb = e("openwebrx-play-button-text");
opb.style.marginTop = (window.innerHeight / 2 - opb.clientHeight / 2).toString() + "px";
} else { } else {
audioEngine.start(onAudioStart); audioEngine.start(onAudioStart);
} }
@ -1972,10 +1976,11 @@ function update_dmr_timeslot_filtering() {
function playButtonClick() { function playButtonClick() {
//On iOS, we can only start audio from a click or touch event. //On iOS, we can only start audio from a click or touch event.
audioEngine.start(onAudioStart); audioEngine.start(onAudioStart);
e("openwebrx-big-grey").style.opacity = 0; var $overlay = $('#openwebrx-autoplay-overlay');
window.setTimeout(function () { $overlay.css('opacity', 0);
e("openwebrx-big-grey").style.display = "none"; $overlay.on('transitionend', function() {
}, 1100); $overlay.hide();
});
} }
var rt = function (s, n) { var rt = function (s, n) {
@ -2049,7 +2054,7 @@ function initPanels() {
el.style.transitionDuration = null; el.style.transitionDuration = null;
el.style.transitionDelay = null; el.style.transitionDelay = null;
el.style.transitionProperty = null; el.style.transitionProperty = null;
if (el.movement && el.movement == 'collapse') { if (el.movement && el.movement === 'collapse') {
el.style.display = 'none'; el.style.display = 'none';
} }
}); });
@ -2219,16 +2224,14 @@ function secondary_demod_push_data(x) {
var c = y.charCodeAt(0); var c = y.charCodeAt(0);
return (c === 10 || (c >= 32 && c <= 126)); return (c === 10 || (c >= 32 && c <= 126));
}).map(function (y) { }).map(function (y) {
if (y === "&" if (y === "&")
)
return "&amp;"; return "&amp;";
if (y === "<") return "&lt;"; if (y === "<") return "&lt;";
if (y === ">") return "&gt;"; if (y === ">") return "&gt;";
if (y === " ") return "&nbsp;"; if (y === " ") return "&nbsp;";
return y; return y;
}).map(function (y) { }).map(function (y) {
if (y === "\n" if (y === "\n")
)
return "<br />"; return "<br />";
return "<span class=\"part\">" + y + "</span>"; return "<span class=\"part\">" + y + "</span>";
}).join(""); }).join("");

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>
""" """
) )
@ -61,3 +63,4 @@ if __name__ == "__main__":
except KeyboardInterrupt: except KeyboardInterrupt:
WebSocketConnection.closeAll() WebSocketConnection.closeAll()
Services.stop() 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

@ -57,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",
@ -70,6 +69,8 @@ class OpenWebRxReceiverClient(Client):
"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):
@ -170,8 +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 that only works because setting the profile always causes plenty of config change # TODO this is a hack to support multiple sdrs
config["profile_id"] = self.sdr.getId() + "|" + self.sdr.getProfileId() config["sdr_id"] = self.sdr.getId()
self.write_config(config) self.write_config(config)
cf = configProps["center_freq"] cf = configProps["center_freq"]
@ -243,7 +244,7 @@ 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):
message = data.decode('ascii') message = data.decode("ascii")
self.send({"type": "secondary_demod", "value": message}) self.send({"type": "secondary_demod", "value": message})
def write_secondary_dsp_config(self, cfg): def write_secondary_dsp_config(self, cfg):

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,7 +1,7 @@
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
@ -110,11 +110,15 @@ 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:
@ -128,22 +132,25 @@ class ServiceScheduler(object):
if self.selectionTimer: if self.selectionTimer:
self.selectionTimer.cancel() self.selectionTimer.cancel()
def isActive(self): def getClientClass(self):
return self.active return SdrSource.CLIENT_BACKGROUND
def onSdrAvailable(self): def onStateChange(self, state):
pass if state == SdrSource.STATE_STOPPING:
self.scheduleSelection()
elif state == SdrSource.STATE_FAILED:
self.cancelTimer()
def onSdrUnavailable(self): def onBusyStateChange(self, state):
if state == SdrSource.BUSYSTATE_IDLE:
self.scheduleSelection()
def onFrequencyChange(self, name, value):
self.scheduleSelection() self.scheduleSelection()
def onSdrFailed(self):
self.cancelTimer()
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()
@ -159,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:
@ -177,32 +183,35 @@ 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 onSdrFailed(self):
logger.debug("sdr source failed; stopping services.")
self.stopServices()
def isSupported(self, mode): def isSupported(self, mode):
# TODO this should be in a more central place (the frontend also needs this) # TODO this should be in a more central place (the frontend also needs this)
requirements = { requirements = {
'ft8': 'wsjt-x', "ft8": "wsjt-x",
'ft4': 'wsjt-x', "ft4": "wsjt-x",
'jt65': 'wsjt-x', "jt65": "wsjt-x",
'jt9': 'wsjt-x', "jt9": "wsjt-x",
'wspr': 'wsjt-x', "wspr": "wsjt-x",
'packet': 'packet', "packet": "packet",
} }
fd = FeatureDetector() fd = FeatureDetector()
@ -214,6 +223,12 @@ class ServiceHandler(object):
return mode in available 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:
services = self.services services = self.services
@ -234,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()
@ -282,13 +289,15 @@ class ServiceHandler(object):
resampler_props["center_freq"] = cf resampler_props["center_freq"] = cf
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths # TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
resampler_props["samp_rate"] = bw + 24000 resampler_props["samp_rate"] = bw + 24000
resampler = Resampler(resampler_props, self.getAvailablePort(), self.source) resampler = Resampler(resampler_props, getAvailablePort(), self.source)
resampler.start() resampler.start()
self.services.append(resampler)
for dial in group: for dial in group:
self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler)) self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler))
# 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"])
distances = [ distances = [
@ -370,6 +379,7 @@ class AprsHandler(object):
class Services(object): class Services(object):
handlers = [] handlers = []
@staticmethod @staticmethod
def start(): def start():
if not PropertyManager.getSharedInstance()["services_enabled"]: if not PropertyManager.getSharedInstance()["services_enabled"]:
@ -380,7 +390,7 @@ class Services(object):
@staticmethod @staticmethod
def stop(): def stop():
for handler in Services.handlers: for handler in Services.handlers:
handler.stopServices() handler.shutdown()
Services.handlers = [] Services.handlers = []

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
@ -99,21 +100,28 @@ class SdrService(object):
class SdrSource(object): class SdrSource(object):
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): def __init__(self, id, props, port):
self.id = id self.id = id
self.props = props self.props = props
self.profile_id = None 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 = []
@ -122,6 +130,30 @@ class SdrSource(object):
self.process = None self.process = None
self.modificationLock = threading.Lock() self.modificationLock = threading.Lock()
self.failed = False 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):
@ -136,10 +168,11 @@ class SdrSource(object):
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: if profile_id == self.profile_id:
return; return
logger.debug("activating profile {0}".format(profile_id)) logger.debug("activating profile {0}".format(profile_id))
self.profile_id = 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":
@ -164,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:
@ -172,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)
@ -229,13 +271,14 @@ class SdrSource(object):
if not available: if not available:
self.failed = True self.failed = True
self.postStart()
self.modificationLock.release() self.modificationLock.release()
for c in self.clients: self.setState(SdrSource.STATE_FAILED if self.failed else SdrSource.STATE_RUNNING)
if self.failed:
c.onSdrFailed() def postStart(self):
else: pass
c.onSdrAvailable()
def isAvailable(self): def isAvailable(self):
return self.monitor is not None return self.monitor is not None
@ -244,8 +287,7 @@ class SdrSource(object):
return self.failed 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()
@ -260,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):
@ -299,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):
@ -321,6 +386,8 @@ class Resampler(SdrSource):
self.modificationLock.release() self.modificationLock.release()
return return
self.setState(SdrSource.STATE_STARTING)
props = self.rtlProps props = self.rtlProps
resampler_command = [ resampler_command = [
@ -380,19 +447,135 @@ class Resampler(SdrSource):
self.modificationLock.release() self.modificationLock.release()
for c in self.clients: self.setState(SdrSource.STATE_FAILED if self.failed else SdrSource.STATE_RUNNING)
if self.failed:
c.onSdrFailed()
else:
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"
@ -400,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"
@ -408,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)
@ -428,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}"
@ -509,17 +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
def onSdrFailed(self):
self.dsp.stop()
class DspManager(csdr.output): class DspManager(csdr.output):
@ -644,21 +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()
def onSdrFailed(self):
logger.debug("received onSdrFailed, shutting down DspSource")
self.dsp.stop()
self.handler.handleSdrFailure("sdr device failed")
class CpuUsageThread(threading.Thread): class CpuUsageThread(threading.Thread):

View File

@ -120,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):

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