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
|
**/*.pyc
|
||||||
*.swp
|
**/*.swp
|
||||||
tags
|
tags
|
||||||
.idea
|
.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)
|
- [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
|
||||||
|
|
||||||
|
@ -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
61
csdr.py
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
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
|
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
|
||||||
|
@ -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;*/
|
||||||
|
@ -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;">
|
||||||
|
@ -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({
|
||||||
|
@ -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 "&";
|
return "&";
|
||||||
if (y === "<") return "<";
|
if (y === "<") return "<";
|
||||||
if (y === ">") return ">";
|
if (y === ">") return ">";
|
||||||
if (y === " ") return " ";
|
if (y === " ") return " ";
|
||||||
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("");
|
||||||
|
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
28
owrx/map.py
28
owrx/map.py
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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]
|
||||||
|
@ -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
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.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):
|
||||||
|
@ -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):
|
||||||
|
33
owrx/wsjt.py
33
owrx/wsjt.py
@ -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:
|
||||||
|
8
push.sh
8
push.sh
@ -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
|
||||||
|
1
sdrhu.py
1
sdrhu.py
@ -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
|
||||||
|
Reference in New Issue
Block a user