Compare commits

..

No commits in common. "develop" and "0.20.3" have entirely different histories.

309 changed files with 7088 additions and 20694 deletions

View File

@ -1,48 +1,3 @@
**unreleased**
- SDR device log messages are now available in the web configuration to simplify troubleshooting
- Added support for the MSK144 digimode
**1.2.1**
- FifiSDR support fixed (pipeline formats now line up correctly)
- Added "Device" input for FifiSDR devices for sound card selection
**1.2.0**
- Major rewrite of all demodulation components to make use of the new csdr/pycsdr and digiham/pydigiham demodulator
modules
- Preliminary display of M17 callsign information
- New devices supported:
- Blade RF
**1.1.0**
- Reworked most graphical elements as SVGs for faster loadtimes and crispier display on hi-dpi displays
- Updated pipelines to match changes in digiham
- Changed D-Star and NXDN integrations to use new decoders from digiham
- Added D-Star and NXDN metadata display
**1.0.0**
- Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level
- Removed `port` configuration option; `rtltcp_compat` takes the port number with the new connectors
- Added support for new WSJT-X modes FST4, FST4W (only available with WSJT-X 2.3) and Q65 (only avilable with
WSJT-X 2.4)
- Added support for demodulating M17 digital voice signals using m17-cxx-demod
- New reporting infrastructure, allowing WSPR and FST4W spots to be sent to wsprnet.org
- Add some basic filtering capabilities to the map
- New arguments to the `openwebrx` command-line to facilitate the administration of users (try `openwebrx admin`)
- Default bandwidth changes:
- "WFM" changed to 150kHz
- "Packet" (APRS) changed to 12.5kHz
- Configuration rework:
- New: fully web-based configuration interface
- System configuration parameters have been moved to a new, separate `openwebrx.conf` file
- Remaining parameters are now editable in the web configuration
- Existing `config_webrx.py` files will still be read, but changes made in the web configuration will be written to
a new storage system
- Added upload of avatar and panorama image via web configuration
- New devices supported:
- HPSDR devices (Hermes Lite 2) thanks to @jancona
- BBRF103 / RX666 / RX888 devices supported by libsddc
- R&S devices using the EB200 or Ammos protocols
**0.20.3** **0.20.3**
- Fix a compatibility issue with python versions <= 3.6 - Fix a compatibility issue with python versions <= 3.6

View File

@ -11,16 +11,11 @@ It has the following features:
- filter passband can be set from GUI - filter passband can be set from GUI
- it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas - it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas
- it works in Google Chrome, Chromium and Mozilla Firefox - it works in Google Chrome, Chromium and Mozilla Firefox
- supports a wide range of [SDR hardware](https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices) - currently supports RTL-SDR, HackRF, SDRplay, AirSpy, LimeSDR, PlutoSDR
- Multiple SDR devices can be used simultaneously - Multiple SDR devices can be used simultaneously
- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag, D-Star, NXDN) - [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag)
- [wsjt-x](https://wsjt.sourceforge.io/) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4, - [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN)
FST4W) - [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9)
- [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets
- [JS8Call](http://js8call.com/) support
- [DRM](https://github.com/jketterl/openwebrx/wiki/DRM-demodulator-notes) support
- [FreeDV](https://github.com/jketterl/openwebrx/wiki/FreeDV-demodulator-notes) support
- M17 support based on [m17-cxx-demod](https://github.com/mobilinkd/m17-cxx-demod)
## Setup ## Setup
@ -40,9 +35,6 @@ If you have trouble setting up or configuring your receiver, you have some great
you just generally want to have some OpenWebRX-related chat, come visit us over on you just generally want to have some OpenWebRX-related chat, come visit us over on
[our groups.io group](https://groups.io/g/openwebrx). [our groups.io group](https://groups.io/g/openwebrx).
If you want to hang out, chat, or get in touch directly with the developers, receiver operators or users, feel free to
drop by in [our Discord server](https://discord.gg/gnE9hPz).
## Usage tips ## Usage tips
You can zoom the waterfall display by the mouse wheel. You can also drag the waterfall to pan across it. You can zoom the waterfall display by the mouse wheel. You can also drag the waterfall to pan across it.

View File

@ -1,24 +1,4 @@
[ [
{
"name": "2190m",
"lower_bound": 135700,
"upper_bound": 137800,
"frequencies": {
"fst4": 136000,
"fst4w": 136000
},
"tags": ["hamradio"]
},
{
"name": "630m",
"lower_bound": 472000,
"upper_bound": 479000,
"frequencies": {
"fst4": 474200,
"fst4w": 474200
},
"tags": ["hamradio"]
},
{ {
"name": "160m", "name": "160m",
"lower_bound": 1810000, "lower_bound": 1810000,
@ -29,11 +9,8 @@
"wspr": 1836600, "wspr": 1836600,
"jt65": 1838000, "jt65": 1838000,
"jt9": 1839000, "jt9": 1839000,
"js8": 1842000, "js8": 1842000
"fst4": 1839000, }
"fst4w": 1836800
},
"tags": ["hamradio"]
}, },
{ {
"name": "80m", "name": "80m",
@ -42,13 +19,12 @@
"frequencies": { "frequencies": {
"bpsk31": 3580000, "bpsk31": 3580000,
"ft8": 3573000, "ft8": 3573000,
"wspr": 3568600, "wspr": 3592600,
"jt65": 3570000, "jt65": 3570000,
"jt9": 3572000, "jt9": 3572000,
"ft4": [3568000, 3575000], "ft4": [3568000, 3575000],
"js8": 3578000 "js8": 3578000
}, }
"tags": ["hamradio"]
}, },
{ {
"name": "60m", "name": "60m",
@ -56,9 +32,8 @@
"upper_bound": 5366500, "upper_bound": 5366500,
"frequencies": { "frequencies": {
"ft8": 5357000, "ft8": 5357000,
"wspr": [5287200, 5364700] "wspr": 5364700
}, }
"tags": ["hamradio"]
}, },
{ {
"name": "40m", "name": "40m",
@ -72,8 +47,7 @@
"jt9": 7078000, "jt9": 7078000,
"ft4": 7047500, "ft4": 7047500,
"js8": 7078000 "js8": 7078000
}, }
"tags": ["hamradio"]
}, },
{ {
"name": "30m", "name": "30m",
@ -87,8 +61,7 @@
"jt9": 10140000, "jt9": 10140000,
"ft4": 10140000, "ft4": 10140000,
"js8": 10130000 "js8": 10130000
}, }
"tags": ["hamradio"]
}, },
{ {
"name": "20m", "name": "20m",
@ -102,8 +75,7 @@
"jt9": 14078000, "jt9": 14078000,
"ft4": 14080000, "ft4": 14080000,
"js8": 14078000 "js8": 14078000
}, }
"tags": ["hamradio"]
}, },
{ {
"name": "17m", "name": "17m",
@ -117,8 +89,7 @@
"jt9": 18104000, "jt9": 18104000,
"ft4": 18104000, "ft4": 18104000,
"js8": 18104000 "js8": 18104000
}, }
"tags": ["hamradio"]
}, },
{ {
"name": "15m", "name": "15m",
@ -132,8 +103,7 @@
"jt9": 21078000, "jt9": 21078000,
"ft4": 21140000, "ft4": 21140000,
"js8": 21078000 "js8": 21078000
}, }
"tags": ["hamradio"]
}, },
{ {
"name": "12m", "name": "12m",
@ -147,8 +117,7 @@
"jt9": 24919000, "jt9": 24919000,
"ft4": 24919000, "ft4": 24919000,
"js8": 24922000 "js8": 24922000
}, }
"tags": ["hamradio"]
}, },
{ {
"name": "10m", "name": "10m",
@ -162,8 +131,7 @@
"jt9": 28078000, "jt9": 28078000,
"ft4": 28180000, "ft4": 28180000,
"js8": 28078000 "js8": 28078000
}, }
"tags": ["hamradio"]
}, },
{ {
"name": "6m", "name": "6m",
@ -176,21 +144,16 @@
"jt65": 50310000, "jt65": 50310000,
"jt9": 50312000, "jt9": 50312000,
"ft4": 50318000, "ft4": 50318000,
"js8": 50318000, "js8": 50318000
"q65": [50211000, 50275000], }
"msk144": 50260000
},
"tags": ["hamradio"]
}, },
{ {
"name": "4m", "name": "4m",
"lower_bound": 70150000, "lower_bound": 70150000,
"upper_bound": 70200000, "upper_bound": 70200000,
"frequencies": { "frequencies": {
"wspr": 70091000, "wspr": 70091000
"msk144": 70230000 }
},
"tags": ["hamradio"]
}, },
{ {
"name": "2m", "name": "2m",
@ -201,171 +164,110 @@
"ft8": 144174000, "ft8": 144174000,
"ft4": 144170000, "ft4": 144170000,
"jt65": 144120000, "jt65": 144120000,
"packet": 144800000, "packet": 144800000
"q65": 144116000, }
"msk144": 144360000
},
"tags": ["hamradio"]
}, },
{ {
"name": "70cm", "name": "70cm",
"lower_bound": 430000000, "lower_bound": 430000000,
"upper_bound": 440000000, "upper_bound": 440000000,
"frequencies": { "frequencies": {
"pocsag": 439987500, "pocsag": 439987500
"q65": 432065000, }
"msk144": 432360000
},
"tags": ["hamradio"]
}, },
{ {
"name": "23cm", "name": "23cm",
"lower_bound": 1240000000, "lower_bound": 1240000000,
"upper_bound": 1300000000, "upper_bound": 1300000000
"frequencies": {
"q65": 1296065000
},
"tags": ["hamradio"]
}, },
{ {
"name": "13cm", "name": "13cm",
"lower_bound": 2320000000, "lower_bound": 2320000000,
"upper_bound": 2450000000, "upper_bound": 2450000000
"frequencies": {
"q65": [2301065000, 2304065000, 2320065000]
},
"tags": ["hamradio"]
}, },
{ {
"name": "9cm", "name": "9cm",
"lower_bound": 3400000000, "lower_bound": 3400000000,
"upper_bound": 3475000000, "upper_bound": 3475000000
"frequencies": {
"q65": 3400065000
},
"tags": ["hamradio"]
}, },
{ {
"name": "6cm", "name": "6cm",
"lower_bound": 5650000000, "lower_bound": 5650000000,
"upper_bound": 5850000000, "upper_bound": 5850000000
"frequencies": {
"q65": 5760200000
},
"tags": ["hamradio"]
}, },
{ {
"name": "3cm", "name": "3cm",
"lower_bound": 10000000000, "lower_bound": 10000000000,
"upper_bound": 10500000000, "upper_bound": 10500000000
"frequencies": {
"q65": 10368200000
},
"tags": ["hamradio"]
}, },
{ {
"name": "120m Broadcast", "name": "120m Broadcast",
"lower_bound": 2300000, "lower_bound": 2300000,
"upper_bound": 2495000, "upper_bound": 2495000
"tags": ["broadcast"]
}, },
{ {
"name": "90m Broadcast", "name": "90m Broadcast",
"lower_bound": 3200000, "lower_bound": 3200000,
"upper_bound": 3400000, "upper_bound": 3400000
"tags": ["broadcast"]
}, },
{ {
"name": "75m Broadcast", "name": "75m Broadcast",
"lower_bound": 3900000, "lower_bound": 3900000,
"upper_bound": 4000000, "upper_bound": 4000000
"tags": ["broadcast"]
}, },
{ {
"name": "60m Broadcast", "name": "60m Broadcast",
"lower_bound": 4750000, "lower_bound": 4750000,
"upper_bound": 4995000, "upper_bound": 4995000
"tags": ["broadcast"]
}, },
{ {
"name": "49m Broadcast", "name": "49m Broadcast",
"lower_bound": 5900000, "lower_bound": 5900000,
"upper_bound": 6200000, "upper_bound": 6200000
"tags": ["broadcast"]
}, },
{ {
"name": "41m Broadcast", "name": "41m Broadcast",
"lower_bound": 7200000, "lower_bound": 7200000,
"upper_bound": 7450000, "upper_bound": 7450000
"tags": ["broadcast"]
}, },
{ {
"name": "31m Broadcast", "name": "31m Broadcast",
"lower_bound": 9400000, "lower_bound": 9400000,
"upper_bound": 9900000, "upper_bound": 9900000
"tags": ["broadcast"]
}, },
{ {
"name": "25m Broadcast", "name": "25m Broadcast",
"lower_bound": 11600000, "lower_bound": 11600000,
"upper_bound": 12100000, "upper_bound": 12100000
"tags": ["broadcast"]
}, },
{ {
"name": "22m Broadcast", "name": "22m Broadcast",
"lower_bound": 13570000, "lower_bound": 13570000,
"upper_bound": 13870000, "upper_bound": 13870000
"tags": ["broadcast"]
}, },
{ {
"name": "19m Broadcast", "name": "19m Broadcast",
"lower_bound": 15100000, "lower_bound": 15100000,
"upper_bound": 15830000, "upper_bound": 15830000
"tags": ["broadcast"]
}, },
{ {
"name": "16m Broadcast", "name": "16m Broadcast",
"lower_bound": 17480000, "lower_bound": 17480000,
"upper_bound": 17900000, "upper_bound": 17900000
"tags": ["broadcast"]
}, },
{ {
"name": "15m Broadcast", "name": "15m Broadcast",
"lower_bound": 18900000, "lower_bound": 18900000,
"upper_bound": 19020000, "upper_bound": 19020000
"tags": ["broadcast"]
}, },
{ {
"name": "13m Broadcast", "name": "13m Broadcast",
"lower_bound": 21450000, "lower_bound": 21450000,
"upper_bound": 21850000, "upper_bound": 21850000
"tags": ["broadcast"]
}, },
{ {
"name": "11m Broadcast", "name": "11m Broadcast",
"lower_bound": 25670000, "lower_bound": 25670000,
"upper_bound": 26100000, "upper_bound": 26100000
"tags": ["broadcast"]
},
{
"name": "FM Broadcast",
"lower_bound": 87500000,
"upper_bound": 108000000,
"tags": ["broadcast"]
},
{
"name": "11m CB",
"lower_bound": 26965000,
"upper_bound": 27405000,
"frequencies": {
"js8": 27245000
},
"tags": ["public"]
},
{
"name": "PMR446",
"lower_bound": 446000000,
"upper_bound": 446200000,
"tags": ["public"]
} }
] ]

217
bookmarks.json Normal file
View File

@ -0,0 +1,217 @@
[
{
"name": "DB0ZU",
"frequency": 145725000,
"modulation": "nfm"
},
{
"name": "DB0ZM",
"frequency": 145750000,
"modulation": "nfm"
},
{
"name": "DM0ULR",
"frequency": 145787500,
"modulation": "nfm"
},
{
"name": "DB0EL",
"frequency": 439275000,
"modulation": "nfm"
},
{
"name": "DB0NJ",
"frequency": 438775000,
"modulation": "nfm"
},
{
"name": "DB0NJ",
"frequency": 439437500,
"modulation": "dmr"
},
{
"name": "DB0UFO",
"frequency": 438312500,
"modulation": "dmr"
},
{
"name": "DB0PV",
"frequency": 438525000,
"modulation": "ysf"
},
{
"name": "DB0BZA",
"frequency": 438412500,
"modulation": "ysf"
},
{
"name": "DB0OSH",
"frequency": 438250000,
"modulation": "ysf"
},
{
"name": "DB0ULR",
"frequency": 439325000,
"modulation": "nfm"
},
{
"name": "DB0ZU",
"frequency": 438850000,
"modulation": "nfm"
},
{
"name": "DB0ISW",
"frequency": 438650000,
"modulation": "nfm"
},
{
"name": "Radio DARC",
"frequency": 6070000,
"modulation": "am"
},
{
"name": "DB0TVM",
"frequency": 439575000,
"modulation": "dstar"
},
{
"name": "DB0TVM",
"frequency": 439800000,
"modulation": "dmr"
},
{
"name": "DB0TR",
"frequency": 438700000,
"modulation": "nfm"
},
{
"name": "DB0PME",
"frequency": 439825000,
"modulation": "dmr"
},
{
"name": "DB0HKN",
"frequency": 438300000,
"modulation": "dmr"
},
{
"name": "OE2XHM",
"frequency": 438825000,
"modulation": "nfm"
},
{
"name": "DM0WW",
"frequency": 438962500,
"modulation": "dmr"
},
{
"name": "OE7XXR",
"frequency": 438200000,
"modulation": "dstar"
},
{
"name": "OE2XZR",
"frequency": 439000000,
"modulation": "dstar"
},
{
"name": "DB0OAL",
"frequency": 439912500,
"modulation": "dmr"
},
{
"name": "DB0AAT",
"frequency": 439550000,
"modulation": "dmr"
},
{
"name": "DB0FSG",
"frequency": 439937500,
"modulation": "dmr"
},
{
"name": "DB0ULR",
"frequency": 145575000,
"modulation": "nfm"
},
{
"name": "DB0RDH",
"frequency": 145737500,
"modulation": "dstar"
},
{
"name": "DM0GAP",
"frequency": 145612500,
"modulation": "nfm"
},
{
"name": "DB0XF",
"frequency": 145600000,
"modulation": "nfm"
},
{
"name": "DB0TOL",
"frequency": 145712500,
"modulation": "nfm"
},
{
"name": "DB0TTB",
"frequency": 439587500,
"modulation": "dmr"
},
{
"name": "DB0TRS",
"frequency": 439125000,
"modulation": "nfm"
},
{
"name": "DB0OAL",
"frequency": 438937500,
"modulation": "nfm"
},
{
"name": "DM0ULR",
"frequency": 439337500,
"modulation": "nxdn"
},
{
"name": "DB0MIR",
"frequency": 439300000,
"modulation": "nfm"
},
{
"name": "DB0PM",
"frequency": 439075000,
"modulation": "nfm"
},
{
"name": "DB0CP",
"frequency": 439025000,
"modulation": "nfm"
},
{
"name": "OE7XGR",
"frequency": 438925000,
"modulation": "dmr"
},
{
"name": "DB0TOL",
"frequency": 438725000,
"modulation": "nfm"
},
{
"name": "DB0OAL",
"frequency": 438325000,
"modulation": "dstar"
},
{
"name": "DB0ROL",
"frequency": 439237500,
"modulation": "nfm"
},
{
"name": "DB0ABX",
"frequency": 439137500,
"modulation": "nfm"
}
]

345
config_webrx.py Normal file
View File

