diff --git a/README.md b/README.md
index d308c36..68178af 100644
--- a/README.md
+++ b/README.md
@@ -9,38 +9,42 @@ OpenWebRX is a multi-user SDR receiver software with a web interface.
It has the following features:
-- csdr based demodulators (AM/FM/SSB/CW/BPSK31),
+- [csdr](https://github.com/simonyiszk/csdr) based demodulators (AM/FM/SSB/CW/BPSK31),
- filter passband can be set from GUI,
-- waterfall display can be shifted back in time,
-- it extensively uses HTML5 features like WebSocket, Web Audio API, and <canvas>,
-- it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28),
-- currently supports RTL-SDR, HackRF, SDRplay, AirSpy and many other devices, see the OpenWebRX Wiki ,
-- it has a 3D waterfall display:
+- it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas
+- it works in Google Chrome, Chromium and Mozilla Firefox
+- currently supports RTL-SDR, HackRF, SDRplay, AirSpy
+- Multiple SDR devices can be used simultaneously
+- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF)
+- [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)
-![OpenWebRX 3D waterfall](http://blog.sdr.hu/images/openwebrx/screenshot-3d.gif)
+**News (2019-07-21 by DD5JFK)**
+- Latest Features:
+ - More WSJT-X modes have been added, including the new FT4 mode
+ - I started adding a bandplan feature, the first thing visible is the "dial" indicator that brings you right to the dial frequency for digital modes
+ - fixed some bugs in the websocket communication which broke the map
-**News (2015-08-18)**
-- My BSc. thesis written on OpenWebRX is available here.
-- Several bugs were fixed to improve reliability and stability.
-- OpenWebRX now supports compression of audio and waterfall stream, so the required network uplink bandwidth has been decreased from 2 Mbit/s to about 200 kbit/s per client! (Measured with the default settings. It is also dependent on `fft_size`.)
-- OpenWebRX now uses sdr.js (*libcsdr* compiled to JavaScript) for some client-side DSP tasks.
-- Receivers can now be listed on SDR.hu .
-- License for OpenWebRX is now Affero GPL v3.
+**News (2019-07-13 by DD5JFK)**
+- Latest Features:
+ - FT8 Integration (using wsjt-x demodulators)
+ - New Map Feature that shows both decoded grid squares from FT8 and Locations decoded from YSF digital voice
+ - New Feature report that will show what functionality is available
+- There's a new Raspbian SD Card image available (see below)
-**News (2016-02-14)**
-- The DDC in *csdr* has been manually optimized for ARM NEON, so it runs around 3 times faster on the Raspberry Pi 2 than before.
-- Also we use *ncat* instead of *rtl_mus*, and it is 3 times faster in some cases.
-- OpenWebRX now supports URLs like: `http://localhost:8073/#freq=145555000,mod=usb`
-- UI improvements were made, thanks to John Seamons and Gnoxter.
+**News (2019-06-30 by DD5JFK)**
+- I have done some major rework on the openwebrx core, and I am planning to continue adding more features in the near future. Please check this place for updates.
+- My work has not been accepted into the upstream repository, so you will need to chose between my fork and the official version.
+- I have enabled the issue tracker on this project, so feel free to file bugs or suggest enhancements there!
+- This version sports the following new and amazing features:
+ - Support of multiple SDR devices simultaneously
+ - Support for multiple profiles per SDR that allow the user to listen to different frequencies
+ - Support for digital voice decoding
+ - Feature detection that will disable functionality when dependencies are not available (if you're missing the digital buttons, this is probably why)
+- Raspbian SD Card Images and Docker builds available (see below)
+- I am currently working on the feature set for a stable release, but you are more than welcome to test development versions!
-**News (2017-04-04)**
-- *ncat* has been replaced with a custom implementation called *nmux* due to a bug that caused regular crashes on some machines. The *nmux* tool is part of the *csdr* package.
-- Most consumer SDR devices are supported via rx_tools , see the OpenWebRX Wiki on that.
-
-**News (2017-07-12)**
-- OpenWebRX now has a BPSK31 demodulator and a 3D waterfall display.
-
-> When upgrading OpenWebRX, please make sure that you also upgrade *csdr*!
+> When upgrading OpenWebRX, please make sure that you also upgrade *csdr* and *digiham*!
## OpenWebRX servers on SDR.hu
@@ -50,22 +54,49 @@ It has the following features:
## Setup
-OpenWebRX currently requires Linux and python 2.7 to run.
+### 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-07-13-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+.
+
+This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply.
+
+Please note: I have not updated this to include the Raspberry Pi 4 yet. (It seems to be impossible to build Rasbpian Buster images on x86 hardware right now. Stay tuned!)
+
+Once you have booted a Raspberry with the SD Card, it will appear in your network with the hostname "openwebrx", which should make it available as http://openwebrx:8073/ on most networks. This may vary depending on your specific setup.
+
+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.
+
+### Docker Images
+
+For those familiar with docker, I am providing [recent builds and Releases for both x86 and arm processors on the Docker hub](https://hub.docker.com/r/jketterl/openwebrx). You can find a short introduction there.
+
+### Manual Installation
+
+OpenWebRX currently requires Linux and python 3 to run.
First you will need to install the dependencies:
-- libcsdr
-- rtl-sdr
+- [csdr](https://github.com/simonyiszk/csdr)
+- [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr)
+
+Optional Dependencies if you want to be able to listen do digital voice:
+
+- [digiham](https://github.com/jketterl/digiham)
+- [dsd](https://github.com/f4exb/dsdcc)
+
+Optional Dependency if you want to decode WSJT-X modes:
+
+- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html)
After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server:
- python openwebrx.py
-
+ ./openwebrx.py
+
You can now open the GUI at http://localhost:8073 .
Please note that the server is also listening on the following ports (on localhost only):
-- port 4951 for the multi-user I/Q server.
+- ports 4950 to 4960 for the multi-user I/Q servers.
Now the next step is to customize the parameters of your server in `config_webrx.py`.
@@ -86,8 +117,6 @@ If you have any problems installing OpenWebRX, you should check out the summary ).
diff --git a/bands.json b/bands.json
new file mode 100644
index 0000000..30a26bb
--- /dev/null
+++ b/bands.json
@@ -0,0 +1,189 @@
+[
+ {
+ "name": "160m",
+ "lower_bound": 1810000,
+ "upper_bound": 2000000,
+ "frequencies": {
+ "psk31": 1838000,
+ "ft8": 1840000,
+ "wspr": 1836600,
+ "jt65": 1838000,
+ "jt9": 1839000
+ }
+ },
+ {
+ "name": "80m",
+ "lower_bound": 3500000,
+ "upper_bound": 3800000,
+ "frequencies": {
+ "psk31": 3580000,
+ "ft8": 3573000,
+ "wspr": 3592600,
+ "jt65": 3570000,
+ "jt9": 3572000,
+ "ft4": [3568000, 3568000]
+ }
+ },
+ {
+ "name": "60m",
+ "lower_bound": 5351500,
+ "upper_bound": 5366500,
+ "frequencies": {
+ "ft8": 5357000,
+ "wspr": 5287200
+ }
+ },
+ {
+ "name": "40m",
+ "lower_bound": 7000000,
+ "upper_bound": 7200000,
+ "frequencies": {
+ "psk31": 7040000,
+ "ft8": 7074000,
+ "wspr": 7038600,
+ "jt65": 7076000,
+ "jt9": 7078000,
+ "ft4": 7047500
+ }
+ },
+ {
+ "name": "30m",
+ "lower_bound": 10100000,
+ "upper_bound": 10150000,
+ "frequencies": {
+ "psk31": 10141000,
+ "ft8": 10136000,
+ "wspr": 10138700,
+ "jt65": 10138000,
+ "jt9": 10140000,
+ "ft4": 10140000
+ }
+ },
+ {
+ "name": "20m",
+ "lower_bound": 14000000,
+ "upper_bound": 14350000,
+ "frequencies": {
+ "psk31": 14070000,
+ "ft8": 14074000,
+ "wspr": 14095600,
+ "jt65": 14076000,
+ "jt9": 14078000,
+ "ft4": 14080000
+ }
+ },
+ {
+ "name": "17m",
+ "lower_bound": 18068000,
+ "upper_bound": 18168000,
+ "frequencies": {
+ "psk31": 18098000,
+ "ft8": 18100000,
+ "wspr": 18104600,
+ "jt65": 18102000,
+ "jt9": 18104000,
+ "ft4": 18104000
+ }
+ },
+ {
+ "name": "15m",
+ "lower_bound": 21000000,
+ "upper_bound": 21450000,
+ "frequencies": {
+ "psk31": 21070000,
+ "ft8": 21074000,
+ "wspr": 21094600,
+ "jt65": 21076000,
+ "jt9": 21078000,
+ "ft4": 21140000
+ }
+ },
+ {
+ "name": "12m",
+ "lower_bound": 24890000,
+ "upper_bound": 24990000,
+ "frequencies": {
+ "psk31": 24920000,
+ "ft8": 24915000,
+ "wspr": 24924600,
+ "jt65": 24917000,
+ "jt9": 24919000,
+ "ft4": 24919000
+ }
+ },
+ {
+ "name": "10m",
+ "lower_bound": 28000000,
+ "upper_bound": 29700000,
+ "frequencies": {
+ "psk31": [28070000, 28120000],
+ "ft8": 28074000,
+ "wspr": 28124600,
+ "jt65": 28076000,
+ "jt9": 28078000,
+ "ft4": 28180000
+ }
+ },
+ {
+ "name": "6m",
+ "lower_bound": 50030000,
+ "upper_bound": 51000000,
+ "frequencies": {
+ "psk31": 50305000,
+ "ft8": 50313000,
+ "wspr": 50293000,
+ "jt65": 50310000,
+ "jt9": 50312000,
+ "ft4": 50318000
+ }
+ },
+ {
+ "name": "4m",
+ "lower_bound": 70150000,
+ "upper_bound": 70200000,
+ "frequencies": {
+ "wspr": 70091000
+ }
+ },
+ {
+ "name": "2m",
+ "lower_bound": 144000000,
+ "upper_bound": 146000000,
+ "frequencies": {
+ "wspr": 144489000,
+ "ft8": 144174000,
+ "ft4": 144170000,
+ "jt65": 144120000
+ }
+ },
+ {
+ "name": "70cm",
+ "lower_bound": 430000000,
+ "upper_bound": 440000000
+ },
+ {
+ "name": "23cm",
+ "lower_bound": 1240000000,
+ "upper_bound": 1300000000
+ },
+ {
+ "name": "13cm",
+ "lower_bound": 2320000000,
+ "upper_bound": 2450000000
+ },
+ {
+ "name": "9cm",
+ "lower_bound": 3400000000,
+ "upper_bound": 3475000000
+ },
+ {
+ "name": "6cm",
+ "lower_bound": 5650000000,
+ "upper_bound": 5850000000
+ },
+ {
+ "name": "3cm",
+ "lower_bound": 10000000000,
+ "upper_bound": 10500000000
+ }
+]
\ No newline at end of file
diff --git a/config_webrx.py b/config_webrx.py
index 0613278..f75d3e3 100644
--- a/config_webrx.py
+++ b/config_webrx.py
@@ -35,21 +35,21 @@ config_webrx: configuration options for OpenWebRX
# https://github.com/simonyiszk/openwebrx/wiki
# ==== Server settings ====
-web_port=8073
-max_clients=20
+web_port = 8073
+max_clients = 20
# ==== Web GUI configuration ====
-receiver_name="[Callsign]"
-receiver_location="Budapest, Hungary"
-receiver_qra="JN97ML"
-receiver_asl=200
-receiver_ant="Longwire"
-receiver_device="RTL-SDR"
-receiver_admin="example@example.com"
-receiver_gps=(47.000000,19.000000)
-photo_height=350
-photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory"
-photo_desc="""
+receiver_name = "[Callsign]"
+receiver_location = "Budapest, Hungary"
+receiver_qra = "JN97ML"
+receiver_asl = 200
+receiver_ant = "Longwire"
+receiver_device = "RTL-SDR"
+receiver_admin = "example@example.com"
+receiver_gps = (47.000000, 19.000000)
+photo_height = 350
+photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
+photo_desc = """
You can add your own background photo and receiver information.
Receiver is operated by: %[RX_ADMIN]
Device: %[RX_DEVICE]
@@ -64,18 +64,20 @@ Website: http://localhost
sdrhu_key = ""
# 3. Set this setting to True to enable listing:
sdrhu_public_listing = False
-server_hostname="localhost"
+server_hostname = "localhost"
# ==== DSP/RX settings ====
-fft_fps=9
-fft_size=4096 #Should be power of 2
-fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
+fft_fps = 9
+fft_size = 4096 # Should be power of 2
+fft_voverlap_factor = (
+ 0.3
+) # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
-audio_compression="adpcm" #valid values: "adpcm", "none"
-fft_compression="adpcm" #valid values: "adpcm", "none"
+audio_compression = "adpcm" # valid values: "adpcm", "none"
+fft_compression = "adpcm" # valid values: "adpcm", "none"
-digimodes_enable=True #Decoding digimodes come with higher CPU usage.
-digimodes_fft_size=1024
+digimodes_enable = True # Decoding digimodes come with higher CPU usage.
+digimodes_fft_size = 1024
# determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes
# if you're running on a Raspi (up to 3B+) you'll want to leave this on 1
@@ -116,7 +118,7 @@ sdrs = {
"rf_gain": 30,
"samp_rate": 2400000,
"start_freq": 439275000,
- "start_mod": "nfm"
+ "start_mod": "nfm",
},
"2m": {
"name": "2m komplett",
@@ -124,9 +126,9 @@ sdrs = {
"rf_gain": 30,
"samp_rate": 2400000,
"start_freq": 145725000,
- "start_mod": "nfm"
- }
- }
+ "start_mod": "nfm",
+ },
+ },
},
"sdrplay": {
"name": "SDRPlay RSP2",
@@ -134,39 +136,39 @@ sdrs = {
"ppm": 0,
"profiles": {
"20m": {
- "name":"20m",
+ "name": "20m",
"center_freq": 14150000,
"rf_gain": 4,
"samp_rate": 500000,
"start_freq": 14070000,
"start_mod": "usb",
- "antenna": "Antenna A"
+ "antenna": "Antenna A",
},
"30m": {
- "name":"30m",
+ "name": "30m",
"center_freq": 10125000,
"rf_gain": 4,
"samp_rate": 250000,
"start_freq": 10142000,
- "start_mod": "usb"
+ "start_mod": "usb",
},
"40m": {
- "name":"40m",
+ "name": "40m",
"center_freq": 7100000,
"rf_gain": 4,
"samp_rate": 500000,
"start_freq": 7070000,
"start_mod": "usb",
- "antenna": "Antenna A"
+ "antenna": "Antenna A",
},
"80m": {
- "name":"80m",
+ "name": "80m",
"center_freq": 3650000,
"rf_gain": 4,
"samp_rate": 500000,
"start_freq": 3570000,
"start_mod": "usb",
- "antenna": "Antenna A"
+ "antenna": "Antenna A",
},
"49m": {
"name": "49m Broadcast",
@@ -175,42 +177,43 @@ sdrs = {
"samp_rate": 500000,
"start_freq": 6070000,
"start_mod": "am",
- "antenna": "Antenna A"
- }
- }
+ "antenna": "Antenna A",
+ },
+ },
},
# this one is just here to test feature detection
- "test": {
- "type": "test"
- }
+ "test": {"type": "test"},
}
# ==== Misc settings ====
client_audio_buffer_size = 5
-#increasing client_audio_buffer_size will:
+# increasing client_audio_buffer_size will:
# - also increase the latency
# - decrease the chance of audio underruns
-iq_port_range = [4950, 4960] #TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
+iq_port_range = [
+ 4950,
+ 4960,
+] # TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
# ==== Color themes ====
-#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
+# A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
### default theme by teejez:
-waterfall_colors = [0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]
-waterfall_min_level = -88 #in dB
+waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF]
+waterfall_min_level = -88 # in dB
waterfall_max_level = -20
waterfall_auto_level_margin = (5, 40)
### old theme by HA7ILM:
-#waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
-#waterfall_min_level = -115 #in dB
-#waterfall_max_level = 0
-#waterfall_auto_level_margin = (20, 30)
+# waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
+# waterfall_min_level = -115 #in dB
+# waterfall_max_level = 0
+# waterfall_auto_level_margin = (20, 30)
##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
-#Note: When the auto waterfall level button is clicked, the following happens:
+# Note: When the auto waterfall level button is clicked, the following happens:
# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]]
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]]
#
@@ -219,14 +222,35 @@ waterfall_auto_level_margin = (5, 40)
# current_max_power_level __|
# 3D view settings
-mathbox_waterfall_frequency_resolution = 128 #bins
-mathbox_waterfall_history_length = 10 #seconds
-mathbox_waterfall_colors = [0x000000ff,0x2e6893ff,0x69a5d0ff,0x214b69ff,0x9dc4e0ff,0xfff775ff,0xff8a8aff,0xb20000ff]
+mathbox_waterfall_frequency_resolution = 128 # bins
+mathbox_waterfall_history_length = 10 # seconds
+mathbox_waterfall_colors = [
+ 0x000000FF,
+ 0x2E6893FF,
+ 0x69A5D0FF,
+ 0x214B69FF,
+ 0x9DC4E0FF,
+ 0xFFF775FF,
+ 0xFF8A8AFF,
+ 0xB20000FF,
+]
# === Experimental settings ===
-#Warning! The settings below are very experimental.
-csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr.
+# Warning! The settings below are very experimental.
+csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr.
csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes.
-csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
+csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
-nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux.
+nmux_memory = 50 # in megabytes. This sets the approximate size of the circular buffer used by nmux.
+
+google_maps_api_key = ""
+
+# how long should positions be visible on the map?
+# they will start fading out after half of that
+# in seconds; default: 2 hours
+map_position_retention_time = 2 * 60 * 60
+
+temporary_directory = "/tmp"
+
+services_enabled = False
+services_decoders = ["ft8", "ft4", "wspr"]
diff --git a/csdr.py b/csdr.py
old mode 100755
new mode 100644
index 93e05d0..6b80da5
--- a/csdr.py
+++ b/csdr.py
@@ -21,33 +21,56 @@ OpenWebRX csdr plugin: do the signal processing with csdr
"""
import subprocess
-import time
import os
import signal
import threading
from functools import partial
+from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper
import logging
+
logger = logging.getLogger(__name__)
+
class output(object):
- def add_output(self, type, read_fn):
- pass
- def reset(self):
+ def send_output(self, t, read_fn):
+ if not self.supports_type(t):
+ # TODO rewrite the output mechanism in a way that avoids producing unnecessary data
+ logger.warning("dumping output of type %s since it is not supported.", t)
+ threading.Thread(target=self.pump(read_fn, lambda x: None)).start()
+ return
+ self.receive_output(t, read_fn)
+
+ def receive_output(self, t, read_fn):
pass
+ def pump(self, read, write):
+ def copy():
+ run = True
+ while run:
+ data = read()
+ if data is None or (isinstance(data, bytes) and len(data) == 0):
+ run = False
+ else:
+ write(data)
+
+ return copy
+
+ def supports_type(self, t):
+ return True
+
+
class dsp(object):
-
def __init__(self, output):
self.samp_rate = 250000
- self.output_rate = 11025 #this is default, and cannot be set at the moment
+ self.output_rate = 11025 # this is default, and cannot be set at the moment
self.fft_size = 1024
self.fft_fps = 5
self.offset_freq = 0
self.low_cut = -4000
self.high_cut = 4000
- self.bpf_transition_bw = 320 #Hz, and this is a constant
- self.ddc_transition_bw_rate = 0.15 # of the IF sample rate
+ self.bpf_transition_bw = 320 # Hz, and this is a constant
+ self.ddc_transition_bw_rate = 0.15 # of the IF sample rate
self.running = False
self.secondary_processes_running = False
self.audio_compression = "none"
@@ -67,105 +90,165 @@ class dsp(object):
self.secondary_fft_size = 1024
self.secondary_process_fft = None
self.secondary_process_demod = None
- self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe",
- "iqtee2_pipe", "dmr_control_pipe"]
- self.secondary_pipe_names=["secondary_shift_pipe"]
+ self.pipe_names = [
+ "bpf_pipe",
+ "shift_pipe",
+ "squelch_pipe",
+ "smeter_pipe",
+ "meta_pipe",
+ "iqtee_pipe",
+ "iqtee2_pipe",
+ "dmr_control_pipe",
+ ]
+ self.secondary_pipe_names = ["secondary_shift_pipe"]
self.secondary_offset_freq = 1000
self.unvoiced_quality = 1
self.modification_lock = threading.Lock()
self.output = output
+ self.temporary_directory = "/tmp"
- def chain(self,which):
- chain ="nc -v 127.0.0.1 {nc_port} | "
- if self.csdr_dynamic_bufsize: chain += "csdr setbuf {start_bufsize} | "
- if self.csdr_through: chain +="csdr through | "
+ def set_temporary_directory(self, what):
+ self.temporary_directory = what
+
+ def chain(self, which):
+ chain = ["nc -v 127.0.0.1 {nc_port}"]
+ if self.csdr_dynamic_bufsize:
+ chain += ["csdr setbuf {start_bufsize}"]
+ if self.csdr_through:
+ chain += ["csdr through"]
if which == "fft":
- chain += "csdr fft_cc {fft_size} {fft_block_size} | " + \
- ("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \
- "csdr fft_exchange_sides_ff {fft_size}"
- if self.fft_compression=="adpcm":
- chain += " | csdr compress_fft_adpcm_f_u8 {fft_size}"
+ chain += [
+ "csdr fft_cc {fft_size} {fft_block_size}",
+ "csdr logpower_cf -70"
+ if self.fft_averages == 0
+ else "csdr logaveragepower_cf -70 {fft_size} {fft_averages}",
+ "csdr fft_exchange_sides_ff {fft_size}",
+ ]
+ if self.fft_compression == "adpcm":
+ chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"]
return chain
- chain += "csdr shift_addition_cc --fifo {shift_pipe} | "
- chain += "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | "
- chain += "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every} | "
+ chain += [
+ "csdr shift_addition_cc --fifo {shift_pipe}",
+ "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING",
+ "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING",
+ ]
+ if self.output.supports_type("smeter"):
+ chain += [
+ "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}"
+ ]
if self.secondary_demodulator:
- chain += "csdr tee {iqtee_pipe} | "
- chain += "csdr tee {iqtee2_pipe} | "
+ if self.output.supports_type("secondary_fft"):
+ chain += ["csdr tee {iqtee_pipe}"]
+ chain += ["csdr tee {iqtee2_pipe}"]
+ # early exit if we don't want audio
+ if not self.output.supports_type("audio"):
+ return chain
# safe some cpu cycles... no need to decimate if decimation factor is 1
- last_decimation_block = "csdr fractional_decimator_ff {last_decimation} | " if self.last_decimation != 1.0 else ""
+ last_decimation_block = (
+ ["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else []
+ )
if which == "nfm":
- chain += "csdr fmdemod_quadri_cf | csdr limit_ff | "
+ chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"]
chain += last_decimation_block
- chain += "csdr deemphasis_nfm_ff {output_rate} | csdr convert_f_s16"
+ chain += ["csdr deemphasis_nfm_ff {output_rate}", "csdr convert_f_s16"]
elif self.isDigitalVoice(which):
- chain += "csdr fmdemod_quadri_cf | dc_block | "
+ chain += ["csdr fmdemod_quadri_cf", "dc_block "]
chain += last_decimation_block
# dsd modes
- if which in [ "dstar", "nxdn" ]:
- chain += "csdr limit_ff | csdr convert_f_s16 | "
+ if which in ["dstar", "nxdn"]:
+ chain += ["csdr limit_ff", "csdr convert_f_s16"]
if which == "dstar":
- chain += "dsd -fd"
+ chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "]
elif which == "nxdn":
- chain += "dsd -fi"
- chain += " -i - -o - -u {unvoiced_quality} -g -1 | CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f | "
+ chain += ["dsd -fi -i - -o - -u {unvoiced_quality} -g -1 "]
+ chain += ["CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f"]
max_gain = 5
# digiham modes
else:
- chain += "rrc_filter | gfsk_demodulator | "
+ chain += ["rrc_filter", "gfsk_demodulator"]
if which == "dmr":
- chain += "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | "
+ chain += [
+ "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}",
+ "mbe_synthesizer -f -u {unvoiced_quality}",
+ ]
elif which == "ysf":
- chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | "
+ chain += ["ysf_decoder --fifo {meta_pipe}", "mbe_synthesizer -y -f -u {unvoiced_quality}"]
max_gain = 0.0005
- chain += "digitalvoice_filter -f | "
- chain += "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain} | ".format(max_gain=max_gain)
- chain += "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
+ chain += [
+ "digitalvoice_filter -f",
+ "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain),
+ "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
+ ]
elif which == "packet":
- chain += "csdr fmdemod_quadri_cf | "
+ chain += ["csdr fmdemod_quadri_cf"]
chain += last_decimation_block
- chain += "csdr convert_f_s16 | "
- chain += "direwolf -r {audio_rate} - 1>&2"
+ chain += [
+ "csdr convert_f_s16",
+ "direwolf -r {audio_rate} - 1>&2"
+ ]
elif which == "am":
- chain += "csdr amdemod_cf | csdr fastdcblock_ff | "
+ chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"]
chain += last_decimation_block
- chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
+ chain += ["csdr agc_ff", "csdr limit_ff", "csdr convert_f_s16"]
elif which == "ssb":
- chain += "csdr realpart_cf | "
+ chain += ["csdr realpart_cf"]
chain += last_decimation_block
- chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
+ chain += ["csdr agc_ff", "csdr limit_ff"]
+ # fixed sample rate necessary for the wsjt-x tools. fix with sox...
+ if self.isWsjtMode() and self.get_audio_rate() != self.get_output_rate():
+ chain += [
+ "sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
+ ]
+ else:
+ chain += ["csdr convert_f_s16"]
- if self.audio_compression=="adpcm":
- chain += " | csdr encode_ima_adpcm_i16_u8"
+ if self.audio_compression == "adpcm":
+ chain += ["csdr encode_ima_adpcm_i16_u8"]
return chain
def secondary_chain(self, which):
- secondary_chain_base="cat {input_pipe} | "
+ secondary_chain_base = "cat {input_pipe} | "
if which == "fft":
- return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
+ return (
+ secondary_chain_base
+ + "csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 "
+ + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression == "adpcm" else "")
+ )
elif which == "bpsk31":
- return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \
- "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + \
- "csdr simple_agc_cc 0.001 0.5 | " + \
- "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \
- "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \
- "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"
+ return (
+ secondary_chain_base
+ + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | "
+ + "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | "
+ + "csdr simple_agc_cc 0.001 0.5 | "
+ + "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | "
+ + "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | "
+ + "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"
+ )
+ elif self.isWsjtMode(which):
+ chain = secondary_chain_base + "csdr realpart_cf | "
+ if self.last_decimation != 1.0:
+ chain += "csdr fractional_decimator_ff {last_decimation} | "
+ chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
+ return chain
def set_secondary_demodulator(self, what):
if self.get_secondary_demodulator() == what:
return
self.secondary_demodulator = what
+ self.calculate_decimation()
self.restart()
def secondary_fft_block_size(self):
- return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here
+ return (self.samp_rate / self.decimation) / (
+ self.fft_fps * 2
+ ) # *2 is there because we do FFT on real signal here
def secondary_decimation(self):
- return 1 #currently unused
+ return 1 # currently unused
def secondary_bpf_cutoff(self):
if self.secondary_demodulator == "bpsk31":
- return 31.25 / self.if_samp_rate()
+ return 31.25 / self.if_samp_rate()
return 0
def secondary_bpf_transition_bw(self):
@@ -175,7 +258,7 @@ class dsp(object):
def secondary_samples_per_bits(self):
if self.secondary_demodulator == "bpsk31":
- return int(round(self.if_samp_rate()/31.25))&~3
+ return int(round(self.if_samp_rate() / 31.25)) & ~3
return 0
def secondary_bw(self):
@@ -183,55 +266,82 @@ class dsp(object):
return 31.25
def start_secondary_demodulator(self):
- if not self.secondary_demodulator: return
- logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate())
- secondary_command_fft=self.secondary_chain("fft")
- secondary_command_demod=self.secondary_chain(self.secondary_demodulator)
- self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft)
+ if not self.secondary_demodulator:
+ return
+ logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate())
+ secondary_command_demod = self.secondary_chain(self.secondary_demodulator)
+ self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod)
- secondary_command_fft=secondary_command_fft.format(
- input_pipe=self.iqtee_pipe,
- secondary_fft_input_size=self.secondary_fft_size,
- secondary_fft_size=self.secondary_fft_size,
- secondary_fft_block_size=self.secondary_fft_block_size(),
- )
- secondary_command_demod=secondary_command_demod.format(
+ secondary_command_demod = secondary_command_demod.format(
input_pipe=self.iqtee2_pipe,
secondary_shift_pipe=self.secondary_shift_pipe,
secondary_decimation=self.secondary_decimation(),
secondary_samples_per_bits=self.secondary_samples_per_bits(),
secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
- if_samp_rate=self.if_samp_rate()
+ if_samp_rate=self.if_samp_rate(),
+ last_decimation=self.last_decimation,
+ )
+
+ logger.debug("secondary command (demod) = %s", secondary_command_demod)
+ my_env = os.environ.copy()
+ # if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
+ if self.csdr_print_bufsizes:
+ my_env["CSDR_PRINT_BUFSIZES"] = "1"
+ if self.output.supports_type("secondary_fft"):
+ secondary_command_fft = self.secondary_chain("fft")
+ secondary_command_fft = secondary_command_fft.format(
+ input_pipe=self.iqtee_pipe,
+ secondary_fft_input_size=self.secondary_fft_size,
+ secondary_fft_size=self.secondary_fft_size,
+ secondary_fft_block_size=self.secondary_fft_block_size(),
+ )
+ logger.debug("secondary command (fft) = %s", secondary_command_fft)
+
+ self.secondary_process_fft = subprocess.Popen(
+ secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env
+ )
+ self.output.send_output(
+ "secondary_fft",
+ partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())),
)
- logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft)
- logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod)
- my_env=os.environ.copy()
- #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
- if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
- self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
- logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)")
- self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes
- logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes
+ self.secondary_process_demod = subprocess.Popen(
+ secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env
+ )
self.secondary_processes_running = True
- self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())))
- self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
+ if self.isWsjtMode():
+ smd = self.get_secondary_demodulator()
+ if smd == "ft8":
+ chopper = Ft8Chopper(self.secondary_process_demod.stdout)
+ elif smd == "wspr":
+ chopper = WsprChopper(self.secondary_process_demod.stdout)
+ elif smd == "jt65":
+ chopper = Jt65Chopper(self.secondary_process_demod.stdout)
+ elif smd == "jt9":
+ chopper = Jt9Chopper(self.secondary_process_demod.stdout)
+ elif smd == "ft4":
+ chopper = Ft4Chopper(self.secondary_process_demod.stdout)
+ chopper.start()
+ self.output.send_output("wsjt_demod", chopper.read)
+ else:
+ self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
- #open control pipes for csdr and send initialization data
- if self.secondary_shift_pipe != None: #TODO digimodes
- self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes
- self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes
+ # open control pipes for csdr and send initialization data
+ if self.secondary_shift_pipe != None: # TODO digimodes
+ self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w") # TODO digimodes
+ self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes
def set_secondary_offset_freq(self, value):
- self.secondary_offset_freq=value
- if self.secondary_processes_running:
- self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate()))
+ self.secondary_offset_freq = value
+ if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"):
+ self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
self.secondary_shift_pipe_file.flush()
def stop_secondary_demodulator(self):
- if self.secondary_processes_running == False: return
+ if self.secondary_processes_running == False:
+ return
self.try_delete_pipes(self.secondary_pipe_names)
if self.secondary_process_fft:
try:
@@ -250,42 +360,47 @@ class dsp(object):
def get_secondary_demodulator(self):
return self.secondary_demodulator
- def set_secondary_fft_size(self,secondary_fft_size):
- #to change this, restart is required
- self.secondary_fft_size=secondary_fft_size
+ def set_secondary_fft_size(self, secondary_fft_size):
+ # to change this, restart is required
+ self.secondary_fft_size = secondary_fft_size
- def set_audio_compression(self,what):
+ def set_audio_compression(self, what):
self.audio_compression = what
- def set_fft_compression(self,what):
+ def set_fft_compression(self, what):
self.fft_compression = what
def get_fft_bytes_to_read(self):
- if self.fft_compression=="none": return self.fft_size*4
- if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2)
+ if self.fft_compression == "none":
+ return self.fft_size * 4
+ if self.fft_compression == "adpcm":
+ return (self.fft_size / 2) + (10 / 2)
def get_secondary_fft_bytes_to_read(self):
- if self.fft_compression=="none": return self.secondary_fft_size*4
- if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2)
+ if self.fft_compression == "none":
+ return self.secondary_fft_size * 4
+ if self.fft_compression == "adpcm":
+ return (self.secondary_fft_size / 2) + (10 / 2)
- def set_samp_rate(self,samp_rate):
- self.samp_rate=samp_rate
+ def set_samp_rate(self, samp_rate):
+ self.samp_rate = samp_rate
self.calculate_decimation()
- if self.running: self.restart()
+ if self.running:
+ self.restart()
def calculate_decimation(self):
(self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate())
def get_decimation(self, input_rate, output_rate):
- decimation=1
- while input_rate / (decimation+1) >= output_rate:
+ decimation = 1
+ while input_rate / (decimation + 1) >= output_rate:
decimation += 1
fraction = float(input_rate / decimation) / output_rate
intermediate_rate = input_rate / decimation
return (decimation, fraction, intermediate_rate)
def if_samp_rate(self):
- return self.samp_rate/self.decimation
+ return self.samp_rate / self.decimation
def get_name(self):
return self.name
@@ -296,61 +411,73 @@ class dsp(object):
def get_audio_rate(self):
if self.isDigitalVoice() or self.isPacket():
return 48000
+ elif self.isWsjtMode():
+ return 12000
return self.get_output_rate()
- def isDigitalVoice(self, demodulator = None):
+ def isDigitalVoice(self, demodulator=None):
if demodulator is None:
demodulator = self.get_demodulator()
return demodulator in ["dmr", "dstar", "nxdn", "ysf"]
+ def isWsjtMode(self, demodulator=None):
+ if demodulator is None:
+ demodulator = self.get_secondary_demodulator()
+ return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
+
def isPacket(self, demodulator = None):
if demodulator is None:
demodulator = self.get_demodulator()
return demodulator == "packet"
- def set_output_rate(self,output_rate):
- self.output_rate=output_rate
+ def set_output_rate(self, output_rate):
+ self.output_rate = output_rate
self.calculate_decimation()
- def set_demodulator(self,demodulator):
- if (self.demodulator == demodulator): return
- self.demodulator=demodulator
+ def set_demodulator(self, demodulator):
+ if self.demodulator == demodulator:
+ return
+ self.demodulator = demodulator
self.calculate_decimation()
self.restart()
def get_demodulator(self):
return self.demodulator
- def set_fft_size(self,fft_size):
- self.fft_size=fft_size
+ def set_fft_size(self, fft_size):
+ self.fft_size = fft_size
self.restart()
- def set_fft_fps(self,fft_fps):
- self.fft_fps=fft_fps
+ def set_fft_fps(self, fft_fps):
+ self.fft_fps = fft_fps
self.restart()
- def set_fft_averages(self,fft_averages):
- self.fft_averages=fft_averages
+ def set_fft_averages(self, fft_averages):
+ self.fft_averages = fft_averages
self.restart()
def fft_block_size(self):
- if self.fft_averages == 0: return self.samp_rate/self.fft_fps
- else: return self.samp_rate/self.fft_fps/self.fft_averages
+ if self.fft_averages == 0:
+ return self.samp_rate / self.fft_fps
+ else:
+ return self.samp_rate / self.fft_fps / self.fft_averages
- def set_offset_freq(self,offset_freq):
- self.offset_freq=offset_freq
+ def set_offset_freq(self, offset_freq):
+ self.offset_freq = offset_freq
if self.running:
self.modification_lock.acquire()
- self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate))
+ self.shift_pipe_file.write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
self.shift_pipe_file.flush()
self.modification_lock.release()
- def set_bpf(self,low_cut,high_cut):
- self.low_cut=low_cut
- self.high_cut=high_cut
+ def set_bpf(self, low_cut, high_cut):
+ self.low_cut = low_cut
+ self.high_cut = high_cut
if self.running:
self.modification_lock.acquire()
- self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) )
+ self.bpf_pipe_file.write(
+ "%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
+ )
self.bpf_pipe_file.flush()
self.modification_lock.release()
@@ -358,12 +485,12 @@ class dsp(object):
return [self.low_cut, self.high_cut]
def set_squelch_level(self, squelch_level):
- self.squelch_level=squelch_level
- #no squelch required on digital voice modes
+ self.squelch_level = squelch_level
+ # no squelch required on digital voice modes
actual_squelch = 0 if self.isDigitalVoice() or self.isPacket() else self.squelch_level
if self.running:
self.modification_lock.acquire()
- self.squelch_pipe_file.write("%g\n"%(float(actual_squelch)))
+ self.squelch_pipe_file.write("%g\n" % (float(actual_squelch)))
self.squelch_pipe_file.flush()
self.modification_lock.release()
@@ -379,7 +506,7 @@ class dsp(object):
self.dmr_control_pipe_file.write("{0}\n".format(filter))
self.dmr_control_pipe_file.flush()
- def mkfifo(self,path):
+ def mkfifo(self, path):
try:
os.unlink(path)
except:
@@ -387,64 +514,94 @@ class dsp(object):
os.mkfifo(path)
def ddc_transition_bw(self):
- return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate))
+ return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate))
def try_create_pipes(self, pipe_names, command_base):
for pipe_name in pipe_names:
- if "{"+pipe_name+"}" in command_base:
- setattr(self, pipe_name, self.pipe_base_path+pipe_name)
+ if "{" + pipe_name + "}" in command_base:
+ setattr(self, pipe_name, self.pipe_base_path + pipe_name)
self.mkfifo(getattr(self, pipe_name))
else:
setattr(self, pipe_name, None)
def try_delete_pipes(self, pipe_names):
for pipe_name in pipe_names:
- pipe_path = getattr(self,pipe_name,None)
+ pipe_path = getattr(self, pipe_name, None)
if pipe_path:
- try: os.unlink(pipe_path)
+ try:
+ os.unlink(pipe_path)
+ except FileNotFoundError:
+ # it seems like we keep calling this twice. no idea why, but we don't need the resulting error.
+ pass
except Exception:
logger.exception("try_delete_pipes()")
def start(self):
self.modification_lock.acquire()
- if (self.running):
+ if self.running:
self.modification_lock.release()
return
self.running = True
- command_base=self.chain(self.demodulator)
+ command_base = " | ".join(self.chain(self.demodulator))
- #create control pipes for csdr
- self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self))
+ # create control pipes for csdr
+ self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self))
self.try_create_pipes(self.pipe_names, command_base)
- #run the command
- command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation,
- last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages,
- bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(),
- flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port,
- squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe,
- output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000),
- unvoiced_quality = self.get_unvoiced_quality(), audio_rate = self.get_audio_rate(),
- dmr_control_pipe = self.dmr_control_pipe)
+ # run the command
+ command = command_base.format(
+ bpf_pipe=self.bpf_pipe,
+ shift_pipe=self.shift_pipe,
+ decimation=self.decimation,
+ last_decimation=self.last_decimation,
+ fft_size=self.fft_size,
+ fft_block_size=self.fft_block_size(),
+ fft_averages=self.fft_averages,
+ bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(),
+ ddc_transition_bw=self.ddc_transition_bw(),
+ flowcontrol=int(self.samp_rate * 2),
+ start_bufsize=self.base_bufsize * self.decimation,
+ nc_port=self.nc_port,
+ squelch_pipe=self.squelch_pipe,
+ smeter_pipe=self.smeter_pipe,
+ meta_pipe=self.meta_pipe,
+ iqtee_pipe=self.iqtee_pipe,
+ iqtee2_pipe=self.iqtee2_pipe,
+ output_rate=self.get_output_rate(),
+ smeter_report_every=int(self.if_samp_rate() / 6000),
+ unvoiced_quality=self.get_unvoiced_quality(),
+ dmr_control_pipe=self.dmr_control_pipe,
+ audio_rate=self.get_audio_rate(),
+ )
- logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command)
- my_env=os.environ.copy()
- if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
- if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
- self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
+ logger.debug("Command = %s", command)
+ my_env = os.environ.copy()
+ if self.csdr_dynamic_bufsize:
+ my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
+ if self.csdr_print_bufsizes:
+ my_env["CSDR_PRINT_BUFSIZES"] = "1"
+
+ out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
+ self.process = subprocess.Popen(command, stdout=out, shell=True, preexec_fn=os.setpgrp, env=my_env)
def watch_thread():
rc = self.process.wait()
logger.debug("dsp thread ended with rc=%d", rc)
- if (rc == 0 and self.running and not self.modification_lock.locked()):
+ if rc == 0 and self.running and not self.modification_lock.locked():
logger.debug("restarting since rc = 0, self.running = true, and no modification")
self.restart()
- threading.Thread(target = watch_thread).start()
+ threading.Thread(target=watch_thread).start()
- self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256))
+ if self.output.supports_type("audio"):
+ self.output.send_output(
+ "audio",
+ partial(
+ self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256
+ ),
+ )
# open control pipes for csdr
if self.bpf_pipe:
@@ -465,24 +622,28 @@ class dsp(object):
if self.bpf_pipe:
self.set_bpf(self.low_cut, self.high_cut)
if self.smeter_pipe:
- self.smeter_pipe_file=open(self.smeter_pipe,"r")
+ self.smeter_pipe_file = open(self.smeter_pipe, "r")
+
def read_smeter():
raw = self.smeter_pipe_file.readline()
if len(raw) == 0:
return None
else:
return float(raw.rstrip("\n"))
- self.output.add_output("smeter", read_smeter)
+
+ self.output.send_output("smeter", read_smeter)
if self.meta_pipe != None:
# TODO make digiham output unicode and then change this here
- self.meta_pipe_file=open(self.meta_pipe, "r", encoding="cp437")
+ self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437")
+
def read_meta():
raw = self.meta_pipe_file.readline()
if len(raw) == 0:
return None
else:
return raw.rstrip("\n")
- self.output.add_output("meta", read_meta)
+
+ self.output.send_output("meta", read_meta)
if self.dmr_control_pipe:
self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w")
@@ -503,10 +664,11 @@ class dsp(object):
self.modification_lock.release()
def restart(self):
- if not self.running: return
+ if not self.running:
+ return
self.stop()
self.start()
def __del__(self):
self.stop()
- del(self.process)
+ del self.process
diff --git a/docker/scripts/install-dependencies-hackrf.sh b/docker/scripts/install-dependencies-hackrf.sh
index 1a460cc..4786644 100755
--- a/docker/scripts/install-dependencies-hackrf.sh
+++ b/docker/scripts/install-dependencies-hackrf.sh
@@ -14,8 +14,8 @@ function cmakebuild() {
cd /tmp
-STATIC_PACKAGES="libusb fftw"
-BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++ libusb-dev fftw-dev"
+STATIC_PACKAGES="libusb fftw udev"
+BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
diff --git a/docker/scripts/install-dependencies-sdrplay.sh b/docker/scripts/install-dependencies-sdrplay.sh
index 3ac29cc..fba8598 100755
--- a/docker/scripts/install-dependencies-sdrplay.sh
+++ b/docker/scripts/install-dependencies-sdrplay.sh
@@ -14,8 +14,8 @@ function cmakebuild() {
cd /tmp
-STATIC_PACKAGES="libusb"
-BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++ libusb-dev"
+STATIC_PACKAGES="libusb udev"
+BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
diff --git a/docker/scripts/install-dependencies-soapysdr.sh b/docker/scripts/install-dependencies-soapysdr.sh
index 9e598c7..1731ed8 100755
--- a/docker/scripts/install-dependencies-soapysdr.sh
+++ b/docker/scripts/install-dependencies-soapysdr.sh
@@ -14,8 +14,10 @@ function cmakebuild() {
cd /tmp
-BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++"
+STATIC_PACKAGES="udev"
+BUILD_PACKAGES="git cmake make patch wget sudo gcc g++"
+apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/pothosware/SoapySDR
diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh
index 48136b2..2c7883e 100755
--- a/docker/scripts/install-dependencies.sh
+++ b/docker/scripts/install-dependencies.sh
@@ -14,8 +14,8 @@ function cmakebuild() {
cd /tmp
-STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack"
-BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers"
+STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport"
+BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev asciidoctor asciidoc"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
@@ -23,7 +23,7 @@ apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://git.code.sf.net/p/itpp/git itpp
cmakebuild itpp
-git clone https://github.com/simonyiszk/csdr.git
+git clone https://github.com/jketterl/csdr.git -b 48khz_filter
cd csdr
patch -Np1 <<'EOF'
--- a/csdr.c
@@ -68,6 +68,10 @@ rm -rf csdr
git clone https://github.com/szechyjs/mbelib.git
cmakebuild mbelib
+if [ -d "/usr/local/lib64" ]; then
+ # no idea why it's put into there now. alpine does not handle it correctly, so move it.
+ mv /usr/local/lib64/libmbe* /usr/local/lib
+fi
git clone https://github.com/jketterl/digiham.git
cmakebuild digiham
@@ -75,4 +79,10 @@ cmakebuild digiham
git clone https://github.com/f4exb/dsd.git
cmakebuild dsd
+WSJT_DIR=wsjtx-2.0.1
+WSJT_TGZ=${WSJT_DIR}.tgz
+wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ
+tar xvfz $WSJT_TGZ
+cmakebuild $WSJT_DIR
+
apk del .build-deps
diff --git a/htdocs/css/features.css b/htdocs/css/features.css
new file mode 100644
index 0000000..7b0b008
--- /dev/null
+++ b/htdocs/css/features.css
@@ -0,0 +1,12 @@
+@import url("openwebrx-header.css");
+@import url("openwebrx-globals.css");
+
+/* expandable photo not implemented on features page */
+#webrx-top-photo-clip {
+ max-height: 67px;
+}
+
+h1 {
+ text-align: center;
+ margin: 50px 0;
+}
\ No newline at end of file
diff --git a/htdocs/css/map.css b/htdocs/css/map.css
new file mode 100644
index 0000000..5d478cd
--- /dev/null
+++ b/htdocs/css/map.css
@@ -0,0 +1,57 @@
+@import url("openwebrx-header.css");
+@import url("openwebrx-globals.css");
+
+/* expandable photo not implemented on map page */
+#webrx-top-photo-clip {
+ max-height: 67px;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+}
+
+#webrx-top-container {
+ flex: none;
+}
+
+.openwebrx-map {
+ flex: 1 1 auto;
+}
+
+h3 {
+ margin: 10px 0;
+ text-align: center;
+}
+
+ul {
+ margin-block-start: 5px;
+ margin-block-end: 5px;
+ padding-inline-start: 25px;
+}
+
+.openwebrx-map-legend {
+ background-color: #fff;
+ padding: 10px;
+ margin: 10px;
+}
+
+.openwebrx-map-legend ul {
+ list-style-type: none;
+ padding: 0;
+}
+
+.openwebrx-map-legend li.square .illustration {
+ display: inline-block;
+ width: 30px;
+ height: 20px;
+ margin-right: 10px;
+ border-width: 2px;
+ border-style: solid;
+}
+
+.openwebrx-map-legend select {
+ background-color: #FFF;
+ border-color: #DDD;
+ padding: 5px;
+}
diff --git a/htdocs/css/openwebrx-globals.css b/htdocs/css/openwebrx-globals.css
new file mode 100644
index 0000000..41ef284
--- /dev/null
+++ b/htdocs/css/openwebrx-globals.css
@@ -0,0 +1,8 @@
+html, body
+{
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
+}
+
diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css
new file mode 100644
index 0000000..ef0a129
--- /dev/null
+++ b/htdocs/css/openwebrx-header.css
@@ -0,0 +1,203 @@
+#webrx-top-container
+{
+ position: relative;
+ z-index:1000;
+}
+
+#webrx-top-photo
+{
+ width: 100%;
+ display: block;
+}
+
+#webrx-top-photo-clip
+{
+ min-height: 67px;
+ max-height: 350px;
+ overflow: hidden;
+ position: relative;
+}
+
+.webrx-top-bar-parts
+{
+ height:67px;
+}
+
+#webrx-top-bar
+{
+ background: rgba(128, 128, 128, 0.15);
+ margin:0;
+ padding:0;
+ user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ overflow: hidden;
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+}
+
+#webrx-top-logo
+{
+ padding: 12px;
+ float: left;
+}
+
+#webrx-ha5kfu-top-logo
+{
+ float: right;
+ padding: 15px;
+}
+
+#webrx-rx-avatar-background
+{
+ cursor:pointer;
+ background-image: url(../gfx/openwebrx-avatar-background.png);
+ background-origin: content-box;
+ background-repeat: no-repeat;
+ float: left;
+ width: 54px;
+ height: 54px;
+ padding: 7px;
+ box-sizing: content-box;
+}
+
+#webrx-rx-avatar
+{
+ cursor:pointer;
+ width: 46px;
+ height: 46px;
+ padding: 4px;
+ border-radius: 8px;
+ box-sizing: content-box;
+}
+
+#webrx-rx-texts {
+ float: left;
+ padding: 10px;
+}
+
+#webrx-rx-texts div {
+ padding: 3px;
+}
+
+#webrx-rx-title
+{
+ white-space:nowrap;
+ overflow: hidden;
+ cursor:pointer;
+ font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
+ color: #909090;
+ font-size: 11pt;
+ font-weight: bold;
+}
+
+#webrx-rx-desc
+{
+ white-space:nowrap;
+ overflow: hidden;
+ cursor:pointer;
+ font-size: 10pt;
+ color: #909090;
+}
+
+#webrx-rx-desc a
+{
+ color: #909090;
+}
+
+#openwebrx-rx-details-arrow
+{
+ cursor:pointer;
+ position: absolute;
+ left: 470px;
+ top: 51px;
+}
+
+#openwebrx-rx-details-arrow a
+{
+ margin: 0;
+ padding: 0;
+}
+
+#openwebrx-rx-details-arrow-down
+{
+ display:none;
+}
+
+#openwebrx-main-buttons ul
+{
+ display: table;
+ margin:0;
+}
+
+
+#openwebrx-main-buttons ul li
+{
+ display: table-cell;
+ padding-left: 5px;
+ padding-right: 5px;
+ cursor:pointer;
+}
+
+#openwebrx-main-buttons a {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+#openwebrx-main-buttons li:hover
+{
+ background-color: rgba(255, 255, 255, 0.3);
+}
+
+#openwebrx-main-buttons li:active
+{
+ background-color: rgba(255, 255, 255, 0.55);
+}
+
+
+#openwebrx-main-buttons
+{
+ float: right;
+ margin:0;
+ color: white;
+ text-shadow: 0px 0px 4px #000000;
+ text-align: center;
+ font-size: 9pt;
+ font-weight: bold;
+}
+
+#webrx-rx-photo-title
+{
+ position: absolute;
+ left: 15px;
+ top: 78px;
+ color: White;
+ font-size: 16pt;
+ text-shadow: 1px 1px 4px #444;
+ opacity: 1;
+}
+
+#webrx-rx-photo-desc
+{
+ position: absolute;
+ left: 15px;
+ top: 109px;
+ color: White;
+ font-size: 10pt;
+ font-weight: bold;
+ text-shadow: 0px 0px 6px #444;
+ opacity: 1;
+ line-height: 1.5em;
+}
+
+#webrx-rx-photo-desc a
+{
+ color: #5ca8ff;
+ text-shadow: none;
+}
+
diff --git a/htdocs/openwebrx.css b/htdocs/css/openwebrx.css
similarity index 80%
rename from htdocs/openwebrx.css
rename to htdocs/css/openwebrx.css
index 5624d79..cd1498b 100644
--- a/htdocs/openwebrx.css
+++ b/htdocs/css/openwebrx.css
@@ -18,13 +18,10 @@
along with this program. If not, see .
*/
+@import url("openwebrx-header.css");
+@import url("openwebrx-globals.css");
-html, body
-{
- margin: 0;
- padding: 0;
- height: 100%;
- font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
+html, body {
overflow: hidden;
}
@@ -147,182 +144,16 @@ input[type=range]:focus::-ms-fill-upper
background: #B6B6B6;
}
-#webrx-top-container
-{
- position: relative;
- z-index:1000;
-}
-
-.webrx-top-bar-parts
-{
- height:67px;
-}
-
-#webrx-top-bar
-{
- background: rgba(128, 128, 128, 0.15);
- margin:0;
- padding:0;
- user-select: none;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- overflow: hidden;
- position: absolute;
- left: 0;
- top: 0;
- right: 0;
-}
-
-#webrx-top-logo
-{
- padding: 12px;
- float: left;
-}
-
-#webrx-ha5kfu-top-logo
-{
- float: right;
- padding: 15px;
-}
-
-#webrx-top-photo
-{
- width: 100%;
- display: block;
-}
-
-#webrx-rx-avatar-background
-{
- cursor:pointer;
- background-image: url(gfx/openwebrx-avatar-background.png);
- background-origin: content-box;
- background-repeat: no-repeat;
- float: left;
- width: 54px;
- height: 54px;
- padding: 7px;
-}
-
-#webrx-rx-avatar
-{
- cursor:pointer;
- width: 46px;
- height: 46px;
- padding: 4px;
-}
-
-#webrx-top-photo-clip
-{
- min-height: 67px;
- max-height: 350px;
- overflow: hidden;
- position: relative;
-}
-
#webrx-page-container
{
min-height:100%;
position:relative;
}
-#webrx-rx-photo-title
-{
- position: absolute;
- left: 15px;
- top: 78px;
- color: White;
- font-size: 16pt;
- text-shadow: 1px 1px 4px #444;
- opacity: 1;
-}
-
-#webrx-rx-photo-desc
-{
- position: absolute;
- left: 15px;
- top: 109px;
- color: White;
- font-size: 10pt;
- font-weight: bold;
- text-shadow: 0px 0px 6px #444;
- opacity: 1;
- line-height: 1.5em;
-}
-
-#webrx-rx-photo-desc a
-{
- color: #5ca8ff;
- text-shadow: none;
-}
-
-#webrx-rx-texts {
- float: left;
- padding: 10px;
-}
-
-#webrx-rx-texts div {
- padding: 3px;
-}
-
-#webrx-rx-title
-{
- white-space:nowrap;
- overflow: hidden;
- cursor:pointer;
- font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
- color: #909090;
- font-size: 11pt;
- font-weight: bold;
-}
-
-#webrx-rx-desc
-{
- white-space:nowrap;
- overflow: hidden;
- cursor:pointer;
- font-size: 10pt;
- color: #909090;
-}
-
-#webrx-rx-desc a
-{
- color: #909090;
-}
-
-#openwebrx-rx-details-arrow
-{
- cursor:pointer;
- position: absolute;
- left: 470px;
- top: 51px;
-}
-
-#openwebrx-rx-details-arrow a
-{
- margin: 0;
- padding: 0;
-}
-
-#openwebrx-rx-details-arrow-down
-{
- display:none;
-}
-
-/*canvas#waterfall-canvas
-{
- border-style: none;
- border-width: 1px;
- height: 150px;
- width: 100%;
-}*/
-
#openwebrx-scale-container
{
height: 47px;
- background-image: url("gfx/openwebrx-scale-background.png");
+ background-image: url("../gfx/openwebrx-scale-background.png");
background-repeat: repeat-x;
overflow: hidden;
z-index:1000;
@@ -331,14 +162,14 @@ input[type=range]:focus::-ms-fill-upper
#webrx-canvas-container
{
- /*background-image:url('gfx/openwebrx-blank-background-1.jpg');*/
+ /*background-image:url('../gfx/openwebrx-blank-background-1.jpg');*/
position: relative;
height: 2000px;
overflow-y: scroll;
overflow-x: hidden;
/*background-color: #646464;*/
/*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/
- background-image: url('gfx/openwebrx-background-cool-blue.png');
+ background-image: url('../gfx/openwebrx-background-cool-blue.png');
background-repeat: no-repeat;
background-color: #1e5f7f;
cursor: crosshair;
@@ -428,15 +259,15 @@ input[type=range]:focus::-ms-fill-upper
/* removed non-free fonts like that: */
/*@font-face {
font-family: 'unibody_8_pro_regregular';
- src: url('gfx/unibody8pro-regular-webfont.eot');
- src: url('gfx/unibody8pro-regular-webfont.ttf');
+ src: url('../gfx/unibody8pro-regular-webfont.eot');
+ src: url('../gfx/unibody8pro-regular-webfont.ttf');
font-weight: normal;
font-style: normal;
}*/
@font-face {
font-family: 'expletus-sans-medium';
- src: url('gfx/font-expletus-sans/ExpletusSans-Medium.ttf');
+ src: url('../gfx/font-expletus-sans/ExpletusSans-Medium.ttf');
font-weight: normal;
font-style: normal;
}
@@ -533,6 +364,20 @@ input[type=range]:focus::-ms-fill-upper
text-align: center;
}
+.openwebrx-dial-button svg {
+ width: 19px;
+ height: 19px;
+ vertical-align: bottom;
+}
+
+.openwebrx-dial-button #ph_dial {
+ fill: #888;
+}
+
+.openwebrx-dial-button.available #ph_dial {
+ fill: #FFF;
+}
+
.openwebrx-square-button img
{
height: 27px;
@@ -637,47 +482,6 @@ img.openwebrx-mirror-img
height: 20px;
}
-#openwebrx-main-buttons img
-{
-}
-
-#openwebrx-main-buttons ul
-{
- display: table;
- margin:0;
-}
-
-
-#openwebrx-main-buttons ul li
-{
- display: table-cell;
- padding-left: 5px;
- padding-right: 5px;
- cursor:pointer;
-}
-
-#openwebrx-main-buttons li:hover
-{
- background-color: rgba(255, 255, 255, 0.3);
-}
-
-#openwebrx-main-buttons li:active
-{
- background-color: rgba(255, 255, 255, 0.55);
-}
-
-
-#openwebrx-main-buttons
-{
- float: right;
- margin:0;
- color: white;
- text-shadow: 0px 0px 4px #000000;
- text-align: center;
- font-size: 9pt;
- font-weight: bold;
-}
-
#openwebrx-panel-receiver
{
width:110px;
@@ -812,7 +616,7 @@ img.openwebrx-mirror-img
#openwebrx-secondary-demod-listbox
{
- width: 201px;
+ width: 174px;
height: 27px;
padding-left:3px;
}
@@ -951,7 +755,7 @@ img.openwebrx-mirror-img
.openwebrx-meta-slot.muted:before {
display: block;
content: "";
- background-image: url("gfx/openwebrx-mute.png");
+ background-image: url("../gfx/openwebrx-mute.png");
width:100%;
height:133px;
background-position: center;
@@ -993,11 +797,11 @@ img.openwebrx-mirror-img
}
.openwebrx-meta-slot.active .openwebrx-meta-user-image {
- background-image: url("gfx/openwebrx-directcall.png");
+ background-image: url("../gfx/openwebrx-directcall.png");
}
.openwebrx-meta-slot.active .openwebrx-meta-user-image.group {
- background-image: url("gfx/openwebrx-groupcall.png");
+ background-image: url("../gfx/openwebrx-groupcall.png");
}
.openwebrx-dmr-timeslot-panel * {
@@ -1005,7 +809,7 @@ img.openwebrx-mirror-img
}
.openwebrx-maps-pin {
- background-image: url("gfx/google_maps_pin.svg");
+ background-image: url("../gfx/google_maps_pin.svg");
background-position: center;
background-repeat: no-repeat;
width: 15px;
@@ -1013,3 +817,62 @@ img.openwebrx-mirror-img
background-size: contain;
display: inline-block;
}
+
+#openwebrx-panel-wsjt-message {
+ height: 180px;
+}
+
+#openwebrx-panel-wsjt-message tbody {
+ display: block;
+ overflow: auto;
+ height: 150px;
+ width: 100%;
+}
+
+#openwebrx-panel-wsjt-message thead tr {
+ display: block;
+}
+
+#openwebrx-panel-wsjt-message th,
+#openwebrx-panel-wsjt-message td {
+ width: 50px;
+ text-align: left;
+ padding: 1px 3px;
+}
+
+#openwebrx-panel-wsjt-message .message {
+ width: 380px;
+}
+
+#openwebrx-panel-wsjt-message .decimal {
+ text-align: right;
+ width: 35px;
+}
+
+#openwebrx-panel-wsjt-message .decimal.freq {
+ width: 70px;
+}
+
+#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container,
+#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container,
+#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container,
+#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-content-container,
+#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container,
+#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
+#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
+#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
+#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel,
+#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel
+{
+ display: none;
+}
+
+#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container,
+#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container,
+#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-canvas-container,
+#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container,
+#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container
+{
+ height: 200px;
+ margin: -10px;
+}
diff --git a/htdocs/features.html b/htdocs/features.html
new file mode 100644
index 0000000..6e1eb55
--- /dev/null
+++ b/htdocs/features.html
@@ -0,0 +1,21 @@
+
+ OpenWebRX Feature report
+
+
+
+
+
+
+ ${header}
+
+
OpenWebRX Feature Report
+
+
+ Feature
+ Requirement
+ Description
+ Available
+
+
+
+
\ No newline at end of file
diff --git a/htdocs/features.js b/htdocs/features.js
new file mode 100644
index 0000000..6da77c8
--- /dev/null
+++ b/htdocs/features.js
@@ -0,0 +1,24 @@
+$(function(){
+ var converter = new showdown.Converter();
+ $.ajax('/api/features').done(function(data){
+ var $table = $('table.features');
+ $.each(data, function(name, details) {
+ var requirements = $.map(details.requirements, function(r, name){
+ return '' +
+ ' ' +
+ '' + name + ' ' +
+ '' + converter.makeHtml(r.description) + ' ' +
+ '' + (r.available ? 'YES' : 'NO') + ' ' +
+ ' ';
+ });
+ $table.append(
+ '' +
+ '' + name + ' ' +
+ '' + converter.makeHtml(details.description) + ' ' +
+ '' + (details.available ? 'YES' : 'NO') + ' ' +
+ ' ' +
+ requirements.join("")
+ );
+ })
+ });
+});
\ No newline at end of file
diff --git a/htdocs/gfx/openwebrx-panel-map.png b/htdocs/gfx/openwebrx-panel-map.png
new file mode 100644
index 0000000..81ec9e2
Binary files /dev/null and b/htdocs/gfx/openwebrx-panel-map.png differ
diff --git a/htdocs/gfx/openwebrx-top-photo.jpg b/htdocs/gfx/openwebrx-top-photo.jpg
index cf521c7..afc8e7e 100644
Binary files a/htdocs/gfx/openwebrx-top-photo.jpg and b/htdocs/gfx/openwebrx-top-photo.jpg differ
diff --git a/htdocs/inactive.html b/htdocs/inactive.html
deleted file mode 100644
index c7214c5..0000000
--- a/htdocs/inactive.html
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-OpenWebRX
-
-
-
-
-
-
-
- Sorry, the receiver is inactive due to internal error.
-
-
-
-
-
diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html
new file mode 100644
index 0000000..c7efe13
--- /dev/null
+++ b/htdocs/include/header.include.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Status
+ Log
+ Receiver
+ Map
+
+
+
+
+
+
+
diff --git a/htdocs/index.html b/htdocs/index.html
index 022e2ac..2629614 100644
--- a/htdocs/index.html
+++ b/htdocs/index.html
@@ -25,43 +25,15 @@
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Status
- Log
- Receiver
-
-
-
-
-
-
-
+ ${header}
@@ -111,7 +83,19 @@
BPSK31
+ FT8
+ WSPR
+ JT65
+ JT9
+ FT4
+
@@ -160,7 +144,7 @@
Under construction
We're working on the code right now, so the application might fail.
-
+
+
+ UTC
+ dB
+ DT
+ Freq
+ Message
+
+
+