Compare commits
47 Commits
image-2019
...
image-2019
Author | SHA1 | Date | |
---|---|---|---|
53de54120e | |||
fa097bf57e | |||
917eb4fdf1 | |||
a8df774e50 | |||
0b98ce1ef2 | |||
c6bbdffea0 | |||
481918ab5b | |||
b27caf2405 | |||
d5b7338531 | |||
9246500c95 | |||
91669a7fda | |||
c7eb67129a | |||
98901ac668 | |||
7dde793f9e | |||
07de82ae82 | |||
9f710cb70e | |||
dab62a04df | |||
de51e266f6 | |||
5375580104 | |||
964d9e873d | |||
7e8e644e6c | |||
6bde623698 | |||
5ba89035b4 | |||
a9b99fa0ff | |||
6619a1b4a6 | |||
a36f106c72 | |||
097f8a2b82 | |||
bcbb911b24 | |||
f18efb2344 | |||
497d98363f | |||
367bf666fc | |||
7489a3bb9d | |||
2a6c7863b1 | |||
bf27f51049 | |||
6ba74a0c30 | |||
ada94f69c3 | |||
dc5ac081ce | |||
8a46922e77 | |||
5fdffb5e0c | |||
9f6a4891ed | |||
41d23c66a4 | |||
9163f3d30e | |||
d49fff65e4 | |||
95253e40bd | |||
af1a99c130 | |||
1638fde181 | |||
52ea2e88e9 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
.git
|
||||
.gitignore
|
||||
.idea
|
||||
**/*.pyc
|
||||
**/*.swp
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
*.pyc
|
||||
*.swp
|
||||
**/*.pyc
|
||||
**/*.swp
|
||||
tags
|
||||
.idea
|
||||
|
34
README.md
34
README.md
@ -19,6 +19,24 @@ It has the following features:
|
||||
- [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)
|
||||
|
||||
**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)**
|
||||
- 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!
|
||||
@ -78,13 +96,13 @@ It has the following features:
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
@ -94,19 +112,23 @@ For those familiar with docker, I am providing [recent builds and Releases for b
|
||||
|
||||
### 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:
|
||||
|
||||
- [csdr](https://github.com/simonyiszk/csdr)
|
||||
- [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)
|
||||
- [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)
|
||||
|
||||
@ -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`.
|
||||
|
||||
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
|
||||
|
||||
|
@ -6,6 +6,7 @@ config_webrx: configuration options for OpenWebRX
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
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
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -66,8 +67,8 @@ server_hostname = "localhost"
|
||||
fft_fps = 9
|
||||
fft_size = 4096 # Should be power of 2
|
||||
fft_voverlap_factor = (
|
||||
0.3
|
||||
) # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
|
||||
0.3 # 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"
|
||||
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"
|
||||
#
|
||||
# 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 = {
|
||||
"rtlsdr": {
|
||||
"name": "RTL-SDR USB Stick",
|
||||
"type": "rtl_sdr",
|
||||
"type": "rtl_sdr_connector",
|
||||
"ppm": 0,
|
||||
# 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,
|
||||
"profiles": {
|
||||
"70cm": {
|
||||
@ -128,7 +142,7 @@ sdrs = {
|
||||
},
|
||||
"sdrplay": {
|
||||
"name": "SDRPlay RSP2",
|
||||
"type": "sdrplay",
|
||||
"type": "sdrplay_connector",
|
||||
"ppm": 0,
|
||||
"profiles": {
|
||||
"20m": {
|
||||
|
61
csdr.py
61
csdr.py
@ -4,6 +4,7 @@ OpenWebRX csdr plugin: do the signal processing with csdr
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
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
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -24,6 +25,7 @@ import subprocess
|
||||
import os
|
||||
import signal
|
||||
import threading
|
||||
import math
|
||||
from functools import partial
|
||||
|
||||
from owrx.kiss import KissClient, DirewolfConfig
|
||||
@ -84,7 +86,7 @@ class dsp(object):
|
||||
self.csdr_dynamic_bufsize = False
|
||||
self.csdr_print_bufsizes = False
|
||||
self.csdr_through = False
|
||||
self.squelch_level = 0
|
||||
self.squelch_level = -150
|
||||
self.fft_averages = 50
|
||||
self.iqtee = False
|
||||
self.iqtee2 = False
|
||||
@ -213,35 +215,35 @@ class dsp(object):
|
||||
return chain
|
||||
|
||||
def secondary_chain(self, which):
|
||||
secondary_chain_base = "cat {input_pipe} | "
|
||||
chain = ["cat {input_pipe}"]
|
||||
if which == "fft":
|
||||
return (
|
||||
secondary_chain_base
|
||||
+ "csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 "
|
||||
+ (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression == "adpcm" else "")
|
||||
)
|
||||
chain += [
|
||||
"csdr realpart_cf",
|
||||
"csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size}",
|
||||
"csdr logpower_cf -70",
|
||||
]
|
||||
if self.fft_compression == "adpcm":
|
||||
chain += ["csdr compress_fft_adpcm_f_u8 {secondary_fft_size}"]
|
||||
return chain
|
||||
elif which == "bpsk31":
|
||||
return (
|
||||
secondary_chain_base
|
||||
+ "csdr shift_addition_cc --fifo {secondary_shift_pipe} | "
|
||||
+ "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | "
|
||||
+ "csdr simple_agc_cc 0.001 0.5 | "
|
||||
+ "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 psk31_varicode_decoder_u8_u8"
|
||||
)
|
||||
return chain + [
|
||||
"csdr shift_addition_cc --fifo {secondary_shift_pipe}",
|
||||
"csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}",
|
||||
"csdr simple_agc_cc 0.001 0.5",
|
||||
"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 psk31_varicode_decoder_u8_u8",
|
||||
]
|
||||
elif self.isWsjtMode(which):
|
||||
chain = secondary_chain_base + "csdr realpart_cf | "
|
||||
chain += ["csdr realpart_cf"]
|
||||
if self.last_decimation != 1.0:
|
||||
chain += "csdr fractional_decimator_ff {last_decimation} | "
|
||||
chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
|
||||
return chain
|
||||
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
||||
return chain + ["csdr limit_ff", "csdr convert_f_s16"]
|
||||
elif which == "packet":
|
||||
chain = secondary_chain_base + "csdr fmdemod_quadri_cf | "
|
||||
chain += ["csdr fmdemod_quadri_cf"]
|
||||
if self.last_decimation != 1.0:
|
||||
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
|
||||
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
||||
return chain + ["csdr convert_f_s16", "direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h - 1>&2"]
|
||||
|
||||
def set_secondary_demodulator(self, what):
|
||||
if self.get_secondary_demodulator() == what:
|
||||
@ -281,7 +283,7 @@ class dsp(object):
|
||||
if not self.secondary_demodulator:
|
||||
return
|
||||
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_configs(secondary_command_demod)
|
||||
|
||||
@ -304,7 +306,7 @@ class dsp(object):
|
||||
if self.csdr_print_bufsizes:
|
||||
my_env["CSDR_PRINT_BUFSIZES"] = "1"
|
||||
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(
|
||||
input_pipe=self.iqtee_pipe,
|
||||
secondary_fft_input_size=self.secondary_fft_size,
|
||||
@ -520,13 +522,16 @@ class dsp(object):
|
||||
def get_bpf(self):
|
||||
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):
|
||||
self.squelch_level = squelch_level
|
||||
# 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:
|
||||
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.modification_lock.release()
|
||||
|
||||
|
@ -3,15 +3,22 @@ FROM $BASE_IMAGE
|
||||
|
||||
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/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
|
||||
|
||||
ENTRYPOINT [ "/openwebrx/docker/scripts/run.sh" ]
|
||||
ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ]
|
||||
EXPOSE 8073
|
||||
|
@ -9,3 +9,6 @@ RUN /install-dependencies-hackrf.sh
|
||||
RUN /install-dependencies-soapysdr.sh
|
||||
RUN /install-dependencies-sdrplay.sh
|
||||
RUN /install-dependencies-airspy.sh
|
||||
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
RUN /install-connectors.sh
|
||||
|
@ -4,3 +4,5 @@ FROM openwebrx-base:$ARCH
|
||||
ADD docker/scripts/install-dependencies-rtlsdr.sh /
|
||||
RUN /install-dependencies-rtlsdr.sh
|
||||
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
RUN /install-connectors.sh
|
||||
|
@ -5,3 +5,5 @@ ADD docker/scripts/install-dependencies-sdrplay.sh /
|
||||
ADD docker/scripts/install-lib.*.patch /
|
||||
RUN /install-dependencies-sdrplay.sh
|
||||
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
RUN /install-connectors.sh
|
||||
|
25
docker/scripts/install-connectors.sh
Executable file
25
docker/scripts/install-connectors.sh
Executable 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
|
@ -23,7 +23,7 @@ apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
git clone https://git.code.sf.net/p/itpp/git 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
|
||||
make
|
||||
make install
|
||||
@ -32,10 +32,6 @@ rm -rf csdr
|
||||
|
||||
git clone https://github.com/szechyjs/mbelib.git
|
||||
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
|
||||
cmakebuild digiham
|
||||
|
@ -3,6 +3,7 @@
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
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
|
||||
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-size: cover;
|
||||
background-color: #444;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container
|
||||
@ -258,16 +260,20 @@ input[type=range]:focus::-ms-fill-upper
|
||||
border-top-color: #0F0;
|
||||
}
|
||||
|
||||
#webrx-canvas-container
|
||||
{
|
||||
#webrx-canvas-background {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-image: url('../gfx/openwebrx-background-cool-blue.png');
|
||||
background-repeat: no-repeat;
|
||||
background-color: #1e5f7f;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
#webrx-canvas-container
|
||||
{
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: crosshair;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#webrx-canvas-container canvas
|
||||
@ -276,7 +282,8 @@ input[type=range]:focus::-ms-fill-upper
|
||||
border-style: none;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
/*transition: left 200ms, width 200ms;*/
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#openwebrx-mathbox-container
|
||||
@ -363,6 +370,8 @@ input[type=range]:focus::-ms-fill-upper
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#openwebrx-panels-container-left {
|
||||
@ -645,7 +654,7 @@ img.openwebrx-mirror-img
|
||||
font-family: 'expletus-sans-medium';
|
||||
}
|
||||
|
||||
#openwebrx-big-grey
|
||||
#openwebrx-autoplay-overlay
|
||||
{
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
@ -657,9 +666,6 @@ img.openwebrx-mirror-img
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1001;
|
||||
display: none;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 20pt;
|
||||
@ -667,11 +673,19 @@ img.openwebrx-mirror-img
|
||||
transition: opacity 0.3s linear;
|
||||
}
|
||||
|
||||
#openwebrx-big-grey img
|
||||
#openwebrx-autoplay-overlay img
|
||||
{
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#openwebrx-autoplay-overlay .overlay-content {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#openwebrx-digimode-canvas-container
|
||||
{
|
||||
/*margin: -10px -10px 10px -10px;*/
|
||||
|
@ -4,6 +4,7 @@
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
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
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -45,8 +46,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="openwebrx-mathbox-container"> </div>
|
||||
<div id="webrx-canvas-container">
|
||||
<!-- add canvas here by javascript -->
|
||||
<div id="webrx-canvas-background">
|
||||
<div id="webrx-canvas-container">
|
||||
<!-- add canvas here by javascript -->
|
||||
</div>
|
||||
</div>
|
||||
<div id="openwebrx-panels-container">
|
||||
<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-inner nano" id="openwebrx-log-scroll">
|
||||
<div class="nano-content">
|
||||
<div id="openwebrx-client-log-title">OpenWebRX client log</strong></div>
|
||||
<span id="openwebrx-client-1">Author: </span><a href="http://blog.sdr.hu/about" target="_blank">András Retzler, HA7ILM</a><br />You can support OpenWebRX development via <a href="http://blog.sdr.hu/support" target="_blank">PayPal!</a><br/>
|
||||
<div id="openwebrx-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>
|
||||
@ -214,10 +218,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="openwebrx-big-grey" onclick="playButtonClick();">
|
||||
<div id="openwebrx-play-button-text">
|
||||
<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" />
|
||||
<br /><br />Start OpenWebRX
|
||||
<div id="openwebrx-autoplay-overlay" style="display:none;">
|
||||
<div class="overlay-content">
|
||||
<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" />
|
||||
<div>Start OpenWebRX</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="openwebrx-dialog-bookmark" class="openwebrx-dialog" style="display:none;">
|
||||
|
@ -1,6 +1,6 @@
|
||||
function Measurement() {
|
||||
this.reset();
|
||||
};
|
||||
}
|
||||
|
||||
Measurement.prototype.add = function(v) {
|
||||
this.value += v;
|
||||
@ -25,7 +25,7 @@ Measurement.prototype.reset = function() {
|
||||
|
||||
Measurement.prototype.report = function(range, interval, callback) {
|
||||
return new Reporter(this, range, interval, callback);
|
||||
}
|
||||
};
|
||||
|
||||
function Reporter(measurement, range, interval, callback) {
|
||||
this.measurement = measurement;
|
||||
@ -33,7 +33,7 @@ function Reporter(measurement, range, interval, callback) {
|
||||
this.samples = [];
|
||||
this.callback = callback;
|
||||
this.interval = setInterval(this.report.bind(this), interval);
|
||||
};
|
||||
}
|
||||
|
||||
Reporter.prototype.sample = function(){
|
||||
this.samples.push({
|
||||
|
@ -3,6 +3,7 @@
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
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
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -140,9 +141,8 @@ function setSquelchToAuto() {
|
||||
}
|
||||
|
||||
function updateSquelch() {
|
||||
var sliderValue = parseInt(e("openwebrx-panel-squelch").value);
|
||||
var outputValue = (sliderValue === parseInt(e("openwebrx-panel-squelch").min)) ? 0 : getLinearSmeterValue(sliderValue);
|
||||
ws.send(JSON.stringify({"type": "dspcontrol", "params": {"squelch_level": outputValue}}));
|
||||
var sliderValue = parseInt($("#openwebrx-panel-squelch").val());
|
||||
ws.send(JSON.stringify({"type": "dspcontrol", "params": {"squelch_level": sliderValue}}));
|
||||
}
|
||||
|
||||
var waterfall_min_level;
|
||||
@ -196,7 +196,8 @@ function setSquelchSliderBackground(val) {
|
||||
var relative = (val - min) / (max - min);
|
||||
// use a brighter color when squelch is open
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
function waterfallWidth() {
|
||||
return $('body').width();
|
||||
}
|
||||
|
||||
|
||||
// ========================================================
|
||||
// =================== SCALE ROUTINES ===================
|
||||
@ -601,7 +606,7 @@ function scale_canvas_mousedown(evt) {
|
||||
|
||||
function scale_offset_freq_from_px(x, visible_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) {
|
||||
@ -642,20 +647,20 @@ function scale_canvas_mouseup(evt) {
|
||||
}
|
||||
|
||||
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() {
|
||||
var out = {};
|
||||
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);
|
||||
};
|
||||
out.start = fcalc(0);
|
||||
out.center = fcalc(canvas_container.clientWidth / 2);
|
||||
out.end = fcalc(canvas_container.clientWidth);
|
||||
out.center = fcalc(waterfallWidth() / 2);
|
||||
out.end = fcalc(waterfallWidth());
|
||||
out.bw = out.end - out.start;
|
||||
out.hps = out.bw / canvas_container.clientWidth;
|
||||
out.hps = out.bw / waterfallWidth();
|
||||
return out;
|
||||
}
|
||||
|
||||
@ -731,7 +736,7 @@ function get_scale_mark_spacing(range) {
|
||||
var out = {};
|
||||
var fcalc = function (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.small = out.large / out.ratio; //distance between small markers
|
||||
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 dpx = range.hps * deltaX;
|
||||
if (
|
||||
!(zoom_center_rel + dpx > (bandwidth / 2 - canvas_container.clientWidth * (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() * (1 - zoom_center_where) * range.hps)) &&
|
||||
!(zoom_center_rel + dpx < -bandwidth / 2 + waterfallWidth() * zoom_center_where * range.hps)
|
||||
) {
|
||||
zoom_center_rel += dpx;
|
||||
}
|
||||
@ -928,14 +933,18 @@ function canvas_end_drag() {
|
||||
}
|
||||
|
||||
function zoom_center_where_calc(screenposX) {
|
||||
//return (screenposX-(window.innerWidth-canvas_container.clientWidth))/canvas_container.clientWidth;
|
||||
return screenposX / canvas_container.clientWidth;
|
||||
return screenposX / waterfallWidth();
|
||||
}
|
||||
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -992,7 +1001,7 @@ function zoom_set(level) {
|
||||
if (!(level >= 0 && level <= zoom_levels.length - 1)) return;
|
||||
level = parseInt(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_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack
|
||||
resize_canvases(true);
|
||||
@ -1001,13 +1010,12 @@ function zoom_set(level) {
|
||||
}
|
||||
|
||||
function zoom_calc() {
|
||||
var winsize = canvas_container.clientWidth;
|
||||
var winsize = waterfallWidth();
|
||||
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));
|
||||
if (zoom_offset_px > 0) zoom_offset_px = 0;
|
||||
if (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;
|
||||
@ -1037,7 +1045,7 @@ function on_ws_recv(evt) {
|
||||
starting_mod = config['start_mod'];
|
||||
starting_offset_frequency = config['start_offset_freq'];
|
||||
bandwidth = config['samp_rate'];
|
||||
center_freq = config['center_freq'] + config['lfo_offset'];
|
||||
center_freq = config['center_freq'];
|
||||
fft_size = config['fft_size'];
|
||||
fft_fps = config['fft_fps'];
|
||||
var audio_compression = config['audio_compression'];
|
||||
@ -1049,6 +1057,9 @@ function on_ws_recv(evt) {
|
||||
mathbox_waterfall_colors = config['mathbox_waterfall_colors'];
|
||||
mathbox_waterfall_frequency_resolution = config['mathbox_waterfall_frequency_resolution'];
|
||||
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();
|
||||
initialize_demodulator();
|
||||
@ -1056,7 +1067,7 @@ function on_ws_recv(evt) {
|
||||
|
||||
waterfall_clear();
|
||||
|
||||
currentprofile = config['profile_id'];
|
||||
currentprofile = config['sdr_id'] + '|' + config['profile_id'];
|
||||
$('#openwebrx-sdr-profiles-listbox').val(currentprofile);
|
||||
|
||||
break;
|
||||
@ -1421,7 +1432,7 @@ function divlog(what, is_error) {
|
||||
was_error |= is_error;
|
||||
if (is_error) {
|
||||
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 />";
|
||||
var nano = $('.nano');
|
||||
@ -1473,9 +1484,7 @@ function onAudioStart(success, apiType){
|
||||
|
||||
//hide log panel in a second (if user has not hidden it yet)
|
||||
window.setTimeout(function () {
|
||||
if (typeof e("openwebrx-panel-log").openwebrxHidden === "undefined" && !was_error) {
|
||||
toggle_panel("openwebrx-panel-log");
|
||||
}
|
||||
toggle_panel("openwebrx-panel-log", !!was_error);
|
||||
}, 2000);
|
||||
|
||||
//Synchronise volume with slider
|
||||
@ -1573,9 +1582,6 @@ function add_canvas() {
|
||||
new_canvas.width = fft_size;
|
||||
new_canvas.height = canvas_default_height;
|
||||
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.style.top = new_canvas.openwebrx_top.toString() + "px";
|
||||
canvas_context = new_canvas.getContext("2d");
|
||||
@ -1615,11 +1621,9 @@ function resize_canvases(zoom) {
|
||||
if (typeof zoom === "undefined") zoom = false;
|
||||
if (!zoom) mkzoomlevels();
|
||||
zoom_calc();
|
||||
var new_width = (canvas_container.clientWidth * zoom_levels[zoom_level]).toString() + "px";
|
||||
var zoom_value = zoom_offset_px.toString() + "px";
|
||||
canvases.forEach(function (p) {
|
||||
p.style.width = new_width;
|
||||
p.style.left = zoom_value;
|
||||
$('#webrx-canvas-container').css({
|
||||
width: waterfallWidth() * zoom_levels[zoom_level] + 'px',
|
||||
left: zoom_offset_px + "px"
|
||||
});
|
||||
}
|
||||
|
||||
@ -1917,10 +1921,10 @@ var audioEngine;
|
||||
|
||||
function openwebrx_init() {
|
||||
audioEngine = new AudioEngine(audio_buffer_maximal_length_sec, audioReporter);
|
||||
$overlay = $('#openwebrx-autoplay-overlay');
|
||||
$overlay.on('click', playButtonClick);
|
||||
if (!audioEngine.isAllowed()) {
|
||||
e("openwebrx-big-grey").style.display = "table-cell";
|
||||
var opb = e("openwebrx-play-button-text");
|
||||
opb.style.marginTop = (window.innerHeight / 2 - opb.clientHeight / 2).toString() + "px";
|
||||
$overlay.show();
|
||||
} else {
|
||||
audioEngine.start(onAudioStart);
|
||||
}
|
||||
@ -1972,10 +1976,11 @@ function update_dmr_timeslot_filtering() {
|
||||
function playButtonClick() {
|
||||
//On iOS, we can only start audio from a click or touch event.
|
||||
audioEngine.start(onAudioStart);
|
||||
e("openwebrx-big-grey").style.opacity = 0;
|
||||
window.setTimeout(function () {
|
||||
e("openwebrx-big-grey").style.display = "none";
|
||||
}, 1100);
|
||||
var $overlay = $('#openwebrx-autoplay-overlay');
|
||||
$overlay.css('opacity', 0);
|
||||
$overlay.on('transitionend', function() {
|
||||
$overlay.hide();
|
||||
});
|
||||
}
|
||||
|
||||
var rt = function (s, n) {
|
||||
@ -2049,7 +2054,7 @@ function initPanels() {
|
||||
el.style.transitionDuration = null;
|
||||
el.style.transitionDelay = null;
|
||||
el.style.transitionProperty = null;
|
||||
if (el.movement && el.movement == 'collapse') {
|
||||
if (el.movement && el.movement === 'collapse') {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
@ -2219,16 +2224,14 @@ function secondary_demod_push_data(x) {
|
||||
var c = y.charCodeAt(0);
|
||||
return (c === 10 || (c >= 32 && c <= 126));
|
||||
}).map(function (y) {
|
||||
if (y === "&"
|
||||
)
|
||||
if (y === "&")
|
||||
return "&";
|
||||
if (y === "<") return "<";
|
||||
if (y === ">") return ">";
|
||||
if (y === " ") return " ";
|
||||
return y;
|
||||
}).map(function (y) {
|
||||
if (y === "\n"
|
||||
)
|
||||
if (y === "\n")
|
||||
return "<br />";
|
||||
return "<span class=\"part\">" + y + "</span>";
|
||||
}).join("");
|
||||
|
@ -9,6 +9,7 @@ from socketserver import ThreadingMixIn
|
||||
from owrx.sdrhu import SdrHuUpdater
|
||||
from owrx.service import Services
|
||||
from owrx.websocket import WebSocketConnection
|
||||
from owrx.pskreporter import PskReporter
|
||||
|
||||
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: Jakob Ketterl, DD5JFK <dd5jfk@darc.de>
|
||||
|
||||
"""
|
||||
)
|
||||
@ -61,3 +63,4 @@ if __name__ == "__main__":
|
||||
except KeyboardInterrupt:
|
||||
WebSocketConnection.closeAll()
|
||||
Services.stop()
|
||||
PskReporter.stop()
|
||||
|
@ -26,6 +26,7 @@ class Bookmark(object):
|
||||
|
||||
class Bookmarks(object):
|
||||
sharedInstance = None
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
if Bookmarks.sharedInstance is None:
|
||||
|
@ -57,7 +57,6 @@ class OpenWebRxReceiverClient(Client):
|
||||
"waterfall_min_level",
|
||||
"waterfall_max_level",
|
||||
"waterfall_auto_level_margin",
|
||||
"lfo_offset",
|
||||
"samp_rate",
|
||||
"fft_size",
|
||||
"fft_fps",
|
||||
@ -70,6 +69,8 @@ class OpenWebRxReceiverClient(Client):
|
||||
"mathbox_waterfall_colors",
|
||||
"mathbox_waterfall_history_length",
|
||||
"mathbox_waterfall_frequency_resolution",
|
||||
"initial_squelch_level",
|
||||
"profile_id",
|
||||
]
|
||||
|
||||
def __init__(self, conn):
|
||||
@ -170,8 +171,8 @@ class OpenWebRxReceiverClient(Client):
|
||||
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
|
||||
# TODO mathematical properties? hmmmm
|
||||
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
|
||||
# TODO this is a hack that only works because setting the profile always causes plenty of config change
|
||||
config["profile_id"] = self.sdr.getId() + "|" + self.sdr.getProfileId()
|
||||
# TODO this is a hack to support multiple sdrs
|
||||
config["sdr_id"] = self.sdr.getId()
|
||||
self.write_config(config)
|
||||
|
||||
cf = configProps["center_freq"]
|
||||
@ -243,7 +244,7 @@ class OpenWebRxReceiverClient(Client):
|
||||
self.send(bytes([0x03]) + data)
|
||||
|
||||
def write_secondary_demod(self, data):
|
||||
message = data.decode('ascii')
|
||||
message = data.decode("ascii")
|
||||
self.send({"type": "secondary_demod", "value": message})
|
||||
|
||||
def write_secondary_dsp_config(self, cfg):
|
||||
|
@ -1,10 +1,11 @@
|
||||
import os
|
||||
import subprocess
|
||||
from functools import reduce
|
||||
from operator import and_
|
||||
import re
|
||||
from distutils.version import LooseVersion
|
||||
import inspect
|
||||
from owrx.config import PropertyManager
|
||||
import shlex
|
||||
|
||||
import logging
|
||||
|
||||
@ -19,9 +20,12 @@ class FeatureDetector(object):
|
||||
features = {
|
||||
"core": ["csdr", "nmux", "nc"],
|
||||
"rtl_sdr": ["rtl_sdr"],
|
||||
"rtl_sdr_connector": ["rtl_connector"],
|
||||
"sdrplay": ["rx_tools"],
|
||||
"sdrplay_connector": ["soapy_connector"],
|
||||
"hackrf": ["hackrf_transfer"],
|
||||
"airspy": ["airspy_rx"],
|
||||
"airspy_connector": ["soapy_connector"],
|
||||
"digital_voice_digiham": ["digiham", "sox"],
|
||||
"digital_voice_dsd": ["dsd", "sox", "digiham"],
|
||||
"wsjt-x": ["wsjtx", "sox"],
|
||||
@ -83,7 +87,13 @@ class FeatureDetector(object):
|
||||
return inspect.getdoc(self._get_requirement_method(requirement))
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -144,9 +154,6 @@ class FeatureDetector(object):
|
||||
# TODO also check if it has the stdout feature
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
try:
|
||||
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)
|
||||
return version >= required_version
|
||||
except FileNotFoundError:
|
||||
@ -185,6 +195,40 @@ class FeatureDetector(object):
|
||||
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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
return self.command_is_runnable("airspy_rx --help 2> /dev/null")
|
||||
return self.command_is_runnable("airspy_rx --help")
|
||||
|
||||
def has_wsjtx(self):
|
||||
"""
|
||||
|
28
owrx/map.py
28
owrx/map.py
@ -25,13 +25,23 @@ class Map(object):
|
||||
def __init__(self):
|
||||
self.clients = []
|
||||
self.positions = {}
|
||||
self.positionsLock = threading.Lock()
|
||||
|
||||
def removeLoop():
|
||||
loops = 0
|
||||
while True:
|
||||
try:
|
||||
self.removeOldPositions()
|
||||
except Exception:
|
||||
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)
|
||||
|
||||
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):
|
||||
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(
|
||||
[
|
||||
{
|
||||
@ -80,13 +91,15 @@ class Map(object):
|
||||
def touchLocation(self, callsign):
|
||||
# not implemented on the client side yet, so do not use!
|
||||
ts = datetime.now()
|
||||
if callsign in self.positions:
|
||||
self.positions[callsign]["updated"] = ts
|
||||
with self.positionsLock:
|
||||
if callsign in self.positions:
|
||||
self.positions[callsign]["updated"] = ts
|
||||
self.broadcast([{"callsign": callsign, "lastseen": ts.timestamp() * 1000}])
|
||||
|
||||
def removeLocation(self, callsign):
|
||||
self.positions.pop(callsign, None)
|
||||
# TODO broadcast removal to clients
|
||||
with self.positionsLock:
|
||||
del self.positions[callsign]
|
||||
# TODO broadcast removal to clients
|
||||
|
||||
def removeOldPositions(self):
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
@ -97,6 +110,11 @@ class Map(object):
|
||||
for callsign in to_be_removed:
|
||||
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):
|
||||
def __init__(self, lat: float, lon: float):
|
||||
|
@ -62,7 +62,7 @@ class DmrMetaEnricher(object):
|
||||
cache = DmrCache.getSharedInstance()
|
||||
if not cache.isValid(id):
|
||||
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()
|
||||
return None
|
||||
data = cache.get(id)
|
||||
|
@ -3,10 +3,12 @@ import threading
|
||||
import time
|
||||
import random
|
||||
import socket
|
||||
from sched import scheduler
|
||||
from functools import reduce
|
||||
from operator import and_
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.version import openwebrx_version
|
||||
from owrx.locator import Locator
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -20,6 +22,9 @@ class PskReporterDummy(object):
|
||||
def spot(self, spot):
|
||||
pass
|
||||
|
||||
def cancelTimer(self):
|
||||
pass
|
||||
|
||||
|
||||
class PskReporter(object):
|
||||
sharedInstance = None
|
||||
@ -37,24 +42,46 @@ class PskReporter(object):
|
||||
PskReporter.sharedInstance = PskReporterDummy()
|
||||
return PskReporter.sharedInstance
|
||||
|
||||
@staticmethod
|
||||
def stop():
|
||||
if PskReporter.sharedInstance:
|
||||
PskReporter.sharedInstance.cancelTimer()
|
||||
|
||||
def __init__(self):
|
||||
self.spots = []
|
||||
self.spotLock = threading.Lock()
|
||||
self.uploader = Uploader()
|
||||
self.scheduler = scheduler(time.time, time.sleep)
|
||||
self.scheduleNextUpload()
|
||||
threading.Thread(target=self.scheduler.run).start()
|
||||
self.timer = None
|
||||
metrics = Metrics.getSharedInstance()
|
||||
self.dupeCounter = CounterMetric()
|
||||
metrics.addMetric("pskreporter.duplicates", self.dupeCounter)
|
||||
self.spotCounter = CounterMetric()
|
||||
metrics.addMetric("pskreporter.spots", self.spotCounter)
|
||||
|
||||
def scheduleNextUpload(self):
|
||||
if self.timer:
|
||||
return
|
||||
delay = PskReporter.interval + random.uniform(0, 30)
|
||||
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):
|
||||
if not spot["mode"] in PskReporter.supportedModes:
|
||||
return
|
||||
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):
|
||||
try:
|
||||
@ -67,8 +94,13 @@ class PskReporter(object):
|
||||
except Exception:
|
||||
logger.exception("Failed to upload spots")
|
||||
|
||||
self.timer = None
|
||||
self.scheduleNextUpload()
|
||||
|
||||
def cancelTimer(self):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
|
||||
|
||||
class Uploader(object):
|
||||
receieverDelimiter = [0x99, 0x92]
|
||||
|
@ -1,7 +1,7 @@
|
||||
import threading
|
||||
import socket
|
||||
from owrx.socket import getAvailablePort
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from owrx.source import SdrService
|
||||
from owrx.source import SdrService, SdrSource
|
||||
from owrx.bands import Bandplan
|
||||
from csdr import dsp, output
|
||||
from owrx.wsjt import WsjtParser
|
||||
@ -110,11 +110,15 @@ class ServiceScheduler(object):
|
||||
def __init__(self, source, schedule):
|
||||
self.source = source
|
||||
self.schedule = Schedule.parse(schedule)
|
||||
self.active = False
|
||||
self.source.addClient(self)
|
||||
self.selectionTimer = None
|
||||
self.source.getProps().collect("center_freq", "samp_rate").wire(self.onFrequencyChange)
|
||||
self.scheduleSelection()
|
||||
|
||||
def shutdown(self):
|
||||
self.cancelTimer()
|
||||
self.source.removeClient(self)
|
||||
|
||||
def scheduleSelection(self, time=None):
|
||||
seconds = 10
|
||||
if time is not None:
|
||||
@ -128,22 +132,25 @@ class ServiceScheduler(object):
|
||||
if self.selectionTimer:
|
||||
self.selectionTimer.cancel()
|
||||
|
||||
def isActive(self):
|
||||
return self.active
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_BACKGROUND
|
||||
|
||||
def onSdrAvailable(self):
|
||||
pass
|
||||
def onStateChange(self, state):
|
||||
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()
|
||||
|
||||
def onSdrFailed(self):
|
||||
self.cancelTimer()
|
||||
|
||||
def selectProfile(self):
|
||||
self.active = False
|
||||
if self.source.hasActiveClients():
|
||||
logger.debug("source has active clients; not touching")
|
||||
if self.source.hasClients(SdrSource.CLIENT_USER):
|
||||
logger.debug("source has active users; not touching")
|
||||
return
|
||||
logger.debug("source seems to be idle, selecting profile for background services")
|
||||
entry = self.schedule.getCurrentEntry()
|
||||
@ -159,7 +166,6 @@ class ServiceScheduler(object):
|
||||
self.scheduleSelection(entry.getScheduledEnd())
|
||||
|
||||
try:
|
||||
self.active = True
|
||||
self.source.activateProfile(entry.getProfile())
|
||||
self.source.start()
|
||||
except KeyError:
|
||||
@ -177,32 +183,35 @@ class ServiceHandler(object):
|
||||
props.collect("center_freq", "samp_rate").wire(self.onFrequencyChange)
|
||||
if self.source.isAvailable():
|
||||
self.scheduleServiceStartup()
|
||||
self.scheduler = None
|
||||
if "schedule" in props:
|
||||
ServiceScheduler(self.source, props["schedule"])
|
||||
self.scheduler = ServiceScheduler(self.source, props["schedule"])
|
||||
|
||||
def isActive(self):
|
||||
return False
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_INACTIVE
|
||||
|
||||
def onSdrAvailable(self):
|
||||
self.scheduleServiceStartup()
|
||||
def onStateChange(self, state):
|
||||
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):
|
||||
logger.debug("sdr source becoming unavailable; stopping services.")
|
||||
self.stopServices()
|
||||
|
||||
def onSdrFailed(self):
|
||||
logger.debug("sdr source failed; stopping services.")
|
||||
self.stopServices()
|
||||
def onBusyStateChange(self, state):
|
||||
pass
|
||||
|
||||
def isSupported(self, mode):
|
||||
# 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',
|
||||
"ft8": "wsjt-x",
|
||||
"ft4": "wsjt-x",
|
||||
"jt65": "wsjt-x",
|
||||
"jt9": "wsjt-x",
|
||||
"wspr": "wsjt-x",
|
||||
"packet": "packet",
|
||||
}
|
||||
fd = FeatureDetector()
|
||||
|
||||
@ -214,6 +223,12 @@ class ServiceHandler(object):
|
||||
|
||||
return mode in available
|
||||
|
||||
def shutdown(self):
|
||||
self.stopServices()
|
||||
self.source.removeClient(self)
|
||||
if self.scheduler:
|
||||
self.scheduler.shutdown()
|
||||
|
||||
def stopServices(self):
|
||||
with self.lock:
|
||||
services = self.services
|
||||
@ -234,14 +249,6 @@ class ServiceHandler(object):
|
||||
self.startupTimer = threading.Timer(10, self.updateServices)
|
||||
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):
|
||||
logger.debug("re-scheduling services due to sdr changes")
|
||||
self.stopServices()
|
||||
@ -282,13 +289,15 @@ class ServiceHandler(object):
|
||||
resampler_props["center_freq"] = cf
|
||||
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
|
||||
resampler_props["samp_rate"] = bw + 24000
|
||||
resampler = Resampler(resampler_props, self.getAvailablePort(), self.source)
|
||||
resampler = Resampler(resampler_props, getAvailablePort(), self.source)
|
||||
resampler.start()
|
||||
self.services.append(resampler)
|
||||
|
||||
for dial in group:
|
||||
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):
|
||||
freqs = sorted(freqs, key=lambda f: f["frequency"])
|
||||
distances = [
|
||||
@ -370,6 +379,7 @@ class AprsHandler(object):
|
||||
|
||||
class Services(object):
|
||||
handlers = []
|
||||
|
||||
@staticmethod
|
||||
def start():
|
||||
if not PropertyManager.getSharedInstance()["services_enabled"]:
|
||||
@ -380,7 +390,7 @@ class Services(object):
|
||||
@staticmethod
|
||||
def stop():
|
||||
for handler in Services.handlers:
|
||||
handler.stopServices()
|
||||
handler.shutdown()
|
||||
Services.handlers = []
|
||||
|
||||
|
||||
|
10
owrx/socket.py
Normal file
10
owrx/socket.py
Normal 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
|
335
owrx/source.py
335
owrx/source.py
@ -5,6 +5,7 @@ from owrx.meta import MetaParser
|
||||
from owrx.wsjt import WsjtParser
|
||||
from owrx.aprs import AprsParser
|
||||
from owrx.metrics import Metrics, DirectMetric
|
||||
from owrx.socket import getAvailablePort
|
||||
import threading
|
||||
import csdr
|
||||
import time
|
||||
@ -99,21 +100,28 @@ class SdrService(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):
|
||||
self.id = id
|
||||
self.props = props
|
||||
self.profile_id = None
|
||||
self.activateProfile()
|
||||
self.rtlProps = self.props.collect(
|
||||
"samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain"
|
||||
).defaults(PropertyManager.getSharedInstance())
|
||||
self.rtlProps = self.props.collect(*self.getEventNames()).defaults(PropertyManager.getSharedInstance())
|
||||
self.wireEvents()
|
||||
|
||||
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.monitor = None
|
||||
self.clients = []
|
||||
@ -122,6 +130,30 @@ class SdrSource(object):
|
||||
self.process = None
|
||||
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
|
||||
def getCommand(self):
|
||||
@ -136,10 +168,11 @@ class SdrSource(object):
|
||||
if profile_id is None:
|
||||
profile_id = list(profiles.keys())[0]
|
||||
if profile_id == self.profile_id:
|
||||
return;
|
||||
return
|
||||
logger.debug("activating profile {0}".format(profile_id))
|
||||
self.profile_id = profile_id
|
||||
profile = profiles[profile_id]
|
||||
self.props["profile_id"] = profile_id
|
||||
for (key, value) in profile.items():
|
||||
# skip the name, that would overwrite the source name.
|
||||
if key == "name":
|
||||
@ -164,6 +197,17 @@ class SdrSource(object):
|
||||
def getPort(self):
|
||||
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):
|
||||
self.modificationLock.acquire()
|
||||
if self.monitor:
|
||||
@ -172,33 +216,31 @@ class SdrSource(object):
|
||||
|
||||
props = self.rtlProps
|
||||
|
||||
start_sdr_command = self.getCommand().format(
|
||||
**props.collect(
|
||||
"samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain"
|
||||
).__dict__()
|
||||
)
|
||||
cmd = self.getCommand().format(**self.getCommandValues())
|
||||
|
||||
format_conversion = self.getFormatConversion()
|
||||
if format_conversion is not None:
|
||||
start_sdr_command += " | " + format_conversion
|
||||
cmd += " | " + format_conversion
|
||||
|
||||
nmux_bufcnt = nmux_bufsize = 0
|
||||
while nmux_bufsize < props["samp_rate"] / 4:
|
||||
nmux_bufsize += 4096
|
||||
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6:
|
||||
nmux_bufcnt += 1
|
||||
if nmux_bufcnt == 0 or nmux_bufsize == 0:
|
||||
logger.error(
|
||||
"Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
|
||||
if self.useNmux():
|
||||
nmux_bufcnt = nmux_bufsize = 0
|
||||
while nmux_bufsize < props["samp_rate"] / 4:
|
||||
nmux_bufsize += 4096
|
||||
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6:
|
||||
nmux_bufcnt += 1
|
||||
if nmux_bufcnt == 0 or nmux_bufsize == 0:
|
||||
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)
|
||||
logger.info("Started rtl source: " + cmd)
|
||||
|
||||
@ -229,13 +271,14 @@ class SdrSource(object):
|
||||
if not available:
|
||||
self.failed = True
|
||||
|
||||
self.postStart()
|
||||
|
||||
self.modificationLock.release()
|
||||
|
||||
for c in self.clients:
|
||||
if self.failed:
|
||||
c.onSdrFailed()
|
||||
else:
|
||||
c.onSdrAvailable()
|
||||
self.setState(SdrSource.STATE_FAILED if self.failed else SdrSource.STATE_RUNNING)
|
||||
|
||||
def postStart(self):
|
||||
pass
|
||||
|
||||
def isAvailable(self):
|
||||
return self.monitor is not None
|
||||
@ -244,8 +287,7 @@ class SdrSource(object):
|
||||
return self.failed
|
||||
|
||||
def stop(self):
|
||||
for c in self.clients:
|
||||
c.onSdrUnavailable()
|
||||
self.setState(SdrSource.STATE_STOPPING)
|
||||
|
||||
self.modificationLock.acquire()
|
||||
|
||||
@ -260,24 +302,33 @@ class SdrSource(object):
|
||||
self.sleepOnRestart()
|
||||
self.modificationLock.release()
|
||||
|
||||
self.setState(SdrSource.STATE_STOPPED)
|
||||
|
||||
def sleepOnRestart(self):
|
||||
pass
|
||||
|
||||
def hasActiveClients(self):
|
||||
activeClients = [c for c in self.clients if c.isActive()]
|
||||
return len(activeClients) > 0
|
||||
def hasClients(self, *args):
|
||||
clients = [c for c in self.clients if c.getClientClass() in args]
|
||||
return len(clients) > 0
|
||||
|
||||
def addClient(self, 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.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE)
|
||||
|
||||
def removeClient(self, c):
|
||||
try:
|
||||
self.clients.remove(c)
|
||||
except ValueError:
|
||||
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()
|
||||
|
||||
def addSpectrumClient(self, c):
|
||||
@ -299,6 +350,20 @@ class SdrSource(object):
|
||||
for c in self.spectrumClients:
|
||||
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):
|
||||
def __init__(self, props, port, sdr):
|
||||
@ -321,6 +386,8 @@ class Resampler(SdrSource):
|
||||
self.modificationLock.release()
|
||||
return
|
||||
|
||||
self.setState(SdrSource.STATE_STARTING)
|
||||
|
||||
props = self.rtlProps
|
||||
|
||||
resampler_command = [
|
||||
@ -380,19 +447,135 @@ class Resampler(SdrSource):
|
||||
|
||||
self.modificationLock.release()
|
||||
|
||||
for c in self.clients:
|
||||
if self.failed:
|
||||
c.onSdrFailed()
|
||||
else:
|
||||
c.onSdrAvailable()
|
||||
self.setState(SdrSource.STATE_FAILED if self.failed else SdrSource.STATE_RUNNING)
|
||||
|
||||
def activateProfile(self, profile_id=None):
|
||||
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):
|
||||
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):
|
||||
return "csdr convert_u8_f"
|
||||
@ -400,7 +583,7 @@ class RtlSdrSource(SdrSource):
|
||||
|
||||
class HackrfSource(SdrSource):
|
||||
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):
|
||||
return "csdr convert_s8_f"
|
||||
@ -408,7 +591,7 @@ class HackrfSource(SdrSource):
|
||||
|
||||
class SdrplaySource(SdrSource):
|
||||
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"}
|
||||
gains = [
|
||||
"{0}={{{1}}}".format(gainMap[name], name)
|
||||
@ -428,7 +611,7 @@ class SdrplaySource(SdrSource):
|
||||
|
||||
class AirspySource(SdrSource):
|
||||
def getCommand(self):
|
||||
frequency = self.props["center_freq"] / 1e6
|
||||
frequency = self.props["tuner_freq"] / 1e6
|
||||
command = "airspy_rx"
|
||||
command += " -f{0}".format(frequency)
|
||||
command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}"
|
||||
@ -509,17 +692,17 @@ class SpectrumThread(csdr.output):
|
||||
c.cancel()
|
||||
self.subscriptions = []
|
||||
|
||||
def isActive(self):
|
||||
return True
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_USER
|
||||
|
||||
def onSdrAvailable(self):
|
||||
self.dsp.start()
|
||||
def onStateChange(self, state):
|
||||
if state in [SdrSource.STATE_STOPPING, SdrSource.STATE_FAILED]:
|
||||
self.dsp.stop()
|
||||
elif state == SdrSource.STATE_RUNNING:
|
||||
self.dsp.start()
|
||||
|
||||
def onSdrUnavailable(self):
|
||||
self.dsp.stop()
|
||||
|
||||
def onSdrFailed(self):
|
||||
self.dsp.stop()
|
||||
def onBusyStateChange(self, state):
|
||||
pass
|
||||
|
||||
|
||||
class DspManager(csdr.output):
|
||||
@ -644,21 +827,23 @@ class DspManager(csdr.output):
|
||||
def setProperty(self, prop, value):
|
||||
self.localProps.getProperty(prop).setValue(value)
|
||||
|
||||
def isActive(self):
|
||||
return True
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_USER
|
||||
|
||||
def onSdrAvailable(self):
|
||||
logger.debug("received onSdrAvailable, attempting DspSource restart")
|
||||
self.dsp.start()
|
||||
def onStateChange(self, state):
|
||||
if state == SdrSource.STATE_RUNNING:
|
||||
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):
|
||||
logger.debug("received onSdrUnavailable, shutting down DspSource")
|
||||
self.dsp.stop()
|
||||
|
||||
def onSdrFailed(self):
|
||||
logger.debug("received onSdrFailed, shutting down DspSource")
|
||||
self.dsp.stop()
|
||||
self.handler.handleSdrFailure("sdr device failed")
|
||||
def onBusyStateChange(self, state):
|
||||
pass
|
||||
|
||||
|
||||
class CpuUsageThread(threading.Thread):
|
||||
|
@ -120,7 +120,6 @@ class WebSocketConnection(object):
|
||||
self._sendBytes(data_to_send)
|
||||
|
||||
def _sendBytes(self, data_to_send):
|
||||
|
||||
def chunks(l, n):
|
||||
"""Yield successive n-sized chunks from l."""
|
||||
for i in range(0, len(l), n):
|
||||
|
33
owrx/wsjt.py
33
owrx/wsjt.py
@ -1,8 +1,6 @@
|
||||
import threading
|
||||
import wave
|
||||
from datetime import datetime, timedelta, date, timezone
|
||||
import time
|
||||
import sched
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import subprocess
|
||||
import os
|
||||
from multiprocessing.connection import Pipe
|
||||
@ -93,8 +91,7 @@ class WsjtChopper(threading.Thread):
|
||||
self.tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"]
|
||||
(self.wavefilename, self.wavefile) = self.getWaveFile()
|
||||
self.switchingLock = threading.Lock()
|
||||
self.scheduler = sched.scheduler(time.time, time.sleep)
|
||||
self.schedulerLock = threading.Lock()
|
||||
self.timer = None
|
||||
(self.outputReader, self.outputWriter) = Pipe()
|
||||
self.doRun = True
|
||||
super().__init__()
|
||||
@ -110,27 +107,23 @@ class WsjtChopper(threading.Thread):
|
||||
return filename, wavefile
|
||||
|
||||
def getNextDecodingTime(self):
|
||||
t = datetime.now()
|
||||
t = datetime.utcnow()
|
||||
zeroed = t.replace(minute=0, second=0, microsecond=0)
|
||||
delta = t - zeroed
|
||||
seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval
|
||||
t = zeroed + timedelta(seconds=seconds)
|
||||
logger.debug("scheduling: {0}".format(t))
|
||||
return t.timestamp()
|
||||
return t
|
||||
|
||||
def startScheduler(self):
|
||||
self._scheduleNextSwitch()
|
||||
threading.Thread(target=self.scheduler.run).start()
|
||||
|
||||
def emptyScheduler(self):
|
||||
with self.schedulerLock:
|
||||
for event in self.scheduler.queue:
|
||||
self.scheduler.cancel(event)
|
||||
def cancelTimer(self):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
|
||||
def _scheduleNextSwitch(self):
|
||||
with self.schedulerLock:
|
||||
if self.doRun:
|
||||
self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles)
|
||||
if self.doRun:
|
||||
delta = self.getNextDecodingTime() - datetime.utcnow()
|
||||
self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
|
||||
self.timer.start()
|
||||
|
||||
def switchFiles(self):
|
||||
self.switchingLock.acquire()
|
||||
@ -169,7 +162,7 @@ class WsjtChopper(threading.Thread):
|
||||
|
||||
def run(self) -> None:
|
||||
logger.debug("WSJT chopper starting up")
|
||||
self.startScheduler()
|
||||
self._scheduleNextSwitch()
|
||||
while self.doRun:
|
||||
data = self.source.read(256)
|
||||
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")
|
||||
self.outputReader.close()
|
||||
self.outputWriter.close()
|
||||
self.emptyScheduler()
|
||||
self.cancelTimer()
|
||||
try:
|
||||
os.unlink(self.wavefilename)
|
||||
except Exception:
|
||||
|
8
push.sh
8
push.sh
@ -3,6 +3,14 @@ set -euxo pipefail
|
||||
|
||||
ARCH=$(uname -m)
|
||||
|
||||
ALL_ARCHS="x86_64 armv7l"
|
||||
|
||||
for image in openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-full openwebrx; do
|
||||
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
|
||||
|
1
sdrhu.py
1
sdrhu.py
@ -4,6 +4,7 @@
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
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
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
|
Reference in New Issue
Block a user