@ -0,0 +1,345 @@
# -*- coding: utf-8 -*-
"""
config_webrx: configuration options for OpenWebRX
This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
In addition, as a special exception, the copyright holders
state that config_rtl.py and config_webrx.py are not part of the
Corresponding Source defined in GNU AGPL version 3 section 1.
(It means that you do not have to redistribute config_rtl.py and
config_webrx.py if you make any changes to these two configuration files,
and use them for running your web service with OpenWebRX.)
"""
# configuration version. please only modify if you're able to perform the associated migration steps.
version = 3
# NOTE: you can find additional information about configuring OpenWebRX in the Wiki:
# https://github.com/jketterl/openwebrx/wiki/Configuration-guide
# ==== Server settings ====
web_port = 8073
max_clients = 20
# ==== Web GUI configuration ====
receiver_name = "[Callsign]"
receiver_location = "Budapest, Hungary"
receiver_asl = 200
receiver_admin = "example@example.com"
receiver_gps = {"lat": 47.000000, "lon": 19.000000}
photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
# photo_desc allows you to put pretty much any HTML you like into the receiver description.
# The lines below should give you some examples of what's possible.
photo_desc = """
You can add your own background photo and receiver information.<br />
Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
Device: Receiver Device<br />
Antenna: Receiver Antenna<br />
Website: <a href="http://localhost" target="_blank">http://localhost</a>
"""
# ==== Public receiver listings ====
# You can publish your receiver on online receiver directories, like https://www.receiverbook.de
# You will receive a receiver key from the directory that will authenticate you as the operator of this receiver.
# Please note that you not share your receiver keys publicly since anyone that obtains your receiver key can take over
# your public listing.
# Your receiver keys should be placed into this array:
receiver_keys = []
# If you list your receiver on multiple sites, you can place all your keys into the array above, or you can append
# keys to the arraylike this:
# receiver_keys += ["my-receiver-key"]
# If you're not sure, simply copy & paste the code you received from your listing site below this line:
# ==== 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.
)
audio_compression = "adpcm" # valid values: "adpcm", "none"
fft_compression = "adpcm" # valid values: "adpcm", "none"
# Tau setting for WFM (broadcast FM) deemphasis\
# Quote from wikipedia https://en.wikipedia.org/wiki/FM_broadcasting#Pre-emphasis_and_de-emphasis
# "In most of the world a 50 µs time constant is used. In the Americas and South Korea, 75 µs is used"
# Enable one of the following lines, depending on your location:
# wfm_deemphasis_tau = 75e-6 # for US and South Korea
wfm_deemphasis_tau = 50e-6 # for the rest of the world
digimodes_enable = True # Decoding digimodes come with higher CPU usage.
digimodes_fft_size = 2048
# 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
digital_voice_unvoiced_quality = 1
# enables lookup of DMR ids using the radioid api
digital_voice_dmr_id_lookup = True
"""
Note: if you experience audio underruns while CPU usage is 100%, you can:
- decrease `samp_rate`,
- set `fft_voverlap_factor` to 0,
- decrease `fft_fps` and `fft_size`,
- limit the number of users by decreasing `max_clients`.
"""
# ==== I/Q sources ====
# (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.)
###############################################################################
# Is my SDR hardware supported? #
# Check here: https://github.com/jketterl/openwebrx/wiki/Supported-Hardware #
###############################################################################
# Currently supported types of sdr receivers:
# "rtl_sdr", "rtl_sdr_soapy", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr",
# "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote"
#
# In order to use rtl_sdr, you will need to install librtlsdr-dev and the connector.
# In order to use sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the
# connector.
#
# https://github.com/jketterl/owrx_connector
#
# In order to use Perseus HF you need to install the libperseus-sdr
#
# https://github.com/Microtelecom/libperseus-sdr
#
# and do the proper changes to the sdrs object below
# (see also Wiki in https://github.com/jketterl/openwebrx/wiki/Sample-configuration-for-Perseus-HF-receiver).
#
sdrs = {
"rtlsdr": {
"name": "RTL-SDR USB Stick",
"type": "rtl_sdr",
"ppm": 0,
# you can change this if you use an upconverter. formula is:
# center_freq + lfo_offset = actual frequency on the sdr
# "lfo_offset": 0,
"profiles": {
"70cm": {
"name": "70cm Relais",
"center_freq": 438800000,
"rf_gain": 29,
"samp_rate": 2400000,
"start_freq": 439275000,
"start_mod": "nfm",
},
"2m": {
"name": "2m komplett",
"center_freq": 145000000,
"rf_gain": 29,
"samp_rate": 2048000,
"start_freq": 145725000,
"start_mod": "nfm",
},
},
},
"airspy": {
"name": "Airspy HF+",
"type": "airspyhf",
"ppm": 0,
"rf_gain": "auto",
"profiles": {
"20m": {
"name": "20m",
"center_freq": 14150000,
"samp_rate": 384000,
"start_freq": 14070000,
"start_mod": "usb",
},
"30m": {
"name": "30m",
"center_freq": 10125000,
"samp_rate": 192000,
"start_freq": 10142000,
"start_mod": "usb",
},
"40m": {
"name": "40m",
"center_freq": 7100000,
"samp_rate": 256000,
"start_freq": 7070000,
"start_mod": "lsb",
},
"80m": {
"name": "80m",
"center_freq": 3650000,
"samp_rate": 384000,
"start_freq": 3570000,
"start_mod": "lsb",
},
"49m": {
"name": "49m Broadcast",
"center_freq": 6050000,
"samp_rate": 384000,
"start_freq": 6070000,
"start_mod": "am",
},
},
},
"sdrplay": {
"name": "SDRPlay RSP2",
"type": "sdrplay",
"ppm": 0,
"antenna": "Antenna A",
"profiles": {
"20m": {
"name": "20m",
"center_freq": 14150000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 14070000,
"start_mod": "usb",
},
"30m": {
"name": "30m",
"center_freq": 10125000,
"rf_gain": 0,
"samp_rate": 250000,
"start_freq": 10142000,
"start_mod": "usb",
},
"40m": {
"name": "40m",
"center_freq": 7100000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 7070000,
"start_mod": "lsb",
},
"80m": {
"name": "80m",
"center_freq": 3650000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 3570000,
"start_mod": "lsb",
},
"49m": {
"name": "49m Broadcast",
"center_freq": 6000000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 6070000,
"start_mod": "am",
},
},
},
}
# ==== Color themes ====
### google turbo colormap (see: https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html)
waterfall_colors = [0x30123b, 0x311542, 0x33184a, 0x341b51, 0x351e58, 0x36215f, 0x372466, 0x38266c, 0x392973, 0x3a2c79, 0x3b2f80, 0x3c3286, 0x3d358b, 0x3e3891, 0x3e3a97, 0x3f3d9c, 0x4040a2, 0x4043a7, 0x4146ac, 0x4248b1, 0x424bb6, 0x434eba, 0x4351bf, 0x4453c3, 0x4456c7, 0x4559cb, 0x455bcf, 0x455ed3, 0x4561d7, 0x4663da, 0x4666dd, 0x4669e1, 0x466be4, 0x466ee7, 0x4671e9, 0x4673ec, 0x4676ee, 0x4678f1, 0x467bf3, 0x467df5, 0x4680f7, 0x4682f9, 0x4685fa, 0x4587fc, 0x458afd, 0x448cfe, 0x448ffe, 0x4391ff, 0x4294ff, 0x4196ff, 0x3f99ff, 0x3e9bff, 0x3d9efe, 0x3ba1fd, 0x3aa3fd, 0x38a6fb, 0x36a8fa, 0x35abf9, 0x33adf7, 0x31b0f6, 0x2fb2f4, 0x2db5f2, 0x2cb7f0, 0x2ab9ee, 0x28bcec, 0x26beea, 0x25c0e7, 0x23c3e5, 0x21c5e2, 0x20c7e0, 0x1fc9dd, 0x1dccdb, 0x1cced8, 0x1bd0d5, 0x1ad2d3, 0x19d4d0, 0x18d6cd, 0x18d8cb, 0x18dac8, 0x17dbc5, 0x17ddc3, 0x17dfc0, 0x18e0be, 0x18e2bb, 0x19e3b9, 0x1ae5b7, 0x1be6b4, 0x1de8b2, 0x1ee9af, 0x20eaad, 0x22ecaa, 0x24eda7, 0x27eea4, 0x29efa1, 0x2cf09e, 0x2ff19b, 0x32f298, 0x35f394, 0x38f491, 0x3cf58e, 0x3ff68b, 0x43f787, 0x46f884, 0x4af980, 0x4efa7d, 0x51fa79, 0x55fb76, 0x59fc73, 0x5dfc6f, 0x61fd6c, 0x65fd69, 0x69fe65, 0x6dfe62, 0x71fe5f, 0x75ff5c, 0x79ff59, 0x7dff56, 0x80ff53, 0x84ff50, 0x88ff4e, 0x8bff4b, 0x8fff49, 0x92ff46, 0x96ff44, 0x99ff42, 0x9cfe40, 0x9ffe3e, 0xa2fd3d, 0xa4fd3b, 0xa7fc3a, 0xaafc39, 0xacfb38, 0xaffa37, 0xb1f936, 0xb4f835, 0xb7f835, 0xb9f634, 0xbcf534, 0xbff434, 0xc1f334, 0xc4f233, 0xc6f033, 0xc9ef34, 0xcbee34, 0xceec34, 0xd0eb34, 0xd2e934, 0xd5e835, 0xd7e635, 0xd9e435, 0xdbe236, 0xdde136, 0xe0df37, 0xe2dd37, 0xe4db38, 0xe6d938, 0xe7d738, 0xe9d539, 0xebd339, 0xedd139, 0xeecf3a, 0xf0cd3a, 0xf1cb3a, 0xf3c93a, 0xf4c73a, 0xf5c53a, 0xf7c33a, 0xf8c13a, 0xf9bf39, 0xfabd39, 0xfaba38, 0xfbb838, 0xfcb637, 0xfcb436, 0xfdb135, 0xfdaf35, 0xfeac34, 0xfea933, 0xfea732, 0xfea431, 0xffa12f, 0xff9e2e, 0xff9c2d, 0xff992c, 0xfe962b, 0xfe932a, 0xfe9028, 0xfe8d27, 0xfd8a26, 0xfd8724, 0xfc8423, 0xfc8122, 0xfb7e20, 0xfb7b1f, 0xfa781e, 0xf9751c, 0xf8721b, 0xf86f1a, 0xf76c19, 0xf66917, 0xf56616, 0xf46315, 0xf36014, 0xf25d13, 0xf05b11, 0xef5810, 0xee550f, 0xed530e, 0xeb500e, 0xea4e0d, 0xe94b0c, 0xe7490b, 0xe6470a, 0xe4450a, 0xe34209, 0xe14009, 0xdf3e08, 0xde3c07, 0xdc3a07, 0xda3806, 0xd83606, 0xd63405, 0xd43205, 0xd23105, 0xd02f04, 0xce2d04, 0xcc2b03, 0xca2903, 0xc82803, 0xc62602, 0xc32402, 0xc12302, 0xbf2102, 0xbc1f01, 0xba1e01, 0xb71c01, 0xb41b01, 0xb21901, 0xaf1801, 0xac1601, 0xaa1501, 0xa71401, 0xa41201, 0xa11101, 0x9e1001, 0x9b0f01, 0x980d01, 0x950c01, 0x920b01, 0x8e0a01, 0x8b0901, 0x880801, 0x850701, 0x810602, 0x7e0502, 0x7a0402]
### original theme by teejez:
#waterfall_colors = [0x000000, 0x0000FF, 0x00FFFF, 0x00FF00, 0xFFFF00, 0xFF0000, 0xFF00FF, 0xFFFFFF]
### old theme by HA7ILM:
#waterfall_colors = [0x000000, 0x2e6893, 0x69a5d0, 0x214b69, 0x9dc4e0, 0xfff775, 0xff8a8a, 0xb20000]
# waterfall_min_level = -115 #in dB
# waterfall_max_level = 0
# waterfall_auto_level_margin = {"min": 20, "max": 30}
##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
waterfall_min_level = -88 # in dB
waterfall_max_level = -20
waterfall_auto_level_margin = {"min": 3, "max": 10, "min_range": 50}
# Note: When the auto waterfall level button is clicked, the following happens:
# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin["min"]]
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin["max"]]
#
# ___|________________________________________|____________________________________|________________________________________|___> signal power
# \_waterfall_auto_level_margin["min"]_/ |__ current_min_power_level | \_waterfall_auto_level_margin["max"]_/
# current_max_power_level __|
# === Experimental settings ===
# 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.
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
# decoder queue configuration
# due to the nature of some operating modes (ft8, ft8, jt9, jt65, wspr and js8), the data is recorded for a given amount
# of time (6 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
# to mitigate this, the recordings will be queued and processed in sequence.
# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
decoding_queue_workers = 2
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up
# if you are running background services, make sure this number is high enough to accept the task influx during peaks
# i.e. this should be higher than the number of decoding services running at the same time
decoding_queue_length = 10
# wsjt decoding depth will allow more results, but will also consume more cpu
wsjt_decoding_depth = 3
# can also be set for each mode separately
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
wsjt_decoding_depths = {"jt65": 1}
# JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled.
js8_enabled_profiles = ["normal", "slow"]
# JS8 decoding depth; higher value will get more results, but will also consume more cpu
js8_decoding_depth = 3
temporary_directory = "/tmp"
services_enabled = False
services_decoders = ["ft8", "ft4", "wspr", "packet"]
# === aprs igate settings ===
# if you want to share your APRS decodes with the aprs network, configure these settings accordingly
aprs_callsign = "N0CALL"
aprs_igate_enabled = False
aprs_igate_server = "euro.aprs2.net"
aprs_igate_password = ""
# beacon uses the receiver_gps setting, so if you enable this, make sure the location is correct there
aprs_igate_beacon = False
# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols)
aprs_symbols_path = "/opt/aprs-symbols/png"
# === PSK Reporter setting ===
# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator
pskreporter_enabled = False
pskreporter_callsign = "N0CALL"
# === Web admin settings ===
# this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote
# changes to the receiver settings. enable for testing in controlled environment only.
# webadmin_enabled = False

View File

View File

@ -1,142 +0,0 @@
from csdr.module import Module
from pycsdr.modules import Buffer
from pycsdr.types import Format
from typing import Union, Callable, Optional
class Chain(Module):
def __init__(self, workers):
super().__init__()
self.workers = workers
for i in range(1, len(self.workers)):
self._connect(self.workers[i - 1], self.workers[i])
def empty(self):
return not self.workers
def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None:
if buffer is None:
buffer = Buffer(w1.getOutputFormat())
w1.setWriter(buffer)
w2.setReader(buffer.getReader())
def setReader(self, reader):
if self.reader is reader:
return
super().setReader(reader)
if self.workers:
self.workers[0].setReader(reader)
def setWriter(self, writer):
if self.writer is writer:
return
super().setWriter(writer)
if self.workers:
self.workers[-1].setWriter(writer)
def indexOf(self, search: Union[Callable, object]) -> int:
def searchFn(x):
if callable(search):
return search(x)
else:
return x is search
try:
return next(i for i, v in enumerate(self.workers) if searchFn(v))
except StopIteration:
return -1
def replace(self, index, newWorker):
if index >= len(self.workers):
raise IndexError("Index {} does not exist".format(index))
self.workers[index].stop()
self.workers[index] = newWorker
error = None
if index == 0:
if self.reader is not None:
newWorker.setReader(self.reader)
else:
try:
previousWorker = self.workers[index - 1]
self._connect(previousWorker, newWorker)
except ValueError as e:
# store error for later raising, but still attempt the second connection
error = e
if index == len(self.workers) - 1:
if self.writer is not None:
newWorker.setWriter(self.writer)
else:
try:
nextWorker = self.workers[index + 1]
self._connect(newWorker, nextWorker)
except ValueError as e:
error = e
if error is not None:
raise error
def append(self, newWorker):
previousWorker = None
if self.workers:
previousWorker = self.workers[-1]
self.workers.append(newWorker)
if previousWorker:
self._connect(previousWorker, newWorker)
elif self.reader is not None:
newWorker.setReader(self.reader)
if self.writer is not None:
newWorker.setWriter(self.writer)
def insert(self, newWorker):
nextWorker = None
if self.workers:
nextWorker = self.workers[0]
self.workers.insert(0, newWorker)
if nextWorker:
self._connect(newWorker, nextWorker)
elif self.writer is not None:
newWorker.setWriter(self.writer)
if self.reader is not None:
newWorker.setReader(self.reader)
def remove(self, index):
removedWorker = self.workers[index]
self.workers.remove(removedWorker)
removedWorker.stop()
if index == 0:
if self.reader is not None and len(self.workers):
self.workers[0].setReader(self.reader)
elif index == len(self.workers):
if self.writer is not None:
self.workers[-1].setWriter(self.writer)
else:
previousWorker = self.workers[index - 1]
nextWorker = self.workers[index]
self._connect(previousWorker, nextWorker)
def stop(self):
for w in self.workers:
w.stop()
def getInputFormat(self) -> Format:
if self.workers:
return self.workers[0].getInputFormat()
else:
raise BufferError("getInputFormat on empty chain")
def getOutputFormat(self) -> Format:
if self.workers:
return self.workers[-1].getOutputFormat()
else:
raise BufferError("getOutputFormat on empty chain")

View File

@ -1,76 +0,0 @@
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, HdAudio, DeemphasisTauChain
from pycsdr.modules import AmDemod, DcBlock, FmDemod, Limit, NfmDeemphasis, Agc, WfmDeemphasis, FractionalDecimator, RealPart
from pycsdr.types import Format, AgcProfile
class Am(BaseDemodulatorChain):
def __init__(self):
agc = Agc(Format.FLOAT)
agc.setProfile(AgcProfile.SLOW)
agc.setInitialGain(200)
workers = [
AmDemod(),
DcBlock(),
agc,
]
super().__init__(workers)
class NFm(BaseDemodulatorChain):
def __init__(self, sampleRate: int):
self.sampleRate = sampleRate
agc = Agc(Format.FLOAT)
agc.setProfile(AgcProfile.SLOW)
agc.setMaxGain(3)
workers = [
FmDemod(),
Limit(),
NfmDeemphasis(sampleRate),
agc,
]
super().__init__(workers)
def setSampleRate(self, sampleRate: int) -> None:
if sampleRate == self.sampleRate:
return
self.sampleRate = sampleRate
self.replace(2, NfmDeemphasis(sampleRate))
class WFm(BaseDemodulatorChain, FixedIfSampleRateChain, DeemphasisTauChain, HdAudio):
def __init__(self, sampleRate: int, tau: float):
self.sampleRate = sampleRate
self.tau = tau
workers = [
FmDemod(),
Limit(),
FractionalDecimator(Format.FLOAT, 200000.0 / self.sampleRate, prefilter=True),
WfmDeemphasis(self.sampleRate, self.tau),
]
super().__init__(workers)
def getFixedIfSampleRate(self):
return 200000
def setDeemphasisTau(self, tau: float) -> None:
if tau == self.tau:
return
self.tau = tau
self.replace(3, WfmDeemphasis(self.sampleRate, self.tau))
def setSampleRate(self, sampleRate: int) -> None:
if sampleRate == self.sampleRate:
return
self.sampleRate = sampleRate
self.replace(2, FractionalDecimator(Format.FLOAT, 200000.0 / self.sampleRate, prefilter=True))
self.replace(3, WfmDeemphasis(self.sampleRate, self.tau))
class Ssb(BaseDemodulatorChain):
def __init__(self):
workers = [
RealPart(),
Agc(Format.FLOAT),
]
super().__init__(workers)

View File

@ -1,72 +0,0 @@
from csdr.chain import Chain
from pycsdr.modules import AudioResampler, Convert, AdpcmEncoder, Limit
from pycsdr.types import Format
class Converter(Chain):
def __init__(self, format: Format, inputRate: int, clientRate: int):
workers = []
if inputRate != clientRate:
# we only have an audio resampler for float ATM so if we need to resample, we need to convert
if format != Format.FLOAT:
workers += [Convert(format, Format.FLOAT)]
workers += [AudioResampler(inputRate, clientRate), Limit(), Convert(Format.FLOAT, Format.SHORT)]
elif format != Format.SHORT:
workers += [Convert(format, Format.SHORT)]
super().__init__(workers)
class ClientAudioChain(Chain):
def __init__(self, format: Format, inputRate: int, clientRate: int, compression: str):
self.format = format
self.inputRate = inputRate
self.clientRate = clientRate
workers = []
converter = self._buildConverter()
if not converter.empty():
workers += [converter]
if compression == "adpcm":
workers += [AdpcmEncoder(sync=True)]
super().__init__(workers)
def _buildConverter(self):
return Converter(self.format, self.inputRate, self.clientRate)
def _updateConverter(self):
converter = self._buildConverter()
index = self.indexOf(lambda x: isinstance(x, Converter))
if converter.empty():
if index >= 0:
self.remove(index)
else:
if index >= 0:
self.replace(index, converter)
else:
self.insert(converter)
def setFormat(self, format: Format) -> None:
if format == self.format:
return
self.format = format
self._updateConverter()
def setInputRate(self, inputRate: int) -> None:
if inputRate == self.inputRate:
return
self.inputRate = inputRate
self._updateConverter()
def setClientRate(self, clientRate: int) -> None:
if clientRate == self.clientRate:
return
self.clientRate = clientRate
self._updateConverter()
def setAudioCompression(self, compression: str) -> None:
index = self.indexOf(lambda x: isinstance(x, AdpcmEncoder))
if compression == "adpcm":
if index < 0:
self.append(AdpcmEncoder(sync=True))
else:
if index >= 0:
self.remove(index)

View File

@ -1,73 +0,0 @@
from csdr.chain import Chain
from abc import ABC, ABCMeta, abstractmethod
from pycsdr.modules import Writer
class FixedAudioRateChain(ABC):
@abstractmethod
def getFixedAudioRate(self) -> int:
pass
class FixedIfSampleRateChain(ABC):
@abstractmethod
def getFixedIfSampleRate(self) -> int:
pass
class DialFrequencyReceiver(ABC):
@abstractmethod
def setDialFrequency(self, frequency: int) -> None:
pass
# marker interface
class HdAudio:
pass
class MetaProvider(ABC):
@abstractmethod
def setMetaWriter(self, writer: Writer) -> None:
pass
class SlotFilterChain(ABC):
@abstractmethod
def setSlotFilter(self, filter: int) -> None:
pass
class SecondarySelectorChain(ABC):
def getBandwidth(self) -> float:
pass
class DeemphasisTauChain(ABC):
@abstractmethod
def setDeemphasisTau(self, tau: float) -> None:
pass
class BaseDemodulatorChain(Chain):
def supportsSquelch(self) -> bool:
return True
def setSampleRate(self, sampleRate: int) -> None:
pass
class SecondaryDemodulator(Chain):
def supportsSquelch(self) -> bool:
return True
def setSampleRate(self, sampleRate: int) -> None:
pass
class ServiceDemodulator(SecondaryDemodulator, FixedAudioRateChain, metaclass=ABCMeta):
pass
class DemodulatorError(Exception):
pass

View File

@ -1,133 +0,0 @@
from csdr.chain.demodulator import BaseDemodulatorChain, FixedAudioRateChain, FixedIfSampleRateChain, DialFrequencyReceiver, MetaProvider, SlotFilterChain, DemodulatorError, ServiceDemodulator
from pycsdr.modules import FmDemod, Agc, Writer, Buffer
from pycsdr.types import Format
from digiham.modules import DstarDecoder, DcBlock, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder, PocsagDecoder
from digiham.ambe import Modes, ServerError
from owrx.meta import MetaParser
from owrx.pocsag import PocsagParser
class DigihamChain(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, DialFrequencyReceiver, MetaProvider):
def __init__(self, fskDemodulator, decoder, mbeMode, filter=None, codecserver: str = ""):
self.decoder = decoder
if codecserver is None:
codecserver = ""
agc = Agc(Format.SHORT)
agc.setMaxGain(30)
agc.setInitialGain(3)
workers = [FmDemod(), DcBlock()]
if filter is not None:
workers += [filter]
try:
mbeSynthesizer = MbeSynthesizer(mbeMode, codecserver)
except ConnectionError as ce:
raise DemodulatorError("Connection to codecserver failed: {}".format(ce))
except ServerError as se:
raise DemodulatorError("Codecserver error: {}".format(se))
workers += [
fskDemodulator,
decoder,
mbeSynthesizer,
DigitalVoiceFilter(),
agc
]
self.metaParser = None
self.dialFrequency = None
super().__init__(workers)
def getFixedIfSampleRate(self):
return 48000
def getFixedAudioRate(self):
return 8000
def setMetaWriter(self, writer: Writer) -> None:
if self.metaParser is None:
self.metaParser = MetaParser()
buffer = Buffer(Format.CHAR)
self.decoder.setMetaWriter(buffer)
self.metaParser.setReader(buffer.getReader())
if self.dialFrequency is not None:
self.metaParser.setDialFrequency(self.dialFrequency)
self.metaParser.setWriter(writer)
def supportsSquelch(self):
return False
def setDialFrequency(self, frequency: int) -> None:
self.dialFrequency = frequency
if self.metaParser is None:
return
self.metaParser.setDialFrequency(frequency)
def stop(self):
if self.metaParser is not None:
self.metaParser.stop()
super().stop()
class Dstar(DigihamChain):
def __init__(self, codecserver: str = ""):
super().__init__(
fskDemodulator=FskDemodulator(samplesPerSymbol=10),
decoder=DstarDecoder(),
mbeMode=Modes.DStarMode,
codecserver=codecserver
)
class Nxdn(DigihamChain):
def __init__(self, codecserver: str = ""):
super().__init__(
fskDemodulator=GfskDemodulator(samplesPerSymbol=20),
decoder=NxdnDecoder(),
mbeMode=Modes.NxdnMode,
filter=NarrowRrcFilter(),
codecserver=codecserver
)
class Dmr(DigihamChain, SlotFilterChain):
def __init__(self, codecserver: str = ""):
super().__init__(
fskDemodulator=GfskDemodulator(samplesPerSymbol=10),
decoder=DmrDecoder(),
mbeMode=Modes.DmrMode,
filter=WideRrcFilter(),
codecserver=codecserver,
)
def setSlotFilter(self, slotFilter: int) -> None:
self.decoder.setSlotFilter(slotFilter)
class Ysf(DigihamChain):
def __init__(self, codecserver: str = ""):
super().__init__(
fskDemodulator=GfskDemodulator(samplesPerSymbol=10),
decoder=YsfDecoder(),
mbeMode=Modes.YsfMode,
filter=WideRrcFilter(),
codecserver=codecserver
)
class PocsagDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self):
self.parser = PocsagParser()
workers = [
FmDemod(),
FskDemodulator(samplesPerSymbol=40, invert=True),
PocsagDecoder(),
self.parser,
]
super().__init__(workers)
def supportsSquelch(self) -> bool:
return False
def getFixedAudioRate(self) -> int:
return 48000
def setDialFrequency(self, frequency: int) -> None:
self.parser.setDialFrequency(frequency)

View File

@ -1,86 +0,0 @@
from csdr.chain.demodulator import ServiceDemodulator, SecondaryDemodulator, DialFrequencyReceiver, SecondarySelectorChain
from csdr.module.msk144 import Msk144Module, ParserAdapter
from owrx.audio.chopper import AudioChopper, AudioChopperParser
from owrx.aprs.kiss import KissDeframer
from owrx.aprs import Ax25Parser, AprsParser
from pycsdr.modules import Convert, FmDemod, Agc, TimingRecovery, DBPskDecoder, VaricodeDecoder
from pycsdr.types import Format
from owrx.aprs.module import DirewolfModule
class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self, mode: str, parser: AudioChopperParser):
self.chopper = AudioChopper(mode, parser)
workers = [Convert(Format.FLOAT, Format.SHORT), self.chopper]
super().__init__(workers)
def getFixedAudioRate(self):
return 12000
def setDialFrequency(self, frequency: int) -> None:
self.chopper.setDialFrequency(frequency)
class Msk144Demodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self):
self.parser = ParserAdapter()
workers = [
Convert(Format.FLOAT, Format.SHORT),
Msk144Module(),
self.parser,
]
super().__init__(workers)
def getFixedAudioRate(self) -> int:
return 12000
def setDialFrequency(self, frequency: int) -> None:
self.parser.setDialFrequency(frequency)
class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self, service: bool = False):
self.parser = AprsParser()
workers = [
FmDemod(),
Convert(Format.FLOAT, Format.SHORT),
DirewolfModule(service=service),
KissDeframer(),
Ax25Parser(),
self.parser,
]
super().__init__(workers)
def supportsSquelch(self) -> bool:
return False
def getFixedAudioRate(self) -> int:
return 48000
def setDialFrequency(self, frequency: int) -> None:
self.parser.setDialFrequency(frequency)
class PskDemodulator(SecondaryDemodulator, SecondarySelectorChain):
def __init__(self, baudRate: float):
self.baudRate = baudRate
# this is an assumption, we will adjust in setSampleRate
self.sampleRate = 12000
secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3
workers = [
Agc(Format.COMPLEX_FLOAT),
TimingRecovery(secondary_samples_per_bits, 0.5, 2, useQ=True),
DBPskDecoder(),
VaricodeDecoder(),
]
super().__init__(workers)
def getBandwidth(self):
return self.baudRate
def setSampleRate(self, sampleRate: int) -> None:
if sampleRate == self.sampleRate:
return
self.sampleRate = sampleRate
secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3
self.replace(1, TimingRecovery(secondary_samples_per_bits, 0.5, 2, useQ=True))

View File

@ -1,19 +0,0 @@
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain
from pycsdr.modules import Convert, Downmix
from pycsdr.types import Format
from csdr.module.drm import DrmModule
class Drm(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain):
def __init__(self):
workers = [Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT), DrmModule(), Downmix()]
super().__init__(workers)
def supportsSquelch(self) -> bool:
return False
def getFixedIfSampleRate(self) -> int:
return 48000
def getFixedAudioRate(self) -> int:
return 48000

View File

@ -1,14 +0,0 @@
from pycsdr.types import Format
from csdr.chain import Module
class DummyDemodulator(Module):
def __init__(self, outputFormat: Format):
self.outputFormat = outputFormat
super().__init__()
def getInputFormat(self) -> Format:
return Format.COMPLEX_FLOAT
def getOutputFormat(self) -> Format:
return self.outputFormat

View File

@ -1,96 +0,0 @@
from csdr.chain import Chain
from pycsdr.modules import Fft, LogPower, LogAveragePower, FftSwap, FftAdpcm
class FftAverager(Chain):
def __init__(self, fft_size, fft_averages):
self.fftSize = fft_size
self.fftAverages = fft_averages
workers = [self._getWorker()]
super().__init__(workers)
def setFftAverages(self, fft_averages):
if self.fftAverages == fft_averages:
return
self.fftAverages = fft_averages
self.replace(0, self._getWorker())
def _getWorker(self):
if self.fftAverages == 0:
return LogPower(add_db=-70)
else:
return LogAveragePower(add_db=-70, fft_size=self.fftSize, avg_number=self.fftAverages)
class FftChain(Chain):
def __init__(self, samp_rate, fft_size, fft_v_overlap_factor, fft_fps, fft_compression):
self.sampleRate = samp_rate
self.vOverlapFactor = fft_v_overlap_factor
self.fps = fft_fps
self.size = fft_size
self.blockSize = 0
self.fft = Fft(size=self.size, every_n_samples=self.blockSize)
self.averager = FftAverager(fft_size=self.size, fft_averages=10)
self.fftExchangeSides = FftSwap(fft_size=self.size)
workers = [
self.fft,
self.averager,
self.fftExchangeSides,
]
self.compressFftAdpcm = None
if fft_compression == "adpcm":
self.compressFftAdpcm = FftAdpcm(fft_size=self.size)
workers += [self.compressFftAdpcm]
self._updateParameters()
super().__init__(workers)
def _setBlockSize(self, fft_block_size):
if self.blockSize == int(fft_block_size):
return
self.blockSize = int(fft_block_size)
self.fft.setEveryNSamples(self.blockSize)
def setVOverlapFactor(self, fft_v_overlap_factor):
if self.vOverlapFactor == fft_v_overlap_factor:
return
self.vOverlapFactor = fft_v_overlap_factor
self._updateParameters()
def setFps(self, fft_fps):
if self.fps == fft_fps:
return
self.fps = fft_fps
self._updateParameters()
def setSampleRate(self, samp_rate):
if self.sampleRate == samp_rate:
return
self.sampleRate = samp_rate
self._updateParameters()
def _updateParameters(self):
fftAverages = 0
if self.vOverlapFactor > 0:
fftAverages = int(round(1.0 * self.sampleRate / self.size / self.fps / (1.0 - self.vOverlapFactor)))
self.averager.setFftAverages(fftAverages)
if fftAverages == 0:
self._setBlockSize(self.sampleRate / self.fps)
else:
self._setBlockSize(self.sampleRate / self.fps / fftAverages)
def setCompression(self, compression: str) -> None:
if compression == "adpcm" and not self.compressFftAdpcm:
self.compressFftAdpcm = FftAdpcm(self.size)
# should always be at the end
self.append(self.compressFftAdpcm)
elif compression == "none" and self.compressFftAdpcm:
self.compressFftAdpcm.stop()
self.compressFftAdpcm = None
# should always be at that position (right?)
self.remove(3)

View File

@ -1,28 +0,0 @@
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain
from csdr.module.freedv import FreeDVModule
from pycsdr.modules import RealPart, Agc, Convert
from pycsdr.types import Format
class FreeDV(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain):
def __init__(self):
agc = Agc(Format.SHORT)
agc.setMaxGain(30)
agc.setInitialGain(3)
workers = [
RealPart(),
Agc(Format.FLOAT),
Convert(Format.FLOAT, Format.SHORT),
FreeDVModule(),
agc,
]
super().__init__(workers)
def getFixedIfSampleRate(self) -> int:
return 8000
def getFixedAudioRate(self) -> int:
return 8000
def supportsSquelch(self) -> bool:
return False

View File

@ -1,30 +0,0 @@
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider
from csdr.module.m17 import M17Module
from pycsdr.modules import FmDemod, Limit, Convert, Writer
from pycsdr.types import Format
from digiham.modules import DcBlock
class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider):
def __init__(self):
self.module = M17Module()
workers = [
FmDemod(),
DcBlock(),
Limit(),
Convert(Format.FLOAT, Format.SHORT),
self.module,
]
super().__init__(workers)
def getFixedIfSampleRate(self) -> int:
return 48000
def getFixedAudioRate(self) -> int:
return 8000
def supportsSquelch(self) -> bool:
return False
def setMetaWriter(self, writer: Writer) -> None:
self.module.setMetaWriter(writer)

View File

@ -1,160 +0,0 @@
from csdr.chain import Chain
from pycsdr.modules import Shift, FirDecimate, Bandpass, Squelch, FractionalDecimator, Writer
from pycsdr.types import Format
import math
class Decimator(Chain):
def __init__(self, inputRate: int, outputRate: int):
if outputRate > inputRate:
raise ValueError("impossible decimation: cannot upsample {} to {}".format(inputRate, outputRate))
self.inputRate = inputRate
self.outputRate = outputRate
decimation, fraction = self._getDecimation(outputRate)
transition = 0.15 * (outputRate / float(self.inputRate))
# set the cutoff on the fist decimation stage lower so that the resulting output
# is already prepared for the second (fractional) decimation stage.
# this spares us a second filter.
cutoff = 0.5 * decimation / (self.inputRate / outputRate)
workers = [
FirDecimate(decimation, transition, cutoff),
]
if fraction != 1.0:
workers += [FractionalDecimator(Format.COMPLEX_FLOAT, fraction)]
super().__init__(workers)
def _getDecimation(self, outputRate: int) -> (int, float):
d = self.inputRate / outputRate
dInt = int(d)
dFloat = float(self.inputRate / dInt) / outputRate
return dInt, dFloat
def _reconfigure(self):
decimation, fraction = self._getDecimation(self.outputRate)
transition = 0.15 * (self.outputRate / float(self.inputRate))
cutoff = 0.5 * decimation / (self.inputRate / self.outputRate)
self.replace(0, FirDecimate(decimation, transition, cutoff))
index = self.indexOf(lambda x: isinstance(x, FractionalDecimator))
if fraction != 1.0:
decimator = FractionalDecimator(Format.COMPLEX_FLOAT, fraction)
if index >= 0:
self.replace(index, decimator)
else:
self.append(decimator)
elif index >= 0:
self.remove(index)
def setOutputRate(self, outputRate: int) -> None:
if outputRate == self.outputRate:
return
self.outputRate = outputRate
self._reconfigure()
def setInputRate(self, inputRate: int) -> None:
if inputRate == self.inputRate:
return
self.inputRate = inputRate
self._reconfigure()
class Selector(Chain):
def __init__(self, inputRate: int, outputRate: int, withSquelch: bool = True):
self.inputRate = inputRate
self.outputRate = outputRate
self.frequencyOffset = 0
self.shift = Shift(0.0)
self.decimation = Decimator(inputRate, outputRate)
self.bandpass = self._buildBandpass()
self.bandpassCutoffs = None
self.setBandpass(-4000, 4000)
workers = [self.shift, self.decimation, self.bandpass]
if withSquelch:
self.readings_per_second = 4
# s-meter readings are available every 1024 samples
# the reporting interval is measured in those 1024-sample blocks
self.squelch = Squelch(5, int(outputRate / (self.readings_per_second * 1024)))
workers += [self.squelch]
super().__init__(workers)
def _buildBandpass(self) -> Bandpass:
bp_transition = 320.0 / self.outputRate
return Bandpass(transition=bp_transition, use_fft=True)
def setFrequencyOffset(self, offset: int) -> None:
if offset == self.frequencyOffset:
return
self.frequencyOffset = offset
self._updateShift()
def _updateShift(self):
shift = -self.frequencyOffset / self.inputRate
self.shift.setRate(shift)
def _convertToLinear(self, db: float) -> float:
return float(math.pow(10, db / 10))
def setSquelchLevel(self, level: float) -> None:
self.squelch.setSquelchLevel(self._convertToLinear(level))
def setBandpass(self, lowCut: float, highCut: float) -> None:
self.bandpassCutoffs = [lowCut, highCut]
scaled = [x / self.outputRate for x in self.bandpassCutoffs]
self.bandpass.setBandpass(*scaled)
def setLowCut(self, lowCut: float) -> None:
self.bandpassCutoffs[0] = lowCut
self.setBandpass(*self.bandpassCutoffs)
def setHighCut(self, highCut: float) -> None:
self.bandpassCutoffs[1] = highCut
self.setBandpass(*self.bandpassCutoffs)
def setPowerWriter(self, writer: Writer) -> None:
self.squelch.setPowerWriter(writer)
def setOutputRate(self, outputRate: int) -> None:
if outputRate == self.outputRate:
return
self.outputRate = outputRate
self.decimation.setOutputRate(outputRate)
self.squelch.setReportInterval(int(outputRate / (self.readings_per_second * 1024)))
self.bandpass = self._buildBandpass()
self.setBandpass(*self.bandpassCutoffs)
self.replace(2, self.bandpass)
def setInputRate(self, inputRate: int) -> None:
if inputRate == self.inputRate:
return
self.inputRate = inputRate
self.decimation.setInputRate(inputRate)
self._updateShift()
class SecondarySelector(Chain):
def __init__(self, sampleRate: int, bandwidth: float):
self.sampleRate = sampleRate
self.frequencyOffset = 0
self.shift = Shift(0.0)
cutoffRate = bandwidth / sampleRate
self.bandpass = Bandpass(-cutoffRate, cutoffRate, cutoffRate, use_fft=True)
workers = [self.shift, self.bandpass]
super().__init__(workers)
def setFrequencyOffset(self, offset: int) -> None:
if offset == self.frequencyOffset:
return
self.frequencyOffset = offset
if self.frequencyOffset is None:
return
self.shift.setRate(-offset / self.sampleRate)

874
csdr/csdr.py Normal file
View File

@ -0,0 +1,874 @@
"""
OpenWebRX csdr plugin: do the signal processing with csdr
This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import subprocess
import os
import signal
import threading
import math
from functools import partial
from owrx.kiss import KissClient, DirewolfConfig
from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile
from owrx.js8 import Js8Profiles
from owrx.audio import AudioChopper
from csdr.pipe import Pipe
import logging
logger = logging.getLogger(__name__)
class output(object):
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), name="csdr_pump_thread").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 = None
try:
data = read()
except ValueError:
pass
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
self.hd_output_rate = 44100
self.fft_size = 1024
self.fft_fps = 5
self.center_freq = 0
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.running = False
self.secondary_processes_running = False
self.audio_compression = "none"
self.fft_compression = "none"
self.demodulator = "nfm"
self.name = "csdr"
self.base_bufsize = 512
self.decimation = None
self.last_decimation = None
self.nc_port = None
self.csdr_dynamic_bufsize = False
self.csdr_print_bufsizes = False
self.csdr_through = False
self.squelch_level = -150
self.fft_averages = 50
self.wfm_deemphasis_tau = 50e-6
self.iqtee = False
self.iqtee2 = False
self.secondary_demodulator = None
self.secondary_fft_size = 1024
self.secondary_process_fft = None
self.secondary_process_demod = None
self.pipe_names = {
"bpf_pipe": Pipe.WRITE,
"shift_pipe": Pipe.WRITE,
"squelch_pipe": Pipe.WRITE,
"smeter_pipe": Pipe.READ,
"meta_pipe": Pipe.READ,
"iqtee_pipe": Pipe.NONE,
"iqtee2_pipe": Pipe.NONE,
"dmr_control_pipe": Pipe.WRITE,
}
self.pipes = {}
self.secondary_pipe_names = {"secondary_shift_pipe": Pipe.WRITE}
self.secondary_offset_freq = 1000
self.unvoiced_quality = 1
self.modification_lock = threading.Lock()
self.output = output
self.temporary_directory = None
self.pipe_base_path = None
self.set_temporary_directory("/tmp")
self.is_service = False
self.direwolf_config = None
self.direwolf_port = None
self.process = None
def set_service(self, flag=True):
self.is_service = flag
def set_temporary_directory(self, what):
self.temporary_directory = what
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_".format(tmp_dir=self.temporary_directory)
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}"]
return chain
chain += ["csdr shift_addfast_cc --fifo {shift_pipe}"]
if self.decimation > 1:
chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"]
chain += ["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:
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 = []
if self.last_decimation >= 2.0:
# activate prefilter if signal has been oversampled, e.g. WFM
last_decimation_block = ["csdr fractional_decimator_ff {last_decimation} 12 --prefilter"]
elif self.last_decimation != 1.0:
last_decimation_block = ["csdr fractional_decimator_ff {last_decimation}"]
if which == "nfm":
chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"]
chain += last_decimation_block
chain += [
"csdr deemphasis_nfm_ff {audio_rate}",
"csdr agc_ff --profile slow --max 3",
]
if 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"]
elif which == "wfm":
chain += [
"csdr fmdemod_quadri_cf",
"csdr limit_ff",
]
chain += last_decimation_block
chain += [
"csdr deemphasis_wfm_ff {audio_rate} {wfm_deemphasis_tau}",
"csdr convert_f_s16"
]
elif self.isDigitalVoice(which):
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 == "dstar":
chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "]
elif which == "nxdn":
chain += ["dsd -fi -i - -o - -u {unvoiced_quality} -g -1 "]
chain += [
"digitalvoice_filter",
"CSDR_FIXED_BUFSIZE=32 csdr agc_s16 --max 30 --initial 3",
"sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
]
# digiham modes
else:
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}",
]
elif which == "ysf":
chain += ["ysf_decoder --fifo {meta_pipe}", "mbe_synthesizer -y -f -u {unvoiced_quality}"]
max_gain = 0.005
chain += [
"digitalvoice_filter -f",
"CSDR_FIXED_BUFSIZE=32 csdr agc_ff --max 0.005 --initial 0.0005",
"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 == "am":
chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"]
chain += last_decimation_block
chain += [
"csdr agc_ff --profile slow --initial 200",
"csdr convert_f_s16",
]
elif self.isFreeDV(which):
chain += ["csdr realpart_cf"]
chain += last_decimation_block
chain += [
"csdr agc_ff",
"csdr convert_f_s16",
"freedv_rx 1600 - -",
"csdr agc_s16 --max 30 --initial 3",
"sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
]
elif self.isDrm(which):
if self.last_decimation != 1.0:
# we are still dealing with complex samples here, so the regular last_decimation_block doesn't fit
chain += ["csdr fractional_decimator_cc {last_decimation}"]
chain += [
"csdr convert_f_s16",
"dream -c 6 --sigsrate 48000 --audsrate 48000 -I - -O -",
"sox -t raw -r 48000 -e signed-integer -b 16 -c 2 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
]
elif which == "ssb":
chain += ["csdr realpart_cf"]
chain += last_decimation_block
chain += ["csdr agc_ff"]
# fixed sample rate necessary for the wsjt-x tools. fix with sox...
if 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"]
return chain
def secondary_chain(self, which):
chain = ["cat {input_pipe}"]
if which == "fft":
chain += [
"csdr fft_cc {secondary_fft_input_size} {secondary_fft_block_size}",
"csdr logpower_cf -70"
if self.fft_averages == 0
else "csdr logaveragepower_cf -70 {secondary_fft_size} {fft_averages}",
"csdr fft_exchange_sides_ff {secondary_fft_input_size}",
]
if self.fft_compression == "adpcm":
chain += ["csdr compress_fft_adpcm_f_u8 {secondary_fft_size}"]
return chain
elif which == "bpsk31" or which == "bpsk63":
return chain + [
"csdr shift_addfast_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) or self.isJs8(which):
chain += ["csdr realpart_cf"]
if self.last_decimation != 1.0:
chain += ["csdr fractional_decimator_ff {last_decimation}"]
return chain + ["csdr limit_ff", "csdr convert_f_s16"]
elif which == "packet":
chain += ["csdr fmdemod_quadri_cf"]
if self.last_decimation != 1.0:
chain += ["csdr fractional_decimator_ff {last_decimation}"]
return chain + ["csdr convert_f_s16", "direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h 1>&2"]
elif which == "pocsag":
chain += ["csdr fmdemod_quadri_cf"]
if self.last_decimation != 1.0:
chain += ["csdr fractional_decimator_ff {last_decimation}"]
return chain + ["fsk_demodulator -i", "pocsag_decoder"]
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):
base = (self.samp_rate / self.decimation) / (self.fft_fps * 2)
if self.fft_averages == 0:
return base
return base / self.fft_averages
def secondary_decimation(self):
return 1 # currently unused
def secondary_bpf_cutoff(self):
if self.secondary_demodulator == "bpsk31":
return 31.25 / self.if_samp_rate()
elif self.secondary_demodulator == "bpsk63":
return 62.5 / self.if_samp_rate()
return 0
def secondary_bpf_transition_bw(self):
if self.secondary_demodulator == "bpsk31":
return 31.25 / self.if_samp_rate()
elif self.secondary_demodulator == "bpsk63":
return 62.5 / self.if_samp_rate()
return 0
def secondary_samples_per_bits(self):
if self.secondary_demodulator == "bpsk31":
return int(round(self.if_samp_rate() / 31.25)) & ~3
elif self.secondary_demodulator == "bpsk63":
return int(round(self.if_samp_rate() / 62.5)) & ~3
return 0
def secondary_bw(self):
if self.secondary_demodulator == "bpsk31":
return 31.25
elif self.secondary_demodulator == "bpsk63":
return 62.5
def start_secondary_demodulator(self):
if not self.secondary_demodulator:
return
logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate())
secondary_command_demod = " | ".join(self.secondary_chain(self.secondary_demodulator))
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod)
self.try_create_configs(secondary_command_demod)
secondary_command_demod = secondary_command_demod.format(
input_pipe=self.pipes["iqtee2_pipe"],
secondary_shift_pipe=self.pipes["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(),
last_decimation=self.last_decimation,
audio_rate=self.get_audio_rate(),
direwolf_config=self.direwolf_config,
)
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 = " | ".join(self.secondary_chain("fft"))
secondary_command_fft = secondary_command_fft.format(
input_pipe=self.pipes["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(),
fft_averages=self.fft_averages,
)
logger.debug("secondary command (fft) = %s", secondary_command_fft)
self.secondary_process_fft = subprocess.Popen(
secondary_command_fft, stdout=subprocess.PIPE, shell=True, start_new_session=True, env=my_env
)
self.output.send_output(
"secondary_fft",
partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())),
)
# direwolf does not provide any meaningful data on stdout
# more specifically, it doesn't provide any data. if however, for any strange reason, it would start to do so,
# it would block if not read. by piping it to devnull, we avoid a potential pitfall here.
secondary_output = subprocess.DEVNULL if self.isPacket() else subprocess.PIPE
self.secondary_process_demod = subprocess.Popen(
secondary_command_demod, stdout=secondary_output, shell=True, start_new_session=True, env=my_env
)
self.secondary_processes_running = True
if self.isWsjtMode():
smd = self.get_secondary_demodulator()
chopper_profile = None
if smd == "ft8":
chopper_profile = Ft8Profile()
elif smd == "wspr":
chopper_profile = WsprProfile()
elif smd == "jt65":
chopper_profile = Jt65Profile()
elif smd == "jt9":
chopper_profile = Jt9Profile()
elif smd == "ft4":
chopper_profile = Ft4Profile()
if chopper_profile is not None:
chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile)
chopper.start()
self.output.send_output("wsjt_demod", chopper.read)
elif self.isJs8():
chopper = AudioChopper(self, self.secondary_process_demod.stdout, *Js8Profiles.getEnabledProfiles())
chopper.start()
self.output.send_output("js8_demod", chopper.read)
elif self.isPacket():
# we best get the ax25 packets from the kiss socket
kiss = KissClient(self.direwolf_port)
self.output.send_output("packet_demod", kiss.read)
elif self.isPocsag():
self.output.send_output("pocsag_demod", self.secondary_process_demod.stdout.readline)
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.has_pipe("secondary_shift_pipe"): # 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 and self.has_pipe("secondary_shift_pipe"):
self.pipes["secondary_shift_pipe"].write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
def stop_secondary_demodulator(self):
if not self.secondary_processes_running:
return
self.try_delete_pipes(self.secondary_pipe_names)
self.try_delete_configs()
if self.secondary_process_fft:
try:
os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
# drain any leftover data to free file descriptors
self.secondary_process_fft.communicate()
self.secondary_process_fft = None
except ProcessLookupError:
# been killed by something else, ignore
pass
if self.secondary_process_demod:
try:
os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
# drain any leftover data to free file descriptors
self.secondary_process_demod.communicate()
self.secondary_process_demod = None
except ProcessLookupError:
# been killed by something else, ignore
pass
self.secondary_processes_running = False
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_audio_compression(self, what):
self.audio_compression = what
def get_audio_bytes_to_read(self):
# desired latency: 5ms
# uncompressed audio has 16 bits = 2 bytes per sample
base = self.output_rate * 0.005 * 2
# adpcm compresses the bitstream by 4
if self.audio_compression == "adpcm":
base = base / 4
return int(base)
def set_fft_compression(self, what):
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 int((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)
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.calculate_decimation()
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
correction = 1
# wideband fm has a much higher frequency deviation (75kHz).
# we cannot cover this if we immediately decimate to the sample rate the audio will have later on, so we need
# to compensate here.
# the factor of 5 is by experimentation only, with a minimum audio rate of 36kHz (enforced by the client)
# this allows us to cover at least +/- 80kHz of frequency spectrum (may be higher, but that's the worst case).
# the correction factor is automatically compensated for by the secondary decimation stage, which comes
# after the demodulator.
if self.get_demodulator() == "wfm":
correction = 5
while input_rate / (decimation + 1) >= output_rate * correction:
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
def get_name(self):
return self.name
def get_output_rate(self):
return self.output_rate
def get_hd_output_rate(self):
return self.hd_output_rate
def get_audio_rate(self):
if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isDrm():
return 48000
elif self.isWsjtMode() or self.isJs8():
return 12000
elif self.isFreeDV():
return 8000
elif self.isHdAudio():
return self.get_hd_output_rate()
return self.get_output_rate()
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 isJs8(self, demodulator = None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator == "js8"
def isPacket(self, demodulator=None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator == "packet"
def isPocsag(self, demodulator=None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator == "pocsag"
def isFreeDV(self, demodulator=None):
if demodulator is None:
demodulator = self.get_demodulator()
return demodulator == "freedv"
def isHdAudio(self, demodulator=None):
if demodulator is None:
demodulator = self.get_demodulator()
return demodulator == "wfm"
def isDrm(self, demodulator=None):
if demodulator is None:
demodulator = self.get_demodulator()
return demodulator == "drm"
def set_output_rate(self, output_rate):
if self.output_rate == output_rate:
return
self.output_rate = output_rate
self.calculate_decimation()
self.restart()
def set_hd_output_rate(self, hd_output_rate):
if self.hd_output_rate == hd_output_rate:
return
self.hd_output_rate = hd_output_rate
self.calculate_decimation()
self.restart()
def set_demodulator(self, demodulator):
if demodulator in ["usb", "lsb", "cw"]:
demodulator = "ssb"
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
self.restart()
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
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
def set_offset_freq(self, offset_freq):
if offset_freq is None:
return
self.offset_freq = offset_freq
if self.running:
self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
def set_center_freq(self, center_freq):
# dsp only needs to know this to be able to pass it to decoders in the form of get_operating_freq()
self.center_freq = center_freq
def get_operating_freq(self):
return self.center_freq + self.offset_freq
def set_bpf(self, low_cut, high_cut):
self.low_cut = low_cut
self.high_cut = high_cut
if self.running:
self.pipes["bpf_pipe"].write(
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
)
def get_bpf(self):
return [self.low_cut, self.high_cut]
def convertToLinear(self, db):
return float(math.pow(10, db / 10))
def set_squelch_level(self, squelch_level):
self.squelch_level = squelch_level
# no squelch required on digital voice modes
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV() else self.squelch_level
if self.running:
self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
def set_unvoiced_quality(self, q):
self.unvoiced_quality = q
self.restart()
def get_unvoiced_quality(self):
return self.unvoiced_quality
def set_dmr_filter(self, filter):
if self.has_pipe("dmr_control_pipe"):
self.pipes["dmr_control_pipe"].write("{0}\n".format(filter))
def set_wfm_deemphasis_tau(self, tau):
if self.wfm_deemphasis_tau == tau:
return
self.wfm_deemphasis_tau = tau
self.restart()
def ddc_transition_bw(self):
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, pipe_type in pipe_names.items():
if self.has_pipe(pipe_name):
logger.warning("{pipe_name} is still in use", pipe_name=pipe_name)
self.pipes[pipe_name].close()
if "{" + pipe_name + "}" in command_base:
p = self.pipe_base_path + pipe_name
encoding = None
# TODO make digiham output unicode and then change this here
# the whole pipe enoding feature onlye exists because of this
if pipe_name == "meta_pipe":
encoding = "cp437"
self.pipes[pipe_name] = Pipe.create(p, pipe_type, encoding=encoding)
else:
self.pipes[pipe_name] = None
def has_pipe(self, name):
return name in self.pipes and self.pipes[name] is not None
def try_delete_pipes(self, pipe_names):
for pipe_name in pipe_names:
if self.has_pipe(pipe_name):
self.pipes[pipe_name].close()
self.pipes[pipe_name] = None
def try_create_configs(self, command):
if "{direwolf_config}" in command:
self.direwolf_config = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
tmp_dir=self.temporary_directory, myid=id(self)
)
self.direwolf_port = KissClient.getFreePort()
file = open(self.direwolf_config, "w")
file.write(DirewolfConfig().getConfig(self.direwolf_port, self.is_service))
file.close()
else:
self.direwolf_config = None
self.direwolf_port = None
def try_delete_configs(self):
if self.direwolf_config:
try:
os.unlink(self.direwolf_config)
except FileNotFoundError:
# result suits our expectations. fine :)
pass
except Exception:
logger.exception("try_delete_configs()")
self.direwolf_config = None
def start(self):
with self.modification_lock:
if self.running:
return
self.running = True
command_base = " | ".join(self.chain(self.demodulator))
# create control pipes for csdr
self.try_create_pipes(self.pipe_names, command_base)
# send initial config through the pipes
if self.has_pipe("bpf_pipe"):
self.set_bpf(self.low_cut, self.high_cut)
if self.has_pipe("shift_pipe"):
self.set_offset_freq(self.offset_freq)
if self.has_pipe("squelch_pipe"):
self.set_squelch_level(self.squelch_level)
if self.has_pipe("dmr_control_pipe"):
self.set_dmr_filter(3)
# run the command
command = command_base.format(
bpf_pipe=self.pipes["bpf_pipe"],
shift_pipe=self.pipes["shift_pipe"],
squelch_pipe=self.pipes["squelch_pipe"],
smeter_pipe=self.pipes["smeter_pipe"],
meta_pipe=self.pipes["meta_pipe"],
iqtee_pipe=self.pipes["iqtee_pipe"],
iqtee2_pipe=self.pipes["iqtee2_pipe"],
dmr_control_pipe=self.pipes["dmr_control_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,
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(),
wfm_deemphasis_tau=self.wfm_deemphasis_tau,
)
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, start_new_session=True, 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():
logger.debug("restarting since rc = 0, self.running = true, and no modification")
self.restart()
threading.Thread(target=watch_thread, name="csdr_watch_thread").start()
audio_type = "hd_audio" if self.isHdAudio() else "audio"
if self.output.supports_type(audio_type):
self.output.send_output(
audio_type,
partial(
self.process.stdout.read,
self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(),
),
)
self.start_secondary_demodulator()
if self.has_pipe("smeter_pipe"):
def read_smeter():
raw = self.pipes["smeter_pipe"].readline()
if len(raw) == 0:
return None
else:
return float(raw.rstrip("\n"))
self.output.send_output("smeter", read_smeter)
if self.has_pipe("meta_pipe"):
def read_meta():
raw = self.pipes["meta_pipe"].readline()
if len(raw) == 0:
return None
else:
return raw.rstrip("\n")
self.output.send_output("meta", read_meta)
if self.csdr_dynamic_bufsize:
self.process.stdout.read(8) # dummy read to skip bufsize & preamble
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
def stop(self):
with self.modification_lock:
self.running = False
if self.process is not None:
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
# drain any leftover data to free file descriptors
self.process.communicate()
self.process = None
except ProcessLookupError:
# been killed by something else, ignore
pass
self.stop_secondary_demodulator()
self.try_delete_pipes(self.pipe_names)
def restart(self):
if not self.running:
return
self.stop()
self.start()
def __del__(self):
self.stop()

View File

@ -1,136 +0,0 @@
from pycsdr.modules import Module as BaseModule
from pycsdr.modules import Reader, Writer
from pycsdr.types import Format
from abc import ABCMeta, abstractmethod
from threading import Thread
from io import BytesIO
from subprocess import Popen, PIPE
from functools import partial
import pickle
class Module(BaseModule, metaclass=ABCMeta):
def __init__(self):
self.reader = None
self.writer = None
super().__init__()
def setReader(self, reader: Reader) -> None:
self.reader = reader
def setWriter(self, writer: Writer) -> None:
self.writer = writer
@abstractmethod
def getInputFormat(self) -> Format:
pass
@abstractmethod
def getOutputFormat(self) -> Format:
pass
def pump(self, read, write):
def copy():
while True:
data = None
try:
data = read()
except ValueError:
pass
except BrokenPipeError:
break
if data is None or isinstance(data, bytes) and len(data) == 0:
break
write(data)
return copy
class AutoStartModule(Module, metaclass=ABCMeta):
def _checkStart(self) -> None:
if self.reader is not None and self.writer is not None:
self.start()
def setReader(self, reader: Reader) -> None:
super().setReader(reader)
self._checkStart()
def setWriter(self, writer: Writer) -> None:
super().setWriter(writer)
self._checkStart()
@abstractmethod
def start(self):
pass
class ThreadModule(AutoStartModule, Thread, metaclass=ABCMeta):
def __init__(self):
self.doRun = True
super().__init__()
Thread.__init__(self)
@abstractmethod
def run(self):
pass
def stop(self):
self.doRun = False
self.reader.stop()
def start(self):
Thread.start(self)
class PickleModule(ThreadModule):
def getInputFormat(self) -> Format:
return Format.CHAR
def getOutputFormat(self) -> Format:
return Format.CHAR
def run(self):
while self.doRun:
data = self.reader.read()
if data is None:
self.doRun = False
break
io = BytesIO(data.tobytes())
try:
while True:
output = self.process(pickle.load(io))
if output is not None:
self.writer.write(pickle.dumps(output))
except EOFError:
pass
@abstractmethod
def process(self, input):
pass
class PopenModule(AutoStartModule, metaclass=ABCMeta):
def __init__(self):
self.process = None
super().__init__()
@abstractmethod
def getCommand(self):
pass
def _getProcess(self):
return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE)
def start(self):
self.process = self._getProcess()
# resume in case the reader has been stop()ed before
self.reader.resume()
Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start()
Thread(target=self.pump(partial(self.process.stdout.read1, 1024), self.writer.write)).start()
def stop(self):
if self.process is not None:
self.process.terminate()
self.process.wait()
self.process = None
self.reader.stop()

View File

@ -1,14 +0,0 @@
from csdr.module import PopenModule
from pycsdr.types import Format
class DrmModule(PopenModule):
def getInputFormat(self) -> Format:
return Format.COMPLEX_FLOAT
def getOutputFormat(self) -> Format:
return Format.SHORT
def getCommand(self):
# dream -c 6 --sigsrate 48000 --audsrate 48000 -I - -O -
return ["dream", "-c", "6", "--sigsrate", "48000", "--audsrate", "48000", "-I", "-", "-O", "-"]

View File

@ -1,13 +0,0 @@
from pycsdr.types import Format
from csdr.module import PopenModule
class FreeDVModule(PopenModule):
def getInputFormat(self) -> Format:
return Format.SHORT
def getOutputFormat(self) -> Format:
return Format.SHORT
def getCommand(self):
return ["freedv_rx", "1600", "-", "-"]

View File

@ -1,58 +0,0 @@
from csdr.module import PopenModule
from pycsdr.types import Format
from pycsdr.modules import Writer
from subprocess import Popen, PIPE
from threading import Thread
import re
import pickle
class M17Module(PopenModule):
lsfRegex = re.compile("SRC: ([a-zA-Z0-9]+), DEST: ([a-zA-Z0-9]+)")
def __init__(self):
super().__init__()
self.metawriter = None
def getInputFormat(self) -> Format:
return Format.SHORT
def getOutputFormat(self) -> Format:
return Format.SHORT
def getCommand(self):
return ["m17-demod", "-l"]
def _getProcess(self):
return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE, stderr=PIPE)
def start(self):
super().start()
Thread(target=self._readOutput).start()
def _readOutput(self):
while True:
line = self.process.stderr.readline()
if not line:
break
self.parseOutput(line.decode())
def parseOutput(self, line):
if self.metawriter is None:
return
matches = self.lsfRegex.match(line)
msg = {"protocol": "M17"}
if matches:
# fake sync
msg["sync"] = "voice"
msg["source"] = matches.group(1)
msg["destination"] = matches.group(2)
elif line.startswith("EOS"):
pass
else:
return
self.metawriter.write(pickle.dumps(msg))
def setMetaWriter(self, writer: Writer) -> None:
self.metawriter = writer

View File

@ -1,57 +0,0 @@
from pycsdr.types import Format
from csdr.module import PopenModule, ThreadModule
from owrx.wsjt import WsjtParser, Msk144Profile
import pickle
import logging
logger = logging.getLogger(__name__)
class Msk144Module(PopenModule):
def getCommand(self):
return ["msk144decoder"]
def getInputFormat(self) -> Format:
return Format.SHORT
def getOutputFormat(self) -> Format:
return Format.CHAR
class ParserAdapter(ThreadModule):
def __init__(self):
self.retained = bytes()
self.parser = WsjtParser()
self.dialFrequency = 0
super().__init__()
def run(self):
profile = Msk144Profile()
while self.doRun:
data = self.reader.read()
if data is None:
self.doRun = False
else:
self.retained += data
lines = self.retained.split(b"\n")
# keep the last line
# this should either be empty if the last char was \n
# or an incomplete line if the read returned early
self.retained = lines[-1]
# parse all completed lines
for line in lines[0:-1]:
# actual messages from msk144decoder should start with "*** "
if line[0:4] == b"*** ":
self.writer.write(pickle.dumps(self.parser.parse(profile, self.dialFrequency, line[4:])))
def getInputFormat(self) -> Format:
return Format.CHAR
def getOutputFormat(self) -> Format:
return Format.CHAR
def setDialFrequency(self, frequency: int) -> None:
self.dialFrequency = frequency

155
csdr/pipe.py Normal file
View File

@ -0,0 +1,155 @@
import os
import select
import time
import threading
import logging
logger = logging.getLogger(__name__)
class Pipe(object):
READ = "r"
WRITE = "w"
NONE = None
@staticmethod
def create(path, t, encoding=None):
if t == Pipe.READ:
return ReadingPipe(path, encoding=encoding)
elif t == Pipe.WRITE:
return WritingPipe(path, encoding=encoding)
elif t == Pipe.NONE:
return Pipe(path, None, encoding=encoding)
def __init__(self, path, direction, encoding=None):
self.doOpen = True
self.path = "{base}_{myid}".format(base=path, myid=id(self))
self.direction = direction
self.encoding = encoding
self.file = None
os.mkfifo(self.path)
def open(self):
"""
this method opens the file descriptor with an added O_NONBLOCK flag. This gives us a special behaviour for
FIFOS, when they are not opened by the opposing side:
- opening a pipe for writing will throw an OSError with errno = 6 (ENXIO). This is handled specially in the
WritingPipe class.
- opening a pipe for reading will pass through this method instantly, even if the opposing end has not been
opened yet, but the resulting file descriptor will behave as if O_NONBLOCK is set (even if we remove it
immediately here), resulting in empty reads until data is available. This is handled specially in the
ReadingPipe class.
"""
def opener(path, flags):
fd = os.open(path, flags | os.O_NONBLOCK)
os.set_blocking(fd, True)
return fd
self.file = open(self.path, self.direction, encoding=self.encoding, opener=opener)
def close(self):
self.doOpen = False
try:
if self.file is not None:
self.file.close()
os.unlink(self.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("Pipe.close()")
def __str__(self):
return self.path
class WritingPipe(Pipe):
def __init__(self, path, encoding=None):
self.queue = []
self.queueLock = threading.Lock()
super().__init__(path, "w", encoding=encoding)
self.open()
def open_and_dequeue(self):
"""
This method implements a retry loop that can be interrupted in case the Pipe gets shutdown before actually
being connected.
After the pipe is opened successfully, all data that has been queued is sent in the order it was passed into
write().
"""
retries = 0
while self.file is None and self.doOpen and retries < 10:
try:
super().open()
except OSError as error:
# ENXIO = FIFO has not been opened for reading
if error.errno == 6:
time.sleep(.1)
retries += 1
else:
raise
# if doOpen is false, opening has been canceled, so no warning in that case.
if self.file is None:
if self.doOpen:
logger.warning("could not open FIFO %s", self.path)
return
with self.queueLock:
for i in self.queue:
self.file.write(i)
self.file.flush()
self.queue = None
def open(self):
"""
This sends the opening operation off to a background thread. If we were to block the thread here, another pipe
may be waiting in the queue to be opened on the opposing side, resulting in a deadlock
"""
threading.Thread(target=self.open_and_dequeue, name="csdr_pipe_thread").start()
def write(self, data):
"""
This method queues all data to be written until the file is actually opened. As soon as a file is available,
it becomes a passthrough.
"""
if self.file is None:
with self.queueLock:
self.queue.append(data)
return
r = self.file.write(data)
self.file.flush()
return r
class ReadingPipe(Pipe):
def __init__(self, path, encoding=None):
super().__init__(path, "r", encoding=encoding)
def open(self):
"""
This method implements an interruptible loop that waits for the file descriptor to be opened and the first
batch of data coming in using repeated select() calls.
:return:
"""
if not self.doOpen:
return
super().open()
while self.doOpen:
(read, _, _) = select.select([self.file], [], [], 1)
if self.file in read:
break
def read(self):
if self.file is None:
self.open()
return self.file.read()
def readline(self):
if self.file is None:
self.open()
return self.file.readline()

68
debian/changelog vendored
View File

@ -1,71 +1,3 @@
openwebrx (1.3.0) UNRELEASED; urgency=low
* SDR device log messages are now available in the web configuration to
simplify troubleshooting
* Added support for the MSK144 digimode
-- Jakob Ketterl <jakob.ketterl@gmx.de> Fri, 30 Sep 2022 16:47:00 +0000
openwebrx (1.2.1) bullseye jammy; urgency=low
* FifiSDR support fixed (pipeline formats now line up correctly)
* Added "Device" input for FifiSDR devices for sound card selection
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 20 Sep 2022 16:01:00 +0000
openwebrx (1.2.0) bullseye jammy; urgency=low
* Major rewrite of all demodulation components to make use of the new
csdr/pycsdr and digiham/pydigiham demodulator modules
* Preliminary display of M17 callsign information
* New devices supported:
- Blade RF
-- Jakob Ketterl <jakob.ketterl@gmx.de> Wed, 15 Jun 2022 16:20:00 +0000
openwebrx (1.1.0) buster hirsute; urgency=low
* Reworked most graphical elements as SVGs for faster loadtimes and crispier
display on hi-dpi displays
* Updated pipelines to match changes in digiham
* Changed D-Star and NXDN integrations to use new decoder from digiham
* Added D-Star and NXDN metadata display
-- Jakob Ketterl <jakob.ketterl@gmx.de> Mon, 02 Aug 2021 16:24:00 +0000
openwebrx (1.0.0) buster hirsute; urgency=low
* Introduced `squelch_auto_margin` config option that allows configuring the
auto squelch level
* Removed `port` configuration option; `rtltcp_compat` takes the port number
with the new connectors
* Added support for new WSJT-X modes FST4, FST4W (only available with WSJT-X
2.3) and Q65 (only available with WSJT-X 2.4)
* Added support for demodulating M17 digital voice signals using
m17-cxx-demod
* New reporting infrastructure, allowing WSPR and FST4W spots to be sent to
wsprnet.org
* Add some basic filtering capabilities to the map
* New arguments to the `openwebrx` command-line to facilitate the
administration of users (try `openwebrx admin`)
* New command-line tool `openwebrx-admin` that facilitates the
administration of users
* Default bandwidth changes:
- "WFM" changed to 150kHz
- "Packet" (APRS) changed to 12.5kHz
* Configuration rework:
- New: fully web-based configuration interface
- System configuration parameters have been moved to a new, separate
`openwebrx.conf` file
- Remaining parameters are now editable in the web configuration
- Existing `config_webrx.py` files will still be read, but changes made in
the web configuration will be written to a new storage system
- Added upload of avatar and panorama image via web configuration
* New devices supported:
- HPSDR devices (Hermes Lite 2) thanks to @jancona
- BBRF103 / RX666 / RX888 devices supported by libsddc
- R&S devices using the EB200 or Ammos protocols
-- Jakob Ketterl <jakob.ketterl@gmx.de> Thu, 06 May 2021 17:22:00 +0000
openwebrx (0.20.3) buster focal; urgency=low openwebrx (0.20.3) buster focal; urgency=low
* Fix a compatibility issue with python versions <= 3.6 * Fix a compatibility issue with python versions <= 3.6

4
debian/control vendored
View File

@ -10,7 +10,7 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git
Package: openwebrx Package: openwebrx
Architecture: all Architecture: all
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, owrx-connector (>= 0.5), soapysdr-tools, python3-csdr (>= 0.18), ${python3:Depends}, ${misc:Depends} Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.17), netcat, owrx-connector (>= 0.3), python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends}
Recommends: python3-digiham (>= 0.6), direwolf (>= 1.4), wsjtx, js8call, runds-connector (>= 0.2), hpsdrconnector, aprs-symbols, m17-demod, js8call, python3-js8py (>= 0.2), nmux (>= 0.18), codecserver (>= 0.1), msk144decoder Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools
Description: multi-user web sdr Description: multi-user web sdr
Open source, multi-user SDR receiver with a web interface Open source, multi-user SDR receiver with a web interface

View File

@ -1,8 +0,0 @@
#!/bin/sh -e
. /usr/share/debconf/confmodule
db_get openwebrx/admin_user_configured
if [ "${1:-}" = "reconfigure" ] || [ "${RET}" != true ]; then
db_input high openwebrx/admin_user_password || true
db_go
fi

View File

@ -1 +0,0 @@
/etc/openwebrx/openwebrx.conf.d

View File

@ -1,3 +1,5 @@
config_webrx.py etc/openwebrx/
bands.json etc/openwebrx/ bands.json etc/openwebrx/
openwebrx.conf etc/openwebrx/ bookmarks.json etc/openwebrx/
users.json etc/openwebrx/
systemd/openwebrx.service lib/systemd/system/ systemd/openwebrx.service lib/systemd/system/

View File

@ -1,59 +0,0 @@
#!/bin/bash
. /usr/share/debconf/confmodule
set -euo pipefail
OWRX_USER="openwebrx"
OWRX_DATADIR="/var/lib/openwebrx"
OWRX_USERS_FILE="${OWRX_DATADIR}/users.json"
OWRX_SETTINGS_FILE="${OWRX_DATADIR}/settings.json"
OWRX_BOOKMARKS_FILE="${OWRX_DATADIR}/bookmarks.json"
case "$1" in
configure|reconfigure)
adduser --system --group --no-create-home --home /nonexistent --quiet "${OWRX_USER}"
usermod -aG plugdev "${OWRX_USER}"
# create OpenWebRX data directory and set the correct permissions
if [ ! -d "${OWRX_DATADIR}" ] && [ ! -L "${OWRX_DATADIR}" ]; then mkdir "${OWRX_DATADIR}"; fi
chown "${OWRX_USER}". ${OWRX_DATADIR}
# create empty config files now to avoid permission problems later
if [ ! -e "${OWRX_USERS_FILE}" ]; then
echo "[]" > "${OWRX_USERS_FILE}"
chown "${OWRX_USER}". "${OWRX_USERS_FILE}"
chmod 0600 "${OWRX_USERS_FILE}"
fi
if [ ! -e "${OWRX_SETTINGS_FILE}" ]; then
echo "{}" > "${OWRX_SETTINGS_FILE}"
chown "${OWRX_USER}". "${OWRX_SETTINGS_FILE}"
fi
if [ ! -e "${OWRX_BOOKMARKS_FILE}" ]; then
touch "${OWRX_BOOKMARKS_FILE}"
chown "${OWRX_USER}". "${OWRX_BOOKMARKS_FILE}"
fi
db_get openwebrx/admin_user_password
if [ ! -z "${RET}" ]; then
if ! openwebrx admin --silent hasuser admin; then
# create initial openwebrx user
OWRX_PASSWORD="${RET}" openwebrx admin --noninteractive adduser admin
else
# change existing user's password
OWRX_PASSWORD="${RET}" openwebrx admin --noninteractive resetpassword admin
fi
fi
# remove password from debconf database
db_unregister openwebrx/admin_user_password
# set a marker that admin is configured to avoid future questions
db_set openwebrx/admin_user_configured true
;;
*)
echo "postinst called with unknown argument '$1'" 1>&2
exit 1
;;
esac
#DEBHELPER#

View File

@ -1,8 +0,0 @@
#!/bin/sh -e
if [ "$1" = purge ] && [ -e /usr/share/debconf/confmodule ]; then
. /usr/share/debconf/confmodule
db_purge
fi
#DEBHELPER#

View File

@ -1,23 +0,0 @@
Template: openwebrx/admin_user_password
Type: password
Description: OpenWebRX "admin" user password:
The system can create a user for the OpenWebRX web configuration interface for
you. Using this user, you will be able to log into the "settings" area of
OpenWebRX to configure your receiver conveniently through your browser.
.
The name of the created user will be "admin".
.
If you do not wish to create a web admin user right now, you can leave this
empty for now. You can return to this prompt at a later time by running the
command "sudo dpkg-reconfigure openwebrx".
.
You can also use the "openwebrx admin" command to create, delete or manage
existing users. More information is available in by running the command
"openwebrx admin --help".
Template: openwebrx/admin_user_configured
Type: boolean
Default: false
Description: OpenWebRX "admin" user previously configured?
Marker used internally by the config scripts to remember if an admin user has
been created.

7
debian/postinst vendored Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
set -euxo pipefail
adduser --system --group --no-create-home --home /nonexistant openwebrx
usermod -aG plugdev openwebrx
#DEBHELPER#

View File

@ -2,10 +2,10 @@
set -euo pipefail set -euo pipefail
ARCH=$(uname -m) ARCH=$(uname -m)
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-bladerf openwebrx-full openwebrx" IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-redpitaya openwebrx-rtltcp openwebrx-full openwebrx"
ALL_ARCHS="x86_64 armv7l aarch64" ALL_ARCHS="x86_64 armv7l aarch64"
TAG=${TAG:-"latest"} TAG=${TAG:-"latest"}
ARCHTAG="${TAG}-${ARCH}" ARCHTAG="$TAG-$ARCH"
usage () { usage () {
echo "Usage: ${0} [command]" echo "Usage: ${0} [command]"
@ -36,7 +36,7 @@ build () {
push () { push () {
for image in ${IMAGES}; do for image in ${IMAGES}; do
docker push jketterl/${image}:${ARCHTAG} docker push jketterl/$image:$ARCHTAG
done done
} }
@ -45,11 +45,11 @@ manifest () {
# there's no docker manifest rm command, and the create --amend does not work, so we have to clean up manually # there's no docker manifest rm command, and the create --amend does not work, so we have to clean up manually
rm -rf "${HOME}/.docker/manifests/docker.io_jketterl_${image}-${TAG}" rm -rf "${HOME}/.docker/manifests/docker.io_jketterl_${image}-${TAG}"
IMAGE_LIST="" IMAGE_LIST=""
for a in ${ALL_ARCHS}; do for a in $ALL_ARCHS; do
IMAGE_LIST="${IMAGE_LIST} jketterl/${image}:${TAG}-${a}" IMAGE_LIST="$IMAGE_LIST jketterl/$image:$TAG-$a"
done done
docker manifest create jketterl/${image}:${TAG} ${IMAGE_LIST} docker manifest create jketterl/$image:$TAG $IMAGE_LIST
docker manifest push --purge jketterl/${image}:${TAG} docker manifest push --purge jketterl/$image:$TAG
done done
} }

View File

@ -1,10 +1,9 @@
FROM debian:bullseye-slim FROM debian:buster-slim
COPY docker/files/js8call/js8call-hamlib.patch \ COPY docker/files/js8call/js8call-hamlib.patch \
docker/files/wsjtx/wsjtx.patch \ docker/files/wsjtx/wsjtx.patch \
docker/files/wsjtx/wsjtx-hamlib.patch \ docker/files/wsjtx/wsjtx-hamlib.patch \
docker/files/dream/dream.patch \ docker/files/dream/dream.patch \
docker/files/direwolf/direwolf-hamlib.patch \
docker/scripts/install-dependencies.sh / docker/scripts/install-dependencies.sh /
RUN /install-dependencies.sh && \ RUN /install-dependencies.sh && \
rm /install-dependencies.sh && \ rm /install-dependencies.sh && \
@ -13,16 +12,12 @@ COPY docker/scripts/install-owrx-tools.sh /
RUN /install-owrx-tools.sh && \ RUN /install-owrx-tools.sh && \
rm /install-owrx-tools.sh rm /install-owrx-tools.sh
COPY docker/files/services/codecserver /etc/services.d/codecserver
ENTRYPOINT ["/init"] ENTRYPOINT ["/init"]
WORKDIR /opt/openwebrx WORKDIR /opt/openwebrx
VOLUME /etc/openwebrx VOLUME /etc/openwebrx
VOLUME /var/lib/openwebrx
ENV S6_CMD_ARG0="/opt/openwebrx/docker/scripts/run.sh" CMD [ "/opt/openwebrx/docker/scripts/run.sh" ]
CMD []
EXPOSE 8073 EXPOSE 8073

View File

@ -1,8 +0,0 @@
ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG
COPY docker/scripts/install-dependencies-bladerf.sh /
RUN /install-dependencies-bladerf.sh &&\
rm /install-dependencies-bladerf.sh
COPY . /opt/openwebrx

View File

@ -18,10 +18,8 @@ RUN /install-dependencies-rtlsdr.sh &&\
/install-dependencies-fcdpp.sh &&\ /install-dependencies-fcdpp.sh &&\
/install-dependencies-radioberry.sh &&\ /install-dependencies-radioberry.sh &&\
/install-dependencies-uhd.sh &&\ /install-dependencies-uhd.sh &&\
/install-dependencies-hpsdr.sh &&\ /install-dependencies-redpitaya.sh &&\
/install-dependencies-bladerf.sh &&\
/install-connectors.sh &&\ /install-connectors.sh &&\
/install-dependencies-runds.sh &&\
rm /install-dependencies-*.sh &&\ rm /install-dependencies-*.sh &&\
rm /install-lib.*.patch && \ rm /install-lib.*.patch && \
rm /install-connectors.sh rm /install-connectors.sh

View File

@ -1,9 +0,0 @@
ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG
COPY docker/scripts/install-dependencies-hpsdr.sh /
RUN /install-dependencies-hpsdr.sh &&\
rm /install-dependencies-hpsdr.sh
COPY . /opt/openwebrx

View File

@ -0,0 +1,8 @@
ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG
COPY docker/scripts/install-dependencies-redpitaya.sh /
RUN /install-dependencies-redpitaya.sh &&\
rm /install-dependencies-redpitaya.sh
COPY . /opt/openwebrx

View File

@ -1,12 +0,0 @@
ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG
COPY docker/scripts/install-connectors.sh \
docker/scripts/install-dependencies-runds.sh /
RUN /install-connectors.sh &&\
rm /install-connectors.sh && \
/install-dependencies-runds.sh && \
rm /install-dependencies-runds.sh
COPY . /opt/openwebrx

5
docker/env Normal file
View File

@ -0,0 +1,5 @@
ARCH=$(uname -m)
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-redpitaya openwebrx-rtltcp openwebrx-full openwebrx"
ALL_ARCHS="x86_64 armv7l aarch64"
TAG=${TAG:-"latest"}
ARCHTAG="$TAG-$ARCH"

View File

@ -1,20 +0,0 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9e710f5..da90b43 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -257,13 +257,8 @@ else()
set(GPSD_LIBRARIES "")
endif()
-find_package(hamlib)
-if(HAMLIB_FOUND)
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_HAMLIB")
-else()
- set(HAMLIB_INCLUDE_DIRS "")
- set(HAMLIB_LIBRARIES "")
-endif()
+set(HAMLIB_INCLUDE_DIRS "")
+set(HAMLIB_LIBRARIES "")
if(LINUX)
find_package(ALSA REQUIRED)

View File

@ -1,2 +0,0 @@
#!/usr/bin/execlineb -P
/usr/local/bin/codecserver

View File

@ -1,17 +1,18 @@
--- CMakeLists.txt.orig 2021-09-28 14:33:14.329598412 +0200 --- CMakeLists.txt.orig 2020-07-21 20:59:55.982026645 +0200
+++ CMakeLists.txt 2021-09-28 14:34:23.052345270 +0200 +++ CMakeLists.txt 2020-07-21 21:01:25.444836112 +0200
@@ -106,24 +106,6 @@ @@ -80,24 +80,6 @@
include (ExternalProject)
# -
-#
-# build and install hamlib locally so it can be referenced by the -# build and install hamlib locally so it can be referenced by the
-# WSJT-X build -# WSJT-X build
-# -#
-ExternalProject_Add (hamlib -ExternalProject_Add (hamlib
- GIT_REPOSITORY ${hamlib_repo} - GIT_REPOSITORY ${hamlib_repo}
- GIT_TAG ${hamlib_TAG} - GIT_TAG ${hamlib_TAG}
- GIT_SHALLOW False - URL ${CMAKE_CURRENT_SOURCE_DIR}/src/${__hamlib_upstream}
- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/${__hamlib_upstream}.tar.gz
- URL_HASH MD5=${hamlib_md5sum} - URL_HASH MD5=${hamlib_md5sum}
- #UPDATE_COMMAND ${CMAKE_COMMAND} -E env "[ -f ./bootstrap ] && ./bootstrap" - #UPDATE_COMMAND ${CMAKE_COMMAND} -E env "[ -f ./bootstrap ] && ./bootstrap"
- PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch - PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch
@ -21,11 +22,10 @@
- STEP_TARGETS update install - STEP_TARGETS update install
- ) - )
- -
-# #
# custom target to make a hamlib source tarball # custom target to make a hamlib source tarball
# #
add_custom_target (hamlib_sources @@ -136,7 +118,6 @@
@@ -161,7 +143,6 @@
# build and optionally install WSJT-X using the hamlib package built # build and optionally install WSJT-X using the hamlib package built
# above # above
# #
@ -33,18 +33,11 @@
ExternalProject_Add (wsjtx ExternalProject_Add (wsjtx
GIT_REPOSITORY ${wsjtx_repo} GIT_REPOSITORY ${wsjtx_repo}
GIT_TAG ${WSJTX_TAG} GIT_TAG ${WSJTX_TAG}
@@ -186,14 +167,8 @@ @@ -160,7 +141,6 @@
DEPENDEES build DEPENDEES build
) )
-set_target_properties (hamlib PROPERTIES EXCLUDE_FROM_ALL 1) -set_target_properties (hamlib PROPERTIES EXCLUDE_FROM_ALL 1)
set_target_properties (wsjtx PROPERTIES EXCLUDE_FROM_ALL 1) set_target_properties (wsjtx PROPERTIES EXCLUDE_FROM_ALL 1)
-add_dependencies (wsjtx-configure hamlib-install) add_dependencies (wsjtx-configure hamlib-install)
-add_dependencies (wsjtx-build hamlib-install)
-add_dependencies (wsjtx-install hamlib-install)
-add_dependencies (wsjtx-package hamlib-install)
-
# export traditional targets
add_custom_target (build ALL DEPENDS wsjtx-build)
add_custom_target (install DEPENDS wsjtx-install)

View File

@ -1,156 +1,59 @@
diff -ur wsjtx-orig/CMake/Modules/Findhamlib.cmake wsjtx/CMake/Modules/Findhamlib.cmake
--- wsjtx-orig/CMake/Modules/Findhamlib.cmake 2020-07-21 21:10:43.124810140 +0200
+++ wsjtx/CMake/Modules/Findhamlib.cmake 2020-07-21 21:11:03.368019114 +0200
@@ -85,4 +85,4 @@
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
# TRUE if all listed variables are TRUE
include (FindPackageHandleStandardArgs)
-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
+find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
--- wsjtx-orig/CMakeLists.txt 2023-01-28 17:43:05.586124507 +0100 --- wsjtx-orig/CMakeLists.txt 2020-07-21 21:10:43.124810140 +0200
+++ wsjtx/CMakeLists.txt 2023-01-28 17:56:07.108634912 +0100 +++ wsjtx/CMakeLists.txt 2020-07-21 22:14:04.454639589 +0200
@@ -122,7 +122,7 @@ @@ -871,7 +871,7 @@
option (WSJT_QDEBUG_TO_FILE "Redirect Qt debuging messages to a trace file.") #
option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON) # libhamlib setup
option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.") #
-option (WSJT_GENERATE_DOCS "Generate documentation files." ON) -set (hamlib_STATIC 1)
+option (WSJT_GENERATE_DOCS "Generate documentation files.") +set (hamlib_STATIC 0)
option (WSJT_RIG_NONE_CAN_SPLIT "Allow split operation with \"None\" as rig.") find_package (hamlib 3 REQUIRED)
option (WSJT_TRACE_UDP "Debugging option that turns on UDP message protocol diagnostics.") find_program (RIGCTL_EXE rigctl)
option (WSJT_BUILD_UTILS "Build simulators and code demonstrators." ON) find_program (RIGCTLD_EXE rigctld)
@@ -170,77 +170,7 @@ @@ -1348,53 +1348,10 @@
)
set (wsjt_qt_CXXSRCS endif(WSJT_BUILD_UTILS)
- helper_functions.cpp
- qt_helpers.cpp
- widgets/MessageBox.cpp
- MetaDataRegistry.cpp
- Network/NetworkServerLookup.cpp
revision_utils.cpp
- L10nLoader.cpp
- WFPalette.cpp
- Radio.cpp
- RadioMetaType.cpp
- NonInheritingProcess.cpp
- models/IARURegions.cpp
- models/Bands.cpp
- models/Modes.cpp
- models/FrequencyList.cpp
- models/StationList.cpp
- widgets/FrequencyLineEdit.cpp
- widgets/FrequencyDeltaLineEdit.cpp
- item_delegates/CandidateKeyFilter.cpp
- item_delegates/ForeignKeyDelegate.cpp
- item_delegates/MessageItemDelegate.cpp
- validators/LiveFrequencyValidator.cpp
- GetUserId.cpp
- Audio/AudioDevice.cpp
- Transceiver/Transceiver.cpp
- Transceiver/TransceiverBase.cpp
- Transceiver/EmulateSplitTransceiver.cpp
- Transceiver/TransceiverFactory.cpp
- Transceiver/PollingTransceiver.cpp
- Transceiver/HamlibTransceiver.cpp
- Transceiver/HRDTransceiver.cpp
- Transceiver/DXLabSuiteCommanderTransceiver.cpp
- Network/NetworkMessage.cpp
- Network/MessageClient.cpp
- widgets/LettersSpinBox.cpp
- widgets/HintedSpinBox.cpp
- widgets/RestrictedSpinBox.cpp
- widgets/HelpTextWindow.cpp
- SampleDownloader.cpp
- SampleDownloader/DirectoryDelegate.cpp
- SampleDownloader/Directory.cpp
- SampleDownloader/FileNode.cpp
- SampleDownloader/RemoteFile.cpp
- DisplayManual.cpp
- MultiSettings.cpp
- validators/MaidenheadLocatorValidator.cpp
- validators/CallsignValidator.cpp
- widgets/SplashScreen.cpp
- EqualizationToolsDialog.cpp
- widgets/DoubleClickablePushButton.cpp
- widgets/DoubleClickableRadioButton.cpp
- Network/LotWUsers.cpp
- models/DecodeHighlightingModel.cpp
- widgets/DecodeHighlightingListView.cpp
- models/FoxLog.cpp
- widgets/AbstractLogWindow.cpp
- widgets/FoxLogWindow.cpp
- widgets/CabrilloLogWindow.cpp
- item_delegates/CallsignDelegate.cpp
- item_delegates/MaidenheadLocatorDelegate.cpp
- item_delegates/FrequencyDelegate.cpp
- item_delegates/FrequencyDeltaDelegate.cpp
- item_delegates/SQLiteDateTimeDelegate.cpp
- models/CabrilloLog.cpp
- logbook/AD1CCty.cpp
- logbook/WorkedBefore.cpp
- logbook/Multiplier.cpp
- Network/NetworkAccessManager.cpp
- widgets/LazyFillComboBox.cpp
- widgets/CheckableItemComboBox.cpp
- widgets/BandComboBox.cpp
)
set (wsjt_qtmm_CXXSRCS
@@ -1089,9 +1019,6 @@
if (WSJT_GENERATE_DOCS)
add_subdirectory (doc)
endif (WSJT_GENERATE_DOCS)
-if (EXISTS ${CMAKE_SOURCE_DIR}/tests AND IS_DIRECTORY ${CMAKE_SOURCE_DIR}/tests)
- add_subdirectory (tests)
-endif ()
# build a library of package functionality (without and optionally with OpenMP support)
add_library (wsjt_cxx STATIC ${wsjt_CSRCS} ${wsjt_CXXSRCS})
@@ -1357,10 +1284,7 @@
add_library (wsjt_qt STATIC ${wsjt_qt_CXXSRCS} ${wsjt_qt_GENUISRCS} ${GENAXSRCS})
# set wsjtx_udp exports to static variants
target_compile_definitions (wsjt_qt PUBLIC UDP_STATIC_DEFINE)
-target_link_libraries (wsjt_qt Hamlib::Hamlib Boost::log qcp Qt5::Widgets Qt5::Network Qt5::Sql)
-if (WIN32)
- target_link_libraries (wsjt_qt Qt5::AxContainer Qt5::AxBase)
-endif (WIN32)
+target_link_libraries (wsjt_qt Qt5::Core)
# build a library of package Qt functionality used in Fortran utilities
add_library (fort_qt STATIC ${fort_qt_CXXSRCS})
@@ -1425,90 +1349,6 @@
add_subdirectory (map65)
endif ()
-# build the main application -# build the main application
-generate_version_info (wsjtx_VERSION_RESOURCES
- NAME wsjtx
- BUNDLE ${PROJECT_BUNDLE_NAME}
- ICON ${WSJTX_ICON_FILE}
- )
-
-add_executable (wsjtx MACOSX_BUNDLE -add_executable (wsjtx MACOSX_BUNDLE
- ${wsjtx_CXXSRCS} - ${wsjtx_CXXSRCS}
- ${wsjtx_GENUISRCS} - ${wsjtx_GENUISRCS}
- wsjtx.rc
- ${WSJTX_ICON_FILE} - ${WSJTX_ICON_FILE}
- ${wsjtx_RESOURCES_RCC} - ${wsjtx_RESOURCES_RCC}
- ${wsjtx_VERSION_RESOURCES}
- ) - )
- -
-if (WSJT_CREATE_WINMAIN) if (WSJT_CREATE_WINMAIN)
- set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON) set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON)
-endif (WSJT_CREATE_WINMAIN) endif (WSJT_CREATE_WINMAIN)
-
-set_target_properties (wsjtx PROPERTIES -set_target_properties (wsjtx PROPERTIES
- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in" - MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in"
- MACOSX_BUNDLE_INFO_STRING "${PROJECT_DESCRIPTION}" - MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}"
- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}" - MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}"
- MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH} - MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION}
- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" - MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}"
- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${SCS_VERSION_STR}" - MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}"
- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_BUNDLE_NAME}" - MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}" - MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}" - MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
- MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx" - MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx"
- ) - )
- -
-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS}) -target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS})
-if ((NOT ${OPENMP_FOUND}) OR APPLE) -if (APPLE)
- target_link_libraries (wsjtx wsjt_fort) - target_link_libraries (wsjtx Qt5::SerialPort wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
-else () -else ()
- target_link_libraries (wsjtx wsjt_fort_omp) - target_link_libraries (wsjtx Qt5::SerialPort wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
- if (OpenMP_C_FLAGS) - if (OpenMP_C_FLAGS)
- set_target_properties (wsjtx PROPERTIES - set_target_properties (wsjtx PROPERTIES
- COMPILE_FLAGS "${OpenMP_C_FLAGS}" - COMPILE_FLAGS "${OpenMP_C_FLAGS}"
@ -162,72 +65,18 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
- ) - )
- if (WIN32) - if (WIN32)
- set_target_properties (wsjtx PROPERTIES - set_target_properties (wsjtx PROPERTIES
- LINK_FLAGS -Wl,--stack,0x1000000,--heap,0x20000000 - LINK_FLAGS -Wl,--stack,16777216
- ) - )
- endif () - endif ()
-endif () -endif ()
-target_link_libraries (wsjtx Qt5::SerialPort wsjt_cxx wsjt_qt wsjt_qtmm ${FFTW3_LIBRARIES} ${LIBM_LIBRARIES})
- -
-# make a library for WSJT-X UDP servers # make a library for WSJT-X UDP servers
-# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS}) # add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
-add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS}) add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
-#target_include_directories (wsjtx_udp @@ -1437,24 +1394,9 @@
-# INTERFACE set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
-# $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/wsjtx> endif (WSJT_CREATE_WINMAIN)
-# )
-target_include_directories (wsjtx_udp-static
- INTERFACE
- $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/wsjtx>
- )
-#set_target_properties (wsjtx_udp PROPERTIES
-# PUBLIC_HEADER "${UDP_library_HEADERS}"
-# )
-set_target_properties (wsjtx_udp-static PROPERTIES
- OUTPUT_NAME wsjtx_udp
- )
-target_compile_definitions (wsjtx_udp-static PUBLIC UDP_STATIC_DEFINE)
-target_link_libraries (wsjtx_udp-static Qt5::Network Qt5::Gui)
-generate_export_header (wsjtx_udp-static BASE_NAME udp)
-
-generate_version_info (udp_daemon_VERSION_RESOURCES
- NAME udp_daemon
- BUNDLE ${PROJECT_BUNDLE_NAME}
- ICON ${WSJTX_ICON_FILE}
- FILE_DESCRIPTION "Example WSJT-X UDP Message Protocol daemon"
- )
-add_executable (udp_daemon UDPExamples/UDPDaemon.cpp ${udp_daemon_VERSION_RESOURCES})
-target_link_libraries (udp_daemon wsjtx_udp-static)
-
generate_version_info (wsjtx_app_version_VERSION_RESOURCES
NAME wsjtx_app_version
BUNDLE ${PROJECT_BUNDLE_NAME}
@@ -1518,47 +1358,9 @@
add_executable (wsjtx_app_version AppVersion/AppVersion.cpp ${wsjtx_app_version_VERSION_RESOURCES})
target_link_libraries (wsjtx_app_version wsjt_qt)
-generate_version_info (message_aggregator_VERSION_RESOURCES
- NAME message_aggregator
- BUNDLE ${PROJECT_BUNDLE_NAME}
- ICON ${WSJTX_ICON_FILE}
- FILE_DESCRIPTION "Example WSJT-X UDP Message Protocol application"
- )
-add_resources (message_aggregator_RESOURCES /qss ${message_aggregator_STYLESHEETS})
-configure_file (UDPExamples/message_aggregator.qrc.in message_aggregator.qrc @ONLY)
-qt5_add_resources (message_aggregator_RESOURCES_RCC
- ${CMAKE_CURRENT_BINARY_DIR}/message_aggregator.qrc
- contrib/QDarkStyleSheet/qdarkstyle/style.qrc
- )
-add_executable (message_aggregator
- ${message_aggregator_CXXSRCS}
- ${message_aggregator_RESOURCES_RCC}
- ${message_aggregator_VERSION_RESOURCES}
- )
-target_link_libraries (message_aggregator wsjt_qt Qt5::Widgets wsjtx_udp-static)
-
-if (WSJT_CREATE_WINMAIN)
- set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
-endif (WSJT_CREATE_WINMAIN)
-
-if (UNIX) -if (UNIX)
- if (NOT WSJT_SKIP_MANPAGES) - if (NOT WSJT_SKIP_MANPAGES)
- add_subdirectory (manpages) - add_subdirectory (manpages)
@ -249,21 +98,21 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# install (TARGETS wsjtx_udp EXPORT udp # install (TARGETS wsjtx_udp EXPORT udp
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -1577,12 +1379,7 @@ @@ -1473,12 +1415,7 @@
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx # DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
# ) # )
-install (TARGETS udp_daemon message_aggregator wsjtx_app_version -install (TARGETS udp_daemon message_aggregator
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
- BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime - BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
- ) - )
- -
-install (TARGETS jt9 wsprd fmtave fcal fmeasure -install (TARGETS jt9 wsprd fmtave fcal fmeasure
+install (TARGETS wsjtx_app_version jt9 wsprd +install (TARGETS jt9 wsprd
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
) )
@@ -1595,38 +1392,6 @@ @@ -1491,39 +1428,6 @@
) )
endif(WSJT_BUILD_UTILS) endif(WSJT_BUILD_UTILS)
@ -294,48 +143,13 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
- AUTHORS - AUTHORS
- THANKS - THANKS
- NEWS - NEWS
- INSTALL
- BUGS - BUGS
- DESTINATION ${CMAKE_INSTALL_DOCDIR} - DESTINATION ${CMAKE_INSTALL_DOCDIR}
- #COMPONENT runtime - #COMPONENT runtime
- ) - )
- -
install (FILES install (FILES
cty.dat contrib/Ephemeris/JPLEPH
cty.dat_copyright.txt DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
@@ -1635,13 +1400,6 @@ Only in wsjtx: .idea
#COMPONENT runtime
)
-install (DIRECTORY
- example_log_configurations
- DESTINATION ${CMAKE_INSTALL_DOCDIR}
- FILES_MATCHING REGEX "^.*[^~]$"
- #COMPONENT runtime
- )
-
#
# Mac installer files
#
@@ -1693,22 +1451,6 @@
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
)
-
-if (NOT WIN32 AND NOT APPLE)
- # install a desktop file so wsjtx appears in the application start
- # menu with an icon
- install (
- FILES wsjtx.desktop message_aggregator.desktop
- DESTINATION share/applications
- #COMPONENT runtime
- )
- install (
- FILES icons/Unix/wsjtx_icon.png
- DESTINATION share/pixmaps
- #COMPONENT runtime
- )
-endif (NOT WIN32 AND NOT APPLE)
-
if (APPLE)
set (CMAKE_POSTFLIGHT_SCRIPT
"${wsjtx_BINARY_DIR}/postflight.sh")

View File

@ -18,14 +18,13 @@ function cmakebuild() {
cd /tmp cd /tmp
BUILD_PACKAGES="git cmake make gcc g++ libsamplerate-dev libfftw3-dev" BUILD_PACKAGES="git cmake make gcc g++"
apt-get update apt-get update
apt-get -y install --no-install-recommends $BUILD_PACKAGES apt-get -y install --no-install-recommends $BUILD_PACKAGES
git clone https://github.com/jketterl/owrx_connector.git git clone https://github.com/jketterl/owrx_connector.git
# latest develop as of 2022-12-11 (std::endl implicit flushing) cmakebuild owrx_connector 0.3.0
cmakebuild owrx_connector bca362707131289f91441c8080fd368fdc067b6d
apt-get -y purge --autoremove $BUILD_PACKAGES apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean apt-get clean

View File

@ -1,36 +0,0 @@
#!/bin/bash
set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb-1.0-0"
BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/Nuand/bladeRF.git
cmakebuild bladeRF 2021.10
git clone https://github.com/pothosware/SoapyBladeRF.git
# latest from master as of 2022-01-12
cmakebuild SoapyBladeRF 70505a5cdf8c9deabc4af3eb3384aa82a7b6f021
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,46 +0,0 @@
#!/bin/bash
set -euxo pipefail
export MAKEFLAGS="-j4"
BUILD_PACKAGES="git wget gcc libc6-dev"
apt-get update
apt-get -y install --no-install-recommends $BUILD_PACKAGES
pushd /tmp
ARCH=$(uname -m)
GOVERSION=1.15.5
case ${ARCH} in
x86_64)
PACKAGE=go${GOVERSION}.linux-amd64.tar.gz
;;
armv*)
PACKAGE=go${GOVERSION}.linux-armv6l.tar.gz
;;
aarch64)
PACKAGE=go${GOVERSION}.linux-arm64.tar.gz
;;
esac
wget https://golang.org/dl/${PACKAGE}
tar xfz $PACKAGE
git clone https://github.com/jancona/hpsdrconnector.git
pushd hpsdrconnector
git checkout v0.6.1
/tmp/go/bin/go build
install -m 0755 hpsdrconnector /usr/local/bin
popd
rm -rf hpsdrconnector
rm -rf go
rm $PACKAGE
popd
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euo pipefail
export MAKEFLAGS="-j4" export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
@ -19,15 +19,14 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="" STATIC_PACKAGES=""
BUILD_PACKAGES="git cmake make gcc g++ pkg-config" BUILD_PACKAGES="git cmake make gcc g++"
apt-get update apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/jketterl/runds_connector.git git clone https://github.com/pothosware/SoapyRedPitaya.git
# latest develop as of 2022-12-11 (std::endl implicit flushing) cmakebuild SoapyRedPitaya soapy-redpitaya-0.1.1
cmakebuild runds_connector 06ca993a3c81ddb0a2581b1474895da07752a9e1
apt-get -y purge --autoremove $BUILD_PACKAGES SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean apt-get clean
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*

View File

@ -38,7 +38,7 @@ case $ARCH in
;; ;;
esac esac
wget --no-http-keep-alive https://www.sdrplay.com/software/$BINARY wget https://www.sdrplay.com/software/$BINARY
sh $BINARY --noexec --target sdrplay sh $BINARY --noexec --target sdrplay
patch --verbose -Np0 < /install-lib.$ARCH.patch patch --verbose -Np0 < /install-lib.$ARCH.patch
@ -48,9 +48,9 @@ cd ..
rm -rf sdrplay rm -rf sdrplay
rm $BINARY rm $BINARY
git clone https://github.com/pothosware/SoapySDRPlay3.git git clone https://github.com/SDRplay/SoapySDRPlay.git
# latest from master as of 2021-06-19 (reliability fixes) # latest from master as of 2020-09-04
cmakebuild SoapySDRPlay3 a869f25364a1f0d5b16169ff908aa21a2ace475d cmakebuild SoapySDRPlay 105f8a6b3d449982d7ef860790c201aa066b8fa9
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean apt-get clean

View File

@ -18,16 +18,17 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb-1.0.0 libboost-chrono1.74.0 libboost-date-time1.74.0 libboost-filesystem1.74.0 libboost-program-options1.74.0 libboost-regex1.74.0 libboost-test1.74.0 libboost-serialization1.74.0 libboost-thread1.74.0 libboost-system1.74.0 python3-numpy python3-mako" STATIC_PACKAGES="libusb-1.0.0 libboost-chrono1.67.0 libboost-date-time1.67.0 libboost-filesystem1.67.0 libboost-program-options1.67.0 libboost-regex1.67.0 libboost-test1.67.0 libboost-serialization1.67.0 libboost-thread1.67.0 libboost-system1.67.0 python3-numpy python3-mako"
BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev libboost-dev libboost-chrono-dev libboost-date-time-dev libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev libboost-test-dev libboost-serialization-dev libboost-thread-dev libboost-system-dev" BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev libboost-dev libboost-chrono-dev libboost-date-time-dev libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev libboost-test-dev libboost-serialization-dev libboost-thread-dev libboost-system-dev"
apt-get update apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/EttusResearch/uhd.git git clone https://github.com/EttusResearch/uhd.git
# 3.15.0.0 Release
mkdir -p uhd/host/build mkdir -p uhd/host/build
cd uhd/host/build cd uhd/host/build
git checkout v4.1.0.4 git checkout v3.15.0.0
# see https://github.com/EttusResearch/uhd/issues/350 # see https://github.com/EttusResearch/uhd/issues/350
case `uname -m` in case `uname -m` in
arm*) arm*)

View File

@ -7,9 +7,6 @@ function cmakebuild() {
if [[ ! -z "${2:-}" ]]; then if [[ ! -z "${2:-}" ]]; then
git checkout $2 git checkout $2
fi fi
if [[ -f ".gitmodules" ]]; then
git submodule update --init
fi
mkdir build mkdir build
cd build cd build
cmake ${CMAKE_ARGS:-} .. cmake ${CMAKE_ARGS:-} ..
@ -21,8 +18,8 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline8 libgfortran5 libgomp1 libasound2 libudev1 ca-certificates libpulse0 libfaad2 libopus0 libboost-program-options1.74.0 libboost-log1.74.0 libcurl4" STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates libqt5gui5 libqt5sql5 libqt5printsupport5 libpulse0 libfaad2 libopus0"
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-qmake libfaad-dev libopus-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev libpulse-dev libcurl4-openssl-dev" BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-default libfaad-dev libopus-dev"
apt-get update apt-get update
apt-get -y install auto-apt-proxy apt-get -y install auto-apt-proxy
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
@ -43,6 +40,15 @@ wget https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s
tar xzf s6-overlay-${PLATFORM}.tar.gz -C / tar xzf s6-overlay-${PLATFORM}.tar.gz -C /
rm s6-overlay-${PLATFORM}.tar.gz rm s6-overlay-${PLATFORM}.tar.gz
git clone https://git.code.sf.net/p/itpp/git itpp
cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d
git clone https://github.com/szechyjs/mbelib.git
cmakebuild mbelib 9a04ed5c78176a9965f3d43f7aa1b1f5330e771f
git clone https://github.com/f4exb/dsd.git
cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7
JS8CALL_VERSION=2.2.0 JS8CALL_VERSION=2.2.0
JS8CALL_DIR=js8call JS8CALL_DIR=js8call
JS8CALL_TGZ=js8call-${JS8CALL_VERSION}.tgz JS8CALL_TGZ=js8call-${JS8CALL_VERSION}.tgz
@ -51,33 +57,25 @@ tar xfz ${JS8CALL_TGZ}
# patch allows us to build against the packaged hamlib # patch allows us to build against the packaged hamlib
patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch
rm /js8call-hamlib.patch rm /js8call-hamlib.patch
cmakebuild ${JS8CALL_DIR} CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_HAMLIB_THREE" cmakebuild ${JS8CALL_DIR}
rm ${JS8CALL_TGZ} rm ${JS8CALL_TGZ}
WSJT_DIR=wsjtx-2.6.1 WSJT_DIR=wsjtx-2.2.2
WSJT_TGZ=${WSJT_DIR}.tgz WSJT_TGZ=${WSJT_DIR}.tgz
wget https://downloads.sourceforge.net/project/wsjt/${WSJT_DIR}/${WSJT_TGZ} wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
tar xfz ${WSJT_TGZ} tar xfz ${WSJT_TGZ}
patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch
mv /wsjtx.patch ${WSJT_DIR} mv /wsjtx.patch ${WSJT_DIR}
cmakebuild ${WSJT_DIR} cmakebuild ${WSJT_DIR}
rm ${WSJT_TGZ} rm ${WSJT_TGZ}
git clone https://github.com/alexander-sholohov/msk144decoder.git git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git
# latest from main as of 2023-02-21
MAKEFLAGS="" cmakebuild msk144decoder fe2991681e455636e258e83c29fd4b2a72d16095
git clone --depth 1 -b 1.6 https://github.com/wb2osz/direwolf.git
cd direwolf cd direwolf
# hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need. # hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need.
# this patch prevents direwolf from linking to it, and it can be stripped at the end of the script. # by setting enable_hamlib we prevent direwolf from linking to it, and it can be stripped at the end of the script.
patch -Np1 < /direwolf-hamlib.patch make enable_hamlib=
mkdir build
cd build
cmake ..
make
make install make install
cd ../.. cd ..
rm -rf direwolf rm -rf direwolf
# strip lots of generic documentation that will never be read inside a docker container # strip lots of generic documentation that will never be read inside a docker container
rm /usr/local/share/doc/direwolf/*.pdf rm /usr/local/share/doc/direwolf/*.pdf
@ -108,14 +106,9 @@ popd
rm -rf dream rm -rf dream
rm dream-2.1.1-svn808.tar.gz rm dream-2.1.1-svn808.tar.gz
git clone https://github.com/mobilinkd/m17-cxx-demod.git git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols
cmakebuild m17-cxx-demod v2.3 pushd /opt/aprs-symbols
git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols
pushd /usr/share/aprs-symbols
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802 git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
# remove unused files (including git meta information)
rm -rf .git aprs-symbols.ai aprs-sym-export.js
popd popd
apt-get -y purge --autoremove $BUILD_PACKAGES apt-get -y purge --autoremove $BUILD_PACKAGES

View File

@ -18,43 +18,30 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libfftw3-bin libprotobuf23 libsamplerate0 libicu67 libudev1" STATIC_PACKAGES="libfftw3-bin"
BUILD_PACKAGES="git autoconf automake libtool libfftw3-dev pkg-config cmake make gcc g++ libprotobuf-dev protobuf-compiler libsamplerate-dev libicu-dev libpython3-dev libudev-dev" BUILD_PACKAGES="git autoconf automake libtool libfftw3-dev pkg-config cmake make gcc g++"
apt-get update apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/jketterl/js8py.git git clone https://github.com/jketterl/js8py.git
pushd js8py pushd js8py
# latest develop as of 2022-11-30 (structured callsign data) git checkout 0.1.0
git checkout f7e394b7892d26cbdcce5d43c0b4081a2a6a48f6
python3 setup.py install python3 setup.py install
popd popd
rm -rf js8py rm -rf js8py
git clone https://github.com/jketterl/csdr.git git clone https://github.com/jketterl/csdr.git
cmakebuild csdr 0.18.1 cd csdr
git checkout 0.17.0
git clone https://github.com/jketterl/pycsdr.git autoreconf -i
cd pycsdr ./configure
git checkout 0.18.1 make
./setup.py install install_headers make install
cd .. cd ..
rm -rf pycsdr rm -rf csdr
git clone https://github.com/jketterl/codecserver.git
mkdir -p /usr/local/etc/codecserver
cp codecserver/conf/codecserver.conf /usr/local/etc/codecserver
cmakebuild codecserver 0.2.0
git clone https://github.com/jketterl/digiham.git git clone https://github.com/jketterl/digiham.git
cmakebuild digiham 0.6.1 cmakebuild digiham 0.3.0
git clone https://github.com/jketterl/pydigiham.git
cd pydigiham
git checkout 0.6.1
./setup.py install
cd ..
rm -rf pydigiham
apt-get -y purge --autoremove $BUILD_PACKAGES apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean apt-get clean

View File

@ -1,25 +1,19 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
mkdir -p /etc/openwebrx/openwebrx.conf.d mkdir -p /etc/openwebrx/
mkdir -p /var/lib/openwebrx
mkdir -p /tmp/openwebrx/ mkdir -p /tmp/openwebrx/
if [[ ! -f /etc/openwebrx/openwebrx.conf.d/20-temporary-directory.conf ]] ; then if [[ ! -f /etc/openwebrx/config_webrx.py ]] ; then
cat << EOF > /etc/openwebrx/openwebrx.conf.d/20-temporary-directory.conf sed 's/temporary_directory = "\/tmp"/temporary_directory = "\/tmp\/openwebrx"/' < "/opt/openwebrx/config_webrx.py" > "/etc/openwebrx/config_webrx.py"
[core]
temporary_directory = /tmp/openwebrx
EOF
fi fi
if [[ ! -f /etc/openwebrx/bands.json ]] ; then if [[ ! -f /etc/openwebrx/bands.json ]] ; then
cp bands.json /etc/openwebrx/ cp bands.json /etc/openwebrx/
fi fi
if [[ ! -f /etc/openwebrx/openwebrx.conf ]] ; then if [[ ! -f /etc/openwebrx/bookmarks.json ]] ; then
cp openwebrx.conf /etc/openwebrx/ cp bookmarks.json /etc/openwebrx/
fi fi
if [[ ! -z "${OPENWEBRX_ADMIN_USER:-}" ]] && [[ ! -z "${OPENWEBRX_ADMIN_PASSWORD:-}" ]] ; then if [[ ! -f /etc/openwebrx/users.json ]] ; then
if ! python3 openwebrx.py admin --silent hasuser "${OPENWEBRX_ADMIN_USER}" ; then cp users.json /etc/openwebrx/
OWRX_PASSWORD="${OPENWEBRX_ADMIN_PASSWORD}" python3 openwebrx.py admin --noninteractive adduser "${OPENWEBRX_ADMIN_USER}"
fi
fi fi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,166 +1,14 @@
@import url("openwebrx-header.css"); @import url("openwebrx-header.css");
@import url("openwebrx-globals.css"); @import url("openwebrx-globals.css");
html, body {
height: unset;
}
body {
margin-bottom: 5rem;
}
hr {
background: #444;
}
.buttons { .buttons {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #222;
z-index: 2;
padding: 10px;
text-align: right; text-align: right;
border-top: 1px solid #444;
} }
.row .map-input { .row .map-input {
margin: 15px 15px 0; margin: 15px 15px 0;
} }
.settings-section h3 { .device {
margin-top: 1em; margin-top: 20px;
margin-bottom: 1em;
}
h1 {
margin: 1em 0;
text-align: center;
}
.matrix {
display: grid;
}
.q65-matrix {
grid-template-columns: repeat(5, auto);
}
.imageupload .image-container {
max-width: 100%;
padding: 7px;
}
.imageupload img.webrx-top-photo {
max-height: 350px;
max-width: 100%;
}
.settings-grid > div {
padding: 20px;
}
.settings-grid .btn {
width: 100%;
height: 100px;
padding: 20px;
font-size: 1.2rem;
}
.tab-body {
overflow: auto;
border: 1px solid #444;
border-top: none;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
.tab-body .form-group {
padding-right: 15px;
}
.bookmarks table .frequency, .bookmark-list table .frequency {
text-align: right;
}
.bookmarks table input, .bookmarks table select {
width: initial;
text-align: inherit;
display: initial;
}
.bookmark-list table .form-check-input {
margin-left: 0;
}
.actions {
margin: 1rem 0;
}
.actions .btn {
width: 100%;
}
.wsjt-decoding-depths-table {
width: auto;
margin: 0;
}
.wsjt-decoding-depths-table td:first-child {
padding-left: 0;
}
.sdr-device-list .list-group-item,
.sdr-profile-list .list-group-item {
background: initial;
}
.sdr-device-list .sdr-profile-list {
max-height: 20rem;
overflow-y: auto;
}
.removable-group.removable, .add-group {
display: flex;
flex-direction: row;
}
.removable-group.removable .removable-item, .add-group .add-group-select {
flex: 1 0 auto;
margin-right: .25rem;
}
.removable-group.removable .option-remove-button, .add-group .option-add-button {
flex: 0 0 70px;
}
.option-add-button, .option-remove-button {
width: 70px;
}
.scheduler-static-time-inputs {
display: flex;
flex-direction: row;
}
.scheduler-static-time-inputs > * {
flex: 0 0 auto;
width: unset;
}
.scheduler-static-time-inputs > select {
flex: 1 0 auto;
}
.breadcrumb {
margin-top: .5rem;
}
.imageupload.is-invalid ~ .invalid-feedback {
display: block;
}
.device-log-messages {
max-height: 500px;
} }

7
htdocs/css/features.css Normal file
View File

@ -0,0 +1,7 @@
@import url("openwebrx-header.css");
@import url("openwebrx-globals.css");
h1 {
text-align: center;
margin: 50px 0;
}

View File

@ -1,16 +1,6 @@
@import url("openwebrx-header.css"); @import url("openwebrx-header.css");
@import url("openwebrx-globals.css"); @import url("openwebrx-globals.css");
body {
display: flex;
flex-direction: column;
}
.login-container {
flex: 1;
position: relative;
}
.login { .login {
position: absolute; position: absolute;
left: 50%; left: 50%;

View File

@ -6,6 +6,10 @@ body {
flex-direction: column; flex-direction: column;
} }
#webrx-top-container {
flex: none;
}
.openwebrx-map { .openwebrx-map {
flex: 1 1 auto; flex: 1 1 auto;
} }
@ -21,18 +25,10 @@ ul {
padding-inline-start: 25px; padding-inline-start: 25px;
} }
/* don't show the filter in it's initial position */
.openwebrx-map-legend { .openwebrx-map-legend {
display: none;
background-color: #fff; background-color: #fff;
padding: 10px; padding: 10px;
margin: 10px; margin: 10px;
user-select: none;
}
/* show it as soon as google maps has moved it to its container */
.openwebrx-map .openwebrx-map-legend {
display: block;
} }
.openwebrx-map-legend ul { .openwebrx-map-legend ul {
@ -40,15 +36,6 @@ ul {
padding: 0; padding: 0;
} }
.openwebrx-map-legend ul li {
cursor: pointer;
}
.openwebrx-map-legend ul li.disabled {
opacity: .3;
filter: grayscale(70%);
}
.openwebrx-map-legend li.square .illustration { .openwebrx-map-legend li.square .illustration {
display: inline-block; display: inline-block;
width: 30px; width: 30px;

View File

@ -5,3 +5,21 @@ html, body
height: 100%; height: 100%;
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
} }
.sprite {
background-image: url(../gfx/openwebrx-sprites.png);
display: inline-block;
}
.openwebrx-button.highlighted .sprite {
background-image: linear-gradient(rgba(255,127,0,0.5), rgba(255,127,0,0.5)), url(../gfx/openwebrx-sprites.png);
background-blend-mode: overlay;
}
@media only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and (min-device-pixel-ratio: 2) {
.sprite {
background-image: url(../gfx/openwebrx-sprites-2x.png);
background-size: 198px 77px;
}
}

View File

@ -1,36 +1,32 @@
.webrx-top-container { #webrx-top-container
{
position: relative; position: relative;
z-index:1000; z-index:1000;
background-color: #575757; background-color: #575757;
}
background-image: url(../gfx/openwebrx-top-photo.jpg); #webrx-top-photo
background-position-x: center; {
background-position-y: top; width: 100%;
background-repeat: no-repeat; display: block;
background-size: cover; }
#webrx-top-photo-clip
{
min-height: 67px;
max-height: 67px;
height: 350px;
overflow: hidden; overflow: hidden;
position: relative;
} }
.openwebrx-description-container { .webrx-top-bar-parts
transition-property: height, opacity; {
transition-duration: 1s;
transition-timing-function: ease-out;
opacity: 0;
height: 0;
/* originally, top-bar + description was 350px */
max-height: 283px;
overflow: hidden;
}
.openwebrx-description-container.expanded {
opacity: 1;
height: 283px;
}
.webrx-top-bar {
height:67px; height:67px;
}
#webrx-top-bar
{
background: rgba(128, 128, 128, 0.15); background: rgba(128, 128, 128, 0.15);
margin:0; margin:0;
padding:0; padding:0;
@ -41,32 +37,34 @@
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
overflow: hidden; overflow: hidden;
position: absolute;
display: flex; left: 0;
flex-direction: row; top: 0;
right: 0;
} }
.webrx-top-bar > * { #webrx-tob-container, #webrx-top-container * {
flex: 0;
}
.webrx-top-container, .webrx-top-container * {
line-height: initial; line-height: initial;
box-sizing: initial; box-sizing: initial;
} }
.webrx-top-logo { #webrx-top-container img {
width: 261px; vertical-align: initial;
padding: 12px;
filter: drop-shadow(0 0 2.5px rgba(0, 0, 0, .9));
/* overwritten by media queries */
display: none;
} }
.webrx-rx-avatar { #webrx-top-logo
{
padding: 12px;
float: left;
}
#webrx-rx-avatar
{
background-color: rgba(154, 154, 154, .5); background-color: rgba(154, 154, 154, .5);
float: left;
margin: 7px; margin: 7px;
cursor:pointer;
width: 46px; width: 46px;
height: 46px; height: 46px;
padding: 4px; padding: 4px;
@ -74,69 +72,88 @@
box-sizing: content-box; box-sizing: content-box;
} }
.webrx-rx-texts { #webrx-rx-texts {
/* minimum layout width */ float: left;
width: 0; padding: 10px;
/* will be getting wider with flex */
flex: 1;
overflow: hidden;
margin: auto 0;
} }
.webrx-rx-texts div, .webrx-rx-texts h1 { #webrx-rx-texts div {
margin: 0 10px;
padding: 3px; padding: 3px;
}
#webrx-rx-title
{
white-space:nowrap; white-space:nowrap;
overflow: hidden; overflow: hidden;
color: #909090; cursor:pointer;
text-align: left;
}
.webrx-rx-title {
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
color: #909090;
font-size: 11pt; font-size: 11pt;
font-weight: bold; font-weight: bold;
} }
.webrx-rx-desc { #webrx-rx-desc
{
white-space:nowrap;
overflow: hidden;
cursor:pointer;
font-size: 10pt; font-size: 10pt;
color: #909090;
} }
.openwebrx-main-buttons .button { #webrx-rx-desc a
{
color: #909090;
}
#openwebrx-rx-details-arrow
{
cursor:pointer;
position: absolute;
left: 470px;
top: 55px;
}
#openwebrx-rx-details-arrow a
{
margin: 0;
padding: 0;
line-height: 0;
display: block;
}
#openwebrx-main-buttons .button {
display: block; display: block;
width: 55px; width: 55px;
cursor:pointer; cursor:pointer;
} }
.openwebrx-main-buttons .button[data-toggle-panel] { #openwebrx-main-buttons .button img {
/* will be enabled by javascript if the panel is present in the DOM */
display: none;
}
.openwebrx-main-buttons .button img,
.openwebrx-main-buttons .button svg {
height: 38px; height: 38px;
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.5));
} }
.openwebrx-main-buttons a { #openwebrx-main-buttons a {
color: inherit; color: inherit;
text-decoration: inherit; text-decoration: inherit;
} }
.openwebrx-main-buttons .button:hover { #openwebrx-main-buttons .button:hover
{
background-color: rgba(255, 255, 255, 0.3); background-color: rgba(255, 255, 255, 0.3);
} }
.openwebrx-main-buttons .button:active { #openwebrx-main-buttons .button:active
{
background-color: rgba(255, 255, 255, 0.55); background-color: rgba(255, 255, 255, 0.55);
} }
.openwebrx-main-buttons { #openwebrx-main-buttons
{
padding: 5px 15px; padding: 5px 15px;
display: flex; display: flex;
list-style: none; list-style: none;
float: right;
margin:0; margin:0;
color: white; color: white;
text-shadow: 0px 0px 4px #000000; text-shadow: 0px 0px 4px #000000;
@ -145,17 +162,23 @@
font-weight: bold; font-weight: bold;
} }
.webrx-rx-photo-title { #webrx-rx-photo-title
margin: 10px 15px; {
color: white; position: absolute;
left: 15px;
top: 78px;
color: White;
font-size: 16pt; font-size: 16pt;
text-shadow: 1px 1px 4px #444; text-shadow: 1px 1px 4px #444;
opacity: 1; opacity: 1;
} }
.webrx-rx-photo-desc { #webrx-rx-photo-desc
margin: 10px 15px; {
color: white; position: absolute;
left: 15px;
top: 109px;
color: White;
font-size: 10pt; font-size: 10pt;
font-weight: bold; font-weight: bold;
text-shadow: 0px 0px 6px #444; text-shadow: 0px 0px 6px #444;
@ -163,65 +186,50 @@
line-height: 1.5em; line-height: 1.5em;
} }
.webrx-rx-photo-desc a { #webrx-rx-photo-desc a
{
color: #5ca8ff; color: #5ca8ff;
text-shadow: none; text-shadow: none;
} }
.openwebrx-photo-trigger { .sprite-panel-status {
cursor: pointer; background-position: 0 0;
width: 44px;
height: 38px;
} }
/* .sprite-panel-log {
* Responsive stuff background-position: -44px 0;
*/ width: 38px;
height: 38px;
@media (min-width: 576px) {
.webrx-rx-texts {
display: initial;
}
} }
@media (min-width: 768px) { .sprite-panel-receiver {
background-position: -82px 0;
width: 40px;
height: 38px;
} }
@media (min-width: 992px) { .sprite-panel-map {
.webrx-top-logo { background-position: -122px 0;
display: initial; width: 38px;
} height: 38px;
} }
@media (min-width: 1200px) { .sprite-panel-settings {
background-position: -160px 0;
width: 38px;
height: 38px;
} }
/* .sprite-rx-details-arrow-down {
* RX details arrow up/down switching background-position: 0 -65px;
*/ width: 43px;
.openwebrx-rx-details-arrow {
position: absolute;
bottom: 0;
left: 50%;
transform: translate(-50%, 0);
margin: 0;
padding: 0;
line-height: 0;
display: block;
}
.openwebrx-rx-details-arrow svg {
height: 12px; height: 12px;
} }
.openwebrx-rx-details-arrow .up { .sprite-rx-details-arrow-up {
display: none; background-position: -43px -65px;
} width: 43px;
height: 12px;
.openwebrx-rx-details-arrow--up .down {
display: none;
}
.openwebrx-rx-details-arrow--up .up {
display: initial;
} }

View File

@ -3,7 +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-2021 by Jakob Ketterl <dd5jfk@darc.de> Copyright (c) 2019-2020 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
@ -36,14 +36,15 @@ input
vertical-align:middle; vertical-align:middle;
} }
input[type=range] { input[type=range]
{
-webkit-appearance: none; -webkit-appearance: none;
margin: 0 0; margin: 0 0;
background: transparent !important; background: transparent;
--track-background: #B6B6B6; --track-background: #B6B6B6;
} }
input[type=range]:focus
input[type=range]:focus { {
outline: none; outline: none;
} }
@ -296,13 +297,11 @@ input[type=range]:disabled {
#webrx-canvas-container canvas #webrx-canvas-container canvas
{ {
position: absolute; position: absolute;
top: 0;
border-style: none; border-style: none;
image-rendering: crisp-edges; image-rendering: crisp-edges;
image-rendering: -webkit-optimize-contrast; image-rendering: -webkit-optimize-contrast;
width: 100%; width: 100%;
height: 200px; height: 200px;
will-change: transform;
} }
#openwebrx-log-scroll #openwebrx-log-scroll
@ -337,58 +336,12 @@ input[type=range]:disabled {
margin: 0; margin: 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
cursor: pointer;
} }
.webrx-actual-freq > * { .webrx-actual-freq > * {
flex: 1; flex: 1;
} }
.webrx-actual-freq .input-group {
display: flex;
flex-direction: row;
}
.webrx-actual-freq .input-group > * {
flex: 0 0 auto;
}
.webrx-actual-freq .input-group input {
flex: 1 0 auto;
margin-right: 0;
border-right: 1px solid #373737;
-moz-appearance: textfield;
}
.webrx-actual-freq .input-group input::-webkit-outer-spin-button,
.webrx-actual-freq .input-group input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.input-group > :not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > :not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group :first-child {
padding-left: 5px;
}
.input-group :last-child {
padding-right: 5px
}
.webrx-actual-freq .input-group input, .webrx-actual-freq .input-group select {
outline: none;
font-size: 16pt;
}
.webrx-actual-freq input { .webrx-actual-freq input {
font-family: 'roboto-mono'; font-family: 'roboto-mono';
width: 0; width: 0;
@ -402,17 +355,7 @@ input[type=range]:disabled {
.webrx-actual-freq, .webrx-actual-freq input { .webrx-actual-freq, .webrx-actual-freq input {
font-size: 16pt; font-size: 16pt;
font-family: 'roboto-mono'; font-family: 'roboto-mono';
} line-height: 22px;
.webrx-actual-freq .digit {
cursor: ns-resize;
}
.webrx-actual-freq .digit:hover {
color: #FFFF50;
border-radius: 5px;
background: -webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) );
background: -moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
} }
.webrx-mouse-freq { .webrx-mouse-freq {
@ -601,35 +544,21 @@ img.openwebrx-mirror-img
-khtml-user-select: none; -khtml-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
overflow: hidden;
z-index: 1
} }
.openwebrx-progressbar-bar { .openwebrx-progressbar-bar
background-color: #00aba6; {
border-radius: 5px; border-radius: 5px;
height: 100%; height: 100%;
width: 100%; width: 100%;
transition-property: transform, background-color;
transition-duration: 1s;
transition-timing-function: ease-in-out;
transform: translate(-100%) translateZ(0);
will-change: transform, background-color;
z-index: 0;
}
.openwebrx-progressbar--over .openwebrx-progressbar-bar {
background-color: #ff6262;
} }
.openwebrx-progressbar-text .openwebrx-progressbar-text
{ {
position: absolute; position: absolute;
left:50%; left:0px;
top:50%; top:4px;
transform: translate(-50%, -50%); width: inherit;
white-space: nowrap;
z-index: 2;
} }
#openwebrx-panel-status #openwebrx-panel-status
@ -654,7 +583,6 @@ img.openwebrx-mirror-img
#openwebrx-panel-receiver .frequencies-container { #openwebrx-panel-receiver .frequencies-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 5px;
} }
#openwebrx-panel-receiver .frequencies { #openwebrx-panel-receiver .frequencies {
@ -667,6 +595,16 @@ img.openwebrx-mirror-img
text-align: center; text-align: center;
} }
#openwebrx-mute-on
{
color: lime;
}
#openwebrx-mute-off
{
color: white;
}
.openwebrx-panel-slider .openwebrx-panel-slider
{ {
position: relative; position: relative;
@ -674,6 +612,13 @@ img.openwebrx-mirror-img
width: 95px; width: 95px;
} }
.openwebrx-sliderbtn-img
{
width: 14px;
position:relative;
top: 1px;
}
.openwebrx-panel-line .openwebrx-panel-line
{ {
padding-top: 5px; padding-top: 5px;
@ -713,7 +658,8 @@ img.openwebrx-mirror-img
} }
} }
#openwebrx-smeter { #openwebrx-smeter-outer
{
border-color: #888; border-color: #888;
border-style: solid; border-style: solid;
border-width: 0px; border-width: 0px;
@ -721,20 +667,16 @@ img.openwebrx-mirror-img
height: 7px; height: 7px;
background-color: #373737; background-color: #373737;
border-radius: 3px; border-radius: 3px;
overflow: hidden; position: relative;
} }
#openwebrx-smeter-bar
.openwebrx-smeter-bar { {
transition-property: transform; transition: all 0.2s linear;
transition-duration: 0.2s; width: 0px;
transition-timing-function: linear; height: 7px;
will-change: transform;
transform: translate(-100%) translateZ(0);
width: 100%;
height: 100%;
background: linear-gradient(to top, #ff5939 , #961700); background: linear-gradient(to top, #ff5939 , #961700);
margin: 0; position: absolute;
padding: 0; margin: 0; padding: 0; left: 0;
border-radius: 3px; border-radius: 3px;
} }
@ -750,19 +692,19 @@ img.openwebrx-mirror-img
} }
.openwebrx-overlay { .openwebrx-overlay {
position: absolute; position: fixed;
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
opacity: 0.8; opacity: 0.8;
background-color: #777; background-color: #777;
left: 0; left: 0;
top: 0; top: 0;
z-index: 1001; z-index: 1001;
color: white; color: white;
font-weight: bold; font-weight: bold;
font-size: 20pt; font-size: 20pt;
} }
#openwebrx-autoplay-overlay #openwebrx-autoplay-overlay
@ -771,7 +713,8 @@ img.openwebrx-mirror-img
transition: opacity 0.3s linear; transition: opacity 0.3s linear;
} }
#openwebrx-autoplay-overlay svg { #openwebrx-autoplay-overlay img
{
width: 150px; width: 150px;
} }
@ -803,14 +746,11 @@ img.openwebrx-mirror-img
#openwebrx-digimode-canvas-container canvas #openwebrx-digimode-canvas-container canvas
{ {
position: absolute; position: absolute;
top: 0;
pointer-events: none; pointer-events: none;
transition: width 500ms, left 500ms; transition: width 500ms, left 500ms;
will-change: transform;
} }
.openwebrx-panel select, .openwebrx-panel select,
.openwebrx-panel input,
.openwebrx-dialog select, .openwebrx-dialog select,
.openwebrx-dialog input { .openwebrx-dialog input {
border-radius: 5px; border-radius: 5px;
@ -819,26 +759,11 @@ img.openwebrx-mirror-img
font-weight: normal; font-weight: normal;
font-size: 13pt; font-size: 13pt;
margin-right: 1px; margin-right: 1px;
background:linear-gradient(#373737, #4F4F4F); background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) );
background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
border-color: transparent; border-color: transparent;
border-width: 0px; border-width: 0px;
} -moz-appearance: none;
@supports(-moz-appearance: none) {
.openwebrx-panel select,
.openwebrx-dialog select {
-moz-appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%20%20xmlns%3Av%3D%22https%3A%2F%2Fvecta.io%2Fnano%22%3E%3Cpath%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8s-1.9-9.2-5.5-12.8z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E'),
linear-gradient(#373737, #4F4F4F);
background-repeat: no-repeat, repeat;
background-position: right .3em top 50%, 0 0;
background-size: .65em auto, 100%;
}
.openwebrx-panel .input-group select,
.openwebrx-dialog .input-group select {
padding-right: 1em;
}
} }
.openwebrx-panel select option, .openwebrx-panel select option,
@ -917,6 +842,33 @@ img.openwebrx-mirror-img
z-index: 10; z-index: 10;
} }
#openwebrx-digimode-content .part
{
perspective: 700px;
}
#openwebrx-digimode-content .part
{
animation: new-digimode-data-3d 100ms;
animation-timing-function: linear;
display: inline-block;
perspective-origin: 50% 50%;
transform-origin: 0% 50%;
}
@keyframes new-digimode-data
{
0%{ opacity: 0; }
100%{ opacity: 1; }
}
@keyframes new-digimode-data-3d
{
0%{ transform: rotateX(0deg) rotateY(-90deg) translateX(-5px) scale(1.3); }
100%{ transform: rotateX(0deg) rotateY(0deg) translateX(0) scale(1); }
}
#openwebrx-digimode-select-channel #openwebrx-digimode-select-channel
{ {
transition: all 500ms; transition: all 500ms;
@ -934,44 +886,34 @@ img.openwebrx-mirror-img
border-color: Red; border-color: Red;
} }
.openwebrx-meta-panel {
display: flex;
flex-direction: row;
gap: 10px;
/* compatibility with iOS 14.2 */
flex: 0 0 auto;
}
.openwebrx-meta-slot { .openwebrx-meta-slot {
flex: 1;
width: 145px; width: 145px;
height: 196px; height: 196px;
float: left;
margin-right: 10px;
background-color: #676767; background-color: #676767;
padding: 2px 0; padding: 2px 0;
color: #333; color: #333;
text-align: center; text-align: center;
display: flex;
flex-direction: column;
position: relative; position: relative;
overflow: hidden;
} }
.openwebrx-meta-slot > * { .openwebrx-meta-slot, .openwebrx-meta-slot.muted:before {
flex: 1 0 0;
line-height: 1.2em;
}
.openwebrx-meta-slot, .openwebrx-meta-slot .mute {
-webkit-border-radius: 5px; -webkit-border-radius: 5px;
-moz-border-radius: 5px; -moz-border-radius: 5px;
border-radius: 5px; border-radius: 5px;
} }
.openwebrx-meta-slot .mute { .openwebrx-meta-slot.muted:before {
display: none; display: block;
content: "";
background-image: url("../gfx/openwebrx-mute.png");
width:100%;
height:133px;
background-position: center;
background-repeat: no-repeat;
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
@ -982,17 +924,6 @@ img.openwebrx-mirror-img
background-color: rgba(0,0,0,.3); background-color: rgba(0,0,0,.3);
} }
.openwebrx-meta-slot .mute svg {
position: absolute;
top: 50%;
left: 0;
transform: translate(0, -50%);
}
.openwebrx-meta-slot.muted .mute {
display: block;
}
.openwebrx-meta-slot.active { .openwebrx-meta-slot.active {
background-color: #95bbdf; background-color: #95bbdf;
} }
@ -1008,95 +939,41 @@ img.openwebrx-mirror-img
box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px; box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px;
} }
.openwebrx-meta-slot:last-child {
margin-right: 0;
}
.openwebrx-meta-slot .openwebrx-meta-user-image { .openwebrx-meta-slot .openwebrx-meta-user-image {
flex: 0 1 100%; width:100%;
height:133px;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
line-height: 0;
overflow: hidden;
} }
.openwebrx-meta-slot .openwebrx-meta-user-image img { .openwebrx-meta-slot.active .openwebrx-meta-user-image {
max-width: 100%; background-image: url("../gfx/openwebrx-directcall.png");
max-height: 100%;
display: none;
} }
.openwebrx-meta-slot.active.direct .openwebrx-meta-user-image .directcall, .openwebrx-meta-slot.active .openwebrx-meta-user-image.group {
.openwebrx-meta-slot.active.individual .openwebrx-meta-user-image .directcall, background-image: url("../gfx/openwebrx-groupcall.png");
#openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall,
#openwebrx-panel-metadata-dstar .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall,
#openwebrx-panel-metadata-m17 .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall {
display: initial;
}
.openwebrx-meta-slot.active.group .openwebrx-meta-user-image .groupcall,
.openwebrx-meta-slot.active.conference .openwebrx-meta-user-image .groupcall {
display: initial;
}
.openwebrx-meta-slot.group .openwebrx-dmr-target:not(:empty):before {
content: "Talkgroup: ";
}
.openwebrx-meta-slot.direct .openwebrx-dmr-target:not(:empty):before {
content: "Direct: ";
} }
.openwebrx-dmr-timeslot-panel * { .openwebrx-dmr-timeslot-panel * {
cursor: pointer; cursor: pointer;
user-select: none;
} }
.openwebrx-ysf-mode:not(:empty):before { .openwebrx-maps-pin {
content: "Mode: "; background-image: url("../gfx/google_maps_pin.svg");
} background-position: center;
background-repeat: no-repeat;
.openwebrx-ysf-up:not(:empty):before {
content: "Up: ";
}
.openwebrx-ysf-down:not(:empty):before {
content: "Down: ";
}
.openwebrx-m17-source:not(:empty):before {
content: "SRC: ";
}
.openwebrx-m17-destination:not(:empty):before {
content: "DEST: ";
}
.openwebrx-dstar-yourcall:not(:empty):before {
content: "UR: ";
}
.openwebrx-dstar-departure:not(:empty):before {
content: "RPT1: ";
}
.openwebrx-dstar-destination:not(:empty):before {
content: "RPT2: ";
}
.openwebrx-meta-slot.individual .openwebrx-nxdn-destination:not(:empty):before {
content: "Direct: ";
}
.openwebrx-meta-slot.conference .openwebrx-nxdn-destination:not(:empty):before {
content: "Conference: ";
}
.openwebrx-maps-pin svg {
width: 15px; width: 15px;
height: 15px; height: 15px;
vertical-align: middle; background-size: contain;
display: inline-block;
} }
.openwebrx-message-panel { .openwebrx-message-panel {
height: 180px; height: 180px;
position: relative;
} }
.openwebrx-message-panel tbody { .openwebrx-message-panel tbody {
@ -1262,10 +1139,6 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, #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="wspr"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
@ -1273,11 +1146,7 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-select-channel
{ {
display: none; display: none;
} }
@ -1289,61 +1158,81 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-canvas-container
{ {
height: 200px; height: 200px;
margin: -10px; margin: -10px;
} }
.openwebrx-zoom-button svg { .sprite-zoom-in {
background-position: 0 -38px;
width: 27px;
height: 27px; height: 27px;
} }
.openwebrx-slider-button svg { .sprite-zoom-out {
position:relative; background-position: -27px -38px;
top: 1px; width: 27px;
height: 27px;
}
.sprite-zoom-in-total {
background-position: -54px -38px;
width: 24px;
height: 27px;
}
.sprite-zoom-out-total {
background-position: -78px -38px;
width: 25px;
height: 27px;
}
.sprite-edit {
background-position: -131px -51px;
width: 14px;
height: 14px; height: 14px;
} }
.openwebrx-mute-button svg.muted { .sprite-trashcan {
display: none; background-position: -145px -38px;
} width: 14px;
.openwebrx-mute-button.muted svg.muted {
display: initial;
}
.openwebrx-mute-button.muted svg.unmuted {
display: none;
}
.bookmark .bookmark-actions .openwebrx-button svg {
height: 14px; height: 14px;
} }
#openwebrx-waterfall-colors-auto .continuous { .sprite-speaker {
display: none; width: 14px;
height: 15px;
} }
#openwebrx-waterfall-colors-auto.highlighted .continuous { #openwebrx-mute-on .sprite-speaker {
display: initial; background-position: -117px -38px;
} }
#openwebrx-waterfall-colors-auto.highlighted .auto { #openwebrx-mute-off .sprite-speaker {
display: none; background-position: -103px -38px;
} }
.openwebrx-waterfall-container { .sprite-squelch {
flex-grow: 1; background-position: -131px -38px;
display: flex; width: 14px;
flex-direction: column; height: 13px;
position: relative;
} }
.openwebrx-waterfall-container > * { .sprite-waterfall-auto {
flex: 0 0 auto; background-position: -103px -53px;
width: 14px;
height: 11px;
}
.sprite-waterfall-default {
background-position: -117px -53px;
width: 14px;
height: 12px;
}
.sprite-bookmark {
background-position: -159px -38px;
width: 21px;
height: 27px;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 318 B

View File

@ -3,6 +3,7 @@
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" /> <link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" /> <link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<link rel="stylesheet" href="static/css/features.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script>
<script src="static/lib/jquery-3.2.1.min.js"></script> <script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script> <script src="static/lib/Header.js"></script>
@ -10,7 +11,6 @@
</HEAD><BODY> </HEAD><BODY>
${header} ${header}
<div class="container"> <div class="container">
${breadcrumb}
<h1>OpenWebRX Feature Report</h1> <h1>OpenWebRX Feature Report</h1>
<table class="features table"> <table class="features table">
<tr> <tr>
@ -20,6 +20,5 @@
<th>Available</th> <th>Available</th>
</tr> </tr>
</table> </table>
${breadcrumb}
</div> </div>
</BODY></HTML> </BODY></HTML>

View File

@ -13,7 +13,8 @@ $(function(){
}); });
$table.append( $table.append(
'<tr>' + '<tr>' +
'<td colspan=3>' + name + '</td>' + '<td colspan=2>' + name + '</td>' +
'<td>' + converter.makeHtml(details.description) + '</td>' +
'<td>' + (details.available ? 'YES' : 'NO') + '</td>' + '<td>' + (details.available ? 'YES' : 'NO') + '</td>' +
'</tr>' + '</tr>' +
requirements.join("") requirements.join("")

View File

@ -0,0 +1,20 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Settings</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<script src="https://unpkg.com/location-picker/dist/location-picker.min.js"></script>
<script src="compiled/settings.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="container">
<div class="col-12">
<h1>General settings</h1>
</div>
${sections}
</div>
</body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
@ -12,8 +14,8 @@
viewBox="0 0 20 34.892337" viewBox="0 0 20 34.892337"
id="svg3455" id="svg3455"
version="1.1" version="1.1"
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)" inkscape:version="0.91 r13725"
sodipodi:docname="google_maps_pin.svg"> sodipodi:docname="Map Pin.svg">
<defs <defs
id="defs3457" /> id="defs3457" />
<sodipodi:namedview <sodipodi:namedview
@ -29,16 +31,15 @@
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1" inkscape:current-layer="layer1"
showgrid="false" showgrid="false"
inkscape:window-width="2560" inkscape:window-width="1024"
inkscape:window-height="1381" inkscape:window-height="705"
inkscape:window-x="0" inkscape:window-x="-4"
inkscape:window-y="348" inkscape:window-y="-4"
inkscape:window-maximized="1" inkscape:window-maximized="1"
fit-margin-top="0" fit-margin-top="0"
fit-margin-left="0" fit-margin-left="0"
fit-margin-right="0" fit-margin-right="0"
fit-margin-bottom="0" fit-margin-bottom="0" />
inkscape:document-rotation="0" />
<metadata <metadata
id="metadata3460"> id="metadata3460">
<rdf:RDF> <rdf:RDF>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1 +0,0 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg"><defs><filter id="a" x="-.25" y="-.25" width="1.5" height="1.5" color-interpolation-filters="sRGB"><feFlood flood-color="#000" flood-opacity=".4" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1"/><feGaussianBlur in="composite1" result="blur" stdDeviation="66.6"/><feOffset result="offset"/><feComposite in="SourceGraphic" in2="offset" result="composite2"/></filter></defs><path d="M550.98 541.91c-.99-28.904-4.377-57.939-9.421-86.393-6.111-34.469-13.889-85.002-43.983-107.46-17.404-12.988-39.941-17.249-59.865-25.081-9.697-3.81-18.384-7.594-26.537-11.901-27.518 30.176-63.4 45.962-105.19 45.964-41.774 0-77.652-15.786-105.17-45.964-8.153 4.308-16.84 8.093-26.537 11.901-19.924 7.832-42.461 12.092-59.863 25.081-30.096 22.463-37.873 72.996-43.983 107.46-5.045 28.454-8.433 57.489-9.422 86.393-.766 22.387 10.288 25.525 29.017 32.284 23.453 8.458 47.666 14.737 72.041 19.884 47.077 9.941 95.603 17.582 143.92 17.924 48.318-.343 96.844-7.983 143.92-17.924 24.375-5.145 48.59-11.424 72.041-19.884 18.736-6.757 29.789-9.895 29.023-32.284zM306 325.99c90.56-.01 123.15-90.68 131.68-165.17C448.19 69.06 404.8 0 306 0c-98.78 0-142.19 69.055-131.68 160.82C182.86 235.304 215.434 326 306 325.99z" filter="url(#a)" transform="matrix(.42446 0 0 .42484 70.12 69)" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1 +0,0 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg"><defs><filter id="a" x="-.25" y="-.25" width="1.5" height="1.5" color-interpolation-filters="sRGB"><feFlood flood-color="#000" flood-opacity=".4" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1"/><feGaussianBlur in="composite1" result="blur" stdDeviation="66.6"/><feOffset result="offset"/><feComposite in="SourceGraphic" in2="offset" result="composite2"/></filter></defs><g fill="#fff"><g transform="matrix(.42446 0 0 .42484 129.12 42)" filter="url(#a)"><path d="M27.982.002c-98.778 0-142.188 69.056-131.678 160.823 8.54 74.484 41.112 165.183 131.678 165.173 90.558-.01 123.148-90.682 131.678-165.173C170.17 69.063 126.781.002 27.982.002zm277.996 0C207.2.002 163.79 69.058 174.3 160.825c8.54 74.484 41.113 165.183 131.678 165.173 90.559-.01 123.148-90.682 131.678-165.173C448.166 69.063 404.777.002 305.978.002zM-77.209 311.093c-8.153 4.308-16.84 8.09-26.537 11.898-19.924 7.833-42.463 12.095-59.863 25.084-30.095 22.463-37.871 72.996-43.98 107.46-5.045 28.454-8.435 57.492-9.424 86.395-.766 22.388 10.288 25.523 29.015 32.284 23.453 8.458 47.665 14.737 72.04 19.884 47.075 9.941 95.6 17.583 143.916 17.925 46.64-.33 93.461-7.487 138.998-16.923 45.538 9.437 92.359 16.593 138.999 16.923 48.317-.343 96.841-7.984 143.917-17.925 24.374-5.145 48.593-11.424 72.043-19.884 18.736-6.757 29.786-9.894 29.02-32.284h.01c-.99-28.903-4.38-57.941-9.424-86.395-6.111-34.47-13.886-85.002-43.98-107.46-17.404-12.989-39.94-17.252-59.863-25.084-9.697-3.81-18.384-7.59-26.537-11.898-27.517 30.177-63.398 45.962-105.186 45.965-41.773 0-77.65-15.787-105.17-45.965-8.153 4.308-16.84 8.09-26.537 11.898-2.394.941-4.828 1.826-7.284 2.685-2.456-.859-4.89-1.744-7.284-2.685-9.697-3.81-18.383-7.59-26.537-11.898-27.517 30.177-63.397 45.962-105.186 45.965-41.773 0-77.65-15.787-105.17-45.965z"/></g><g transform="matrix(.42446 0 0 .42484 70.12 102)" filter="url(#a)"><path d="M550.98 541.91c-.99-28.904-4.377-57.939-9.421-86.393-6.111-34.469-13.889-85.002-43.983-107.46-17.404-12.988-39.941-17.249-59.865-25.081-9.697-3.81-18.384-7.594-26.537-11.901-27.518 30.176-63.4 45.962-105.19 45.964-41.774 0-77.652-15.786-105.17-45.964-8.153 4.308-16.84 8.093-26.537 11.901-19.924 7.832-42.461 12.092-59.863 25.081-30.096 22.463-37.873 72.996-43.983 107.46-5.045 28.454-8.433 57.489-9.422 86.393-.766 22.387 10.288 25.525 29.017 32.284 23.453 8.458 47.666 14.737 72.041 19.884 47.077 9.941 95.603 17.582 143.92 17.924 48.318-.343 96.844-7.983 143.92-17.924 24.375-5.145 48.59-11.424 72.041-19.884 18.736-6.757 29.789-9.895 29.023-32.284zM306 325.99c90.56-.01 123.15-90.68 131.68-165.17C448.19 69.06 404.8 0 306 0c-98.78 0-142.19 69.055-131.68 160.82C182.86 235.304 215.434 326 306 325.99z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Some files were not shown because too many files have changed in this diff Show More