Compare commits
185 Commits
Author | SHA1 | Date | |
---|---|---|---|
a37aec3bdf | |||
1cec386c18 | |||
ce39de14e8 | |||
3975073efd | |||
f31685e4e7 | |||
a856c27fe4 | |||
0435225a29 | |||
be757c7968 | |||
9b977ac878 | |||
37344c0cb8 | |||
ff25fa25dd | |||
ac0e44857c | |||
9f17f1bc17 | |||
1faa61ad50 | |||
815831b1ed | |||
6c70e19c63 | |||
4a8e9472ab | |||
5d4f3b8d90 | |||
f37c7baefb | |||
efca3520ab | |||
cc385f851f | |||
349604ac50 | |||
eaaa214dc9 | |||
e3e94ad14e | |||
c1347de1f0 | |||
71a2352d2b | |||
34414de4e5 | |||
ff34e793a0 | |||
31295efbff | |||
a3285d5943 | |||
b9e19421c1 | |||
6a6d4a3c9b | |||
82825fee41 | |||
2018dd444f | |||
35243fb62e | |||
fa08f1e2cf | |||
e10a52b39e | |||
c947204356 | |||
994bf7439b | |||
97f3642262 | |||
0e8aece991 | |||
39a473c8c2 | |||
b9e6ffe03d | |||
9f9a5ceaa3 | |||
36cf6097b3 | |||
45c0d05fec | |||
3cd6af9ef9 | |||
d12af6d203 | |||
5f5cafe5ca | |||
d45cc207ad | |||
6e3a13e0d2 | |||
0d6e9a5b9f | |||
7d509eeb48 | |||
87ba4ea524 | |||
42f975a926 | |||
63c31eba22 | |||
626fa7681b | |||
d412d482b2 | |||
cf2f7377ab | |||
6c8cadace6 | |||
320f64a611 | |||
bfc3684d75 | |||
19a4a37144 | |||
f2d284989b | |||
3f01fc6d67 | |||
d4396cc61a | |||
298da694ca | |||
a5bc7850a0 | |||
f6e0cf2b71 | |||
9a5286ca24 | |||
e10143b6db | |||
6fe41f8e02 | |||
e8068a8795 | |||
e8ee94d13b | |||
2411929455 | |||
bec02795b8 | |||
b5bc63e76b | |||
1aa487ff1a | |||
f47ebb2adb | |||
f90670f477 | |||
95ac5aeb7d | |||
9be0664e14 | |||
805039ec02 | |||
322ebb1baa | |||
32105538c5 | |||
820ca16cd9 | |||
45e3c910da | |||
d609acc6aa | |||
02b4822be8 | |||
c16a1b4726 | |||
d1cea95eb4 | |||
53eefa7c80 | |||
b06732dbf5 | |||
22feb8dd1c | |||
56f976e495 | |||
f830c7efa6 | |||
04d6515337 | |||
f78a68d53f | |||
c8687f2f8d | |||
1884b89a6e | |||
008787a938 | |||
f41814c6ca | |||
055269504b | |||
dea5b15656 | |||
6650438d2f | |||
4204e4d9e2 | |||
9e41d49d46 | |||
6aa25760c5 | |||
1bff6d1289 | |||
23c69fb5a3 | |||
b158e0d17d | |||
c9dd33ba57 | |||
bc000451cc | |||
47da9a9d70 | |||
66703cb5e1 | |||
0066b4dbfd | |||
18d8b81f70 | |||
8d52bde6b0 | |||
dd3bf121c1 | |||
cfc3f926fe | |||
6f8c8a3b66 | |||
1c2125f969 | |||
0030c6d656 | |||
7e5ea6e065 | |||
49383e757f | |||
0cd0a1085a | |||
5bc69b6fa4 | |||
ddb5fe51b3 | |||
56debcd08a | |||
de34856d57 | |||
80c25f459c | |||
ccb322016e | |||
08ba0c7b02 | |||
7f57e4f45c | |||
f0b3a50c23 | |||
e51dbac2c5 | |||
f4c43ffab6 | |||
69a12650d2 | |||
8c5a7a087f | |||
5a938b8c0b | |||
448e266097 | |||
da3f59fb9b | |||
ef2ec1e1c5 | |||
031c937c0c | |||
c6ec21747b | |||
b54be3384d | |||
62ee2ca445 | |||
03b2f83981 | |||
20f0a5cd6c | |||
640f438c4c | |||
b068fb5756 | |||
645ace75c3 | |||
0518ff9358 | |||
a65fd7916e | |||
a77108dd0c | |||
7234ff4309 | |||
7ea8c8f7c6 | |||
c8e5b4f822 | |||
780d51286a | |||
2252547fc1 | |||
7e5409160e | |||
9b187140ff | |||
77ae13723d | |||
9efc839128 | |||
660301a43b | |||
11fd918d62 | |||
de67d36cd6 | |||
1f8b2f7909 | |||
d9bc03d1fc | |||
369a61ec59 | |||
c54f19282a | |||
174e9afa7b | |||
e53f1f60eb | |||
7eb0a8cf7e | |||
0e6518915d | |||
e0129fd0f7 | |||
929cf5e230 | |||
d6512e0a86 | |||
480b728c06 | |||
9e323a08ff | |||
75f4f0bfe0 | |||
2eece08d27 | |||
b930bb432d | |||
83ff417f4d | |||
4f36df6324 |
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Installation method**
|
||||
How did you install OpenWebRX? (Raspberry Pi SD card image, Debian / Ubuntu packages, Docker image, manually?)
|
||||
|
||||
**Versions**
|
||||
What version of OpenWebRX are you running? (Check on startup, or see `owrx/version.py`. If a `-dev` version is used, ideally state the commit the issue is appearing on)
|
||||
|
||||
**Log messages**
|
||||
Are there any relevant messages relating to the bug in the output / log of OpenWebRX? (On most installations, the log should be available using the command `sudo journalctl -u openwebrx`)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: General support request or other project-relasted question
|
||||
url: https://groups.io/g/openwebrx
|
||||
about: Request help on the community mailing list
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before posting a new feature request, please check if a similar idea has already been listed
|
||||
* on the issue tracker
|
||||
* on the [OpenWebRX github project](https://github.com/users/jketterl/projects/1).
|
||||
|
||||
In the latter case, please only proceed if you have additional information about the feature, and please let us know that there's already a card there.
|
||||
|
||||
**Feature description**
|
||||
Please describe in plain words what functionality you'd like to see in OpenWebRX, and why you think it's useful.
|
||||
|
||||
**Target audience**
|
||||
Please let us know if you think that this feature is of particular interest for a particular group of users (e.g. hams, SWLs, DXers, ...)
|
14
CHANGELOG.md
14
CHANGELOG.md
@ -1,3 +1,17 @@
|
||||
**unreleased**
|
||||
- Added the ability to sign multiple keys in a single request, thus enabling multiple users to claim a single receiver
|
||||
on receiverbook.de
|
||||
- Fixed file descriptor leaks to prevent "too many open files" errors
|
||||
- Add new demodulator chain for FreeDV
|
||||
- Added new HD audio streaming mode along with a new WFM demodulator
|
||||
- Reworked AGC code for better results in AM, SSB and digital modes
|
||||
- Added support for demodulation of "Digital Radio Mondiale" (DRM) broadcast using the "dream" decoder.
|
||||
- New default waterfall color scheme
|
||||
- Prototype of a continuous automatic waterfall calibration mode
|
||||
- New devices supported:
|
||||
- FunCube Dongle Pro+ (`"type": "fcdpp"`)
|
||||
- Support for connections to rtl_tcp (`"type": "rtl_tcp"`)
|
||||
|
||||
**0.19.1**
|
||||
- Added ability to authenticate receivers with listing sites using "receiver id" tokens
|
||||
|
||||
|
20
bands.json
20
bands.json
@ -4,7 +4,7 @@
|
||||
"lower_bound": 1810000,
|
||||
"upper_bound": 2000000,
|
||||
"frequencies": {
|
||||
"psk31": 1838000,
|
||||
"bpsk31": 1838000,
|
||||
"ft8": 1840000,
|
||||
"wspr": 1836600,
|
||||
"jt65": 1838000,
|
||||
@ -17,7 +17,7 @@
|
||||
"lower_bound": 3500000,
|
||||
"upper_bound": 3800000,
|
||||
"frequencies": {
|
||||
"psk31": 3580000,
|
||||
"bpsk31": 3580000,
|
||||
"ft8": 3573000,
|
||||
"wspr": 3592600,
|
||||
"jt65": 3570000,
|
||||
@ -40,7 +40,7 @@
|
||||
"lower_bound": 7000000,
|
||||
"upper_bound": 7200000,
|
||||
"frequencies": {
|
||||
"psk31": 7040000,
|
||||
"bpsk31": 7040000,
|
||||
"ft8": 7074000,
|
||||
"wspr": 7038600,
|
||||
"jt65": 7076000,
|
||||
@ -54,7 +54,7 @@
|
||||
"lower_bound": 10100000,
|
||||
"upper_bound": 10150000,
|
||||
"frequencies": {
|
||||
"psk31": 10141000,
|
||||
"bpsk31": 10141000,
|
||||
"ft8": 10136000,
|
||||
"wspr": 10138700,
|
||||
"jt65": 10138000,
|
||||
@ -68,7 +68,7 @@
|
||||
"lower_bound": 14000000,
|
||||
"upper_bound": 14350000,
|
||||
"frequencies": {
|
||||
"psk31": 14070000,
|
||||
"bpsk31": 14070000,
|
||||
"ft8": 14074000,
|
||||
"wspr": 14095600,
|
||||
"jt65": 14076000,
|
||||
@ -82,7 +82,7 @@
|
||||
"lower_bound": 18068000,
|
||||
"upper_bound": 18168000,
|
||||
"frequencies": {
|
||||
"psk31": 18098000,
|
||||
"bpsk31": 18098000,
|
||||
"ft8": 18100000,
|
||||
"wspr": 18104600,
|
||||
"jt65": 18102000,
|
||||
@ -96,7 +96,7 @@
|
||||
"lower_bound": 21000000,
|
||||
"upper_bound": 21450000,
|
||||
"frequencies": {
|
||||
"psk31": 21070000,
|
||||
"bpsk31": 21070000,
|
||||
"ft8": 21074000,
|
||||
"wspr": 21094600,
|
||||
"jt65": 21076000,
|
||||
@ -110,7 +110,7 @@
|
||||
"lower_bound": 24890000,
|
||||
"upper_bound": 24990000,
|
||||
"frequencies": {
|
||||
"psk31": 24920000,
|
||||
"bpsk31": 24920000,
|
||||
"ft8": 24915000,
|
||||
"wspr": 24924600,
|
||||
"jt65": 24917000,
|
||||
@ -124,7 +124,7 @@
|
||||
"lower_bound": 28000000,
|
||||
"upper_bound": 29700000,
|
||||
"frequencies": {
|
||||
"psk31": [28070000, 28120000],
|
||||
"bpsk31": [28070000, 28120000],
|
||||
"ft8": 28074000,
|
||||
"wspr": 28124600,
|
||||
"jt65": 28076000,
|
||||
@ -138,7 +138,7 @@
|
||||
"lower_bound": 50030000,
|
||||
"upper_bound": 51000000,
|
||||
"frequencies": {
|
||||
"psk31": 50305000,
|
||||
"bpsk31": 50305000,
|
||||
"ft8": 50313000,
|
||||
"wspr": 50293000,
|
||||
"jt65": 50310000,
|
||||
|
16
build.sh
16
build.sh
@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
. docker/env
|
||||
|
||||
docker build --pull -t openwebrx-base:$ARCHTAG -f docker/Dockerfiles/Dockerfile-base .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t openwebrx-soapysdr-base:$ARCHTAG -f docker/Dockerfiles/Dockerfile-soapysdr .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-sdrplay:$ARCHTAG -f docker/Dockerfiles/Dockerfile-sdrplay .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-hackrf:$ARCHTAG -f docker/Dockerfiles/Dockerfile-hackrf .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-airspy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-airspy .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr-soapy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr-soapy .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-plutosdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-plutosdr .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-limesdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-limesdr .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-soapyremote:$ARCHTAG -f docker/Dockerfiles/Dockerfile-soapyremote .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-perseus:$ARCHTAG -f docker/Dockerfiles/Dockerfile-perseus .
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-full:$ARCHTAG -t jketterl/openwebrx:$ARCHTAG -f docker/Dockerfiles/Dockerfile-full .
|
@ -33,7 +33,7 @@ config_webrx: configuration options for OpenWebRX
|
||||
"""
|
||||
|
||||
# configuration version. please only modify if you're able to perform the associated migration steps.
|
||||
version = 2
|
||||
version = 3
|
||||
|
||||
# NOTE: you can find additional information about configuring OpenWebRX in the Wiki:
|
||||
# https://github.com/jketterl/openwebrx/wiki/Configuration-guide
|
||||
@ -82,8 +82,15 @@ fft_voverlap_factor = (
|
||||
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 = 1024
|
||||
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
|
||||
@ -137,7 +144,7 @@ sdrs = {
|
||||
"70cm": {
|
||||
"name": "70cm Relais",
|
||||
"center_freq": 438800000,
|
||||
"rf_gain": 30,
|
||||
"rf_gain": 29,
|
||||
"samp_rate": 2400000,
|
||||
"start_freq": 439275000,
|
||||
"start_mod": "nfm",
|
||||
@ -145,8 +152,8 @@ sdrs = {
|
||||
"2m": {
|
||||
"name": "2m komplett",
|
||||
"center_freq": 145000000,
|
||||
"rf_gain": 30,
|
||||
"samp_rate": 2400000,
|
||||
"rf_gain": 29,
|
||||
"samp_rate": 2048000,
|
||||
"start_freq": 145725000,
|
||||
"start_mod": "nfm",
|
||||
},
|
||||
@ -247,20 +254,23 @@ sdrs = {
|
||||
|
||||
# ==== Color themes ====
|
||||
|
||||
# A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
|
||||
### 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]
|
||||
|
||||
### default theme by teejez:
|
||||
waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF]
|
||||
waterfall_min_level = -88 # in dB
|
||||
waterfall_max_level = -20
|
||||
waterfall_auto_level_margin = {"min": 5, "max": 40}
|
||||
### old theme by HA7ILM:
|
||||
# waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
|
||||
#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"]]
|
||||
|
263
csdr/csdr.py
263
csdr/csdr.py
@ -33,6 +33,8 @@ from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profi
|
||||
from owrx.js8 import Js8Profiles
|
||||
from owrx.audio import AudioChopper
|
||||
|
||||
from csdr.pipe import Pipe
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -43,7 +45,7 @@ class output(object):
|
||||
if not self.supports_type(t):
|
||||
# TODO rewrite the output mechanism in a way that avoids producing unnecessary data
|
||||
logger.warning("dumping output of type %s since it is not supported.", t)
|
||||
threading.Thread(target=self.pump(read_fn, lambda x: None)).start()
|
||||
threading.Thread(target=self.pump(read_fn, lambda x: None), name="csdr_pump_thread").start()
|
||||
return
|
||||
self.receive_output(t, read_fn)
|
||||
|
||||
@ -54,7 +56,11 @@ class output(object):
|
||||
def copy():
|
||||
run = True
|
||||
while run:
|
||||
data = read()
|
||||
data = None
|
||||
try:
|
||||
data = read()
|
||||
except ValueError:
|
||||
pass
|
||||
if data is None or (isinstance(data, bytes) and len(data) == 0):
|
||||
run = False
|
||||
else:
|
||||
@ -66,102 +72,11 @@ class output(object):
|
||||
return True
|
||||
|
||||
|
||||
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.path = path
|
||||
self.direction = direction
|
||||
self.encoding = encoding
|
||||
self.file = None
|
||||
try:
|
||||
os.unlink(path)
|
||||
except Exception:
|
||||
pass
|
||||
os.mkfifo(path)
|
||||
|
||||
def open(self):
|
||||
self.file = open(self.path, self.direction, encoding=self.encoding)
|
||||
|
||||
def close(self):
|
||||
if self.file is None:
|
||||
return
|
||||
try:
|
||||
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):
|
||||
super().open()
|
||||
with self.queueLock:
|
||||
for i in self.queue:
|
||||
self.file.write(i)
|
||||
self.file.flush()
|
||||
self.queue = None
|
||||
|
||||
def open(self):
|
||||
threading.Thread(target=self.open_and_dequeue).start()
|
||||
|
||||
def write(self, data):
|
||||
if self.file is None:
|
||||
with self.queueLock:
|
||||
self.queue.append(data)
|
||||
return
|
||||
r = self.file.write(data)
|
||||
self.file.flush()
|
||||
return r
|
||||
|
||||
def close(self):
|
||||
if self.file is None:
|
||||
logger.warning("queue %s never successfully opened - thread leak!", self.path)
|
||||
super().close()
|
||||
|
||||
|
||||
class ReadingPipe(Pipe):
|
||||
def __init__(self, path, encoding=None):
|
||||
super().__init__(path, "r", encoding=encoding)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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
|
||||
@ -185,6 +100,7 @@ class dsp(object):
|
||||
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
|
||||
@ -222,7 +138,7 @@ class dsp(object):
|
||||
|
||||
def set_temporary_directory(self, what):
|
||||
self.temporary_directory = what
|
||||
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self))
|
||||
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}"]
|
||||
@ -257,19 +173,35 @@ class dsp(object):
|
||||
if not self.output.supports_type("audio"):
|
||||
return chain
|
||||
# safe some cpu cycles... no need to decimate if decimation factor is 1
|
||||
last_decimation_block = (
|
||||
["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else []
|
||||
)
|
||||
last_decimation_block = []
|
||||
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}"]
|
||||
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
|
||||
@ -280,8 +212,11 @@ class dsp(object):
|
||||
chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "]
|
||||
elif which == "nxdn":
|
||||
chain += ["dsd -fi -i - -o - -u {unvoiced_quality} -g -1 "]
|
||||
chain += ["CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f"]
|
||||
max_gain = 5
|
||||
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"]
|
||||
@ -292,20 +227,42 @@ class dsp(object):
|
||||
]
|
||||
elif which == "ysf":
|
||||
chain += ["ysf_decoder --fifo {meta_pipe}", "mbe_synthesizer -y -f -u {unvoiced_quality}"]
|
||||
max_gain = 0.0005
|
||||
chain += [
|
||||
"digitalvoice_filter -f",
|
||||
"CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain),
|
||||
"sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
|
||||
]
|
||||
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", "csdr limit_ff", "csdr convert_f_s16"]
|
||||
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", "csdr limit_ff"]
|
||||
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 += [
|
||||
@ -322,9 +279,11 @@ class dsp(object):
|
||||
chain = ["cat {input_pipe}"]
|
||||
if which == "fft":
|
||||
chain += [
|
||||
"csdr realpart_cf",
|
||||
"csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size}",
|
||||
"csdr logpower_cf -70",
|
||||
"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}"]
|
||||
@ -362,9 +321,10 @@ class dsp(object):
|
||||
self.restart()
|
||||
|
||||
def secondary_fft_block_size(self):
|
||||
return (self.samp_rate / self.decimation) / (
|
||||
self.fft_fps * 2
|
||||
) # *2 is there because we do FFT on real signal here
|
||||
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
|
||||
@ -429,6 +389,7 @@ class dsp(object):
|
||||
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)
|
||||
|
||||
@ -496,12 +457,18 @@ class dsp(object):
|
||||
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
|
||||
@ -552,7 +519,17 @@ class dsp(object):
|
||||
|
||||
def get_decimation(self, input_rate, output_rate):
|
||||
decimation = 1
|
||||
while input_rate / (decimation + 1) >= output_rate:
|
||||
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
|
||||
@ -567,11 +544,18 @@ class dsp(object):
|
||||
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():
|
||||
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):
|
||||
@ -599,6 +583,21 @@ class dsp(object):
|
||||
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
|
||||
@ -606,6 +605,13 @@ class dsp(object):
|
||||
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"
|
||||
@ -637,6 +643,8 @@ class dsp(object):
|
||||
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))
|
||||
@ -665,7 +673,7 @@ class dsp(object):
|
||||
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() else self.squelch_level
|
||||
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)))
|
||||
|
||||
@ -680,11 +688,20 @@ class dsp(object):
|
||||
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
|
||||
@ -774,6 +791,7 @@ class dsp(object):
|
||||
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)
|
||||
@ -793,11 +811,12 @@ class dsp(object):
|
||||
logger.debug("restarting since rc = 0, self.running = true, and no modification")
|
||||
self.restart()
|
||||
|
||||
threading.Thread(target=watch_thread).start()
|
||||
threading.Thread(target=watch_thread, name="csdr_watch_thread").start()
|
||||
|
||||
if self.output.supports_type("audio"):
|
||||
audio_type = "hd_audio" if self.isHdAudio() else "audio"
|
||||
if self.output.supports_type(audio_type):
|
||||
self.output.send_output(
|
||||
"audio",
|
||||
audio_type,
|
||||
partial(
|
||||
self.process.stdout.read,
|
||||
self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(),
|
||||
@ -835,6 +854,8 @@ class dsp(object):
|
||||
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
|
||||
|
155
csdr/pipe.py
Normal file
155
csdr/pipe.py
Normal 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()
|
18
debian/changelog
vendored
18
debian/changelog
vendored
@ -1,3 +1,21 @@
|
||||
openwebrx (0.20.0) buster focal; urgency=low
|
||||
|
||||
* Added the ability to sign multiple keys in a single request, thus enabling
|
||||
multiple users to claim a single receiver on receiverbook.de
|
||||
* Fixed file descriptor leaks to prevent "too many open files" errors
|
||||
* Add new demodulator chain for FreeDV
|
||||
* Added new HD audio streaming mode along with a new WFM demodulator
|
||||
* Reworked AGC code for better results in AM, SSB and digital modes
|
||||
* Added support for demodulation of "Digital Radio Mondiale" (DRM) broadcast
|
||||
using the "dream" decoder.
|
||||
* New default waterfall color scheme
|
||||
* Prototype of a continuous automatic waterfall calibration mode
|
||||
* New devices supported:
|
||||
- FunCube Dongle Pro+ (`"type": "fcdpp"`)
|
||||
- Support for connections to rtl_tcp (`"type": "rtl_tcp"`)
|
||||
|
||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 11 Oct 2020 13:02:00 +0000
|
||||
|
||||
openwebrx (0.19.1) buster focal; urgency=low
|
||||
|
||||
* Added ability to authenticate receivers with listing sites using
|
||||
|
2
debian/control
vendored
2
debian/control
vendored
@ -10,7 +10,7 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git
|
||||
|
||||
Package: openwebrx
|
||||
Architecture: all
|
||||
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.2), python3-js8py (>= 0.1), ${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: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools
|
||||
Description: multi-user web sdr
|
||||
Open source, multi-user SDR receiver with a web interface
|
3
debian/rules
vendored
3
debian/rules
vendored
@ -3,3 +3,6 @@ export PYBUILD_NAME=openwebrx
|
||||
|
||||
%:
|
||||
dh $@ --with python3 --buildsystem=pybuild --with systemd
|
||||
|
||||
override_dh_strip_nondeterminism:
|
||||
dh_strip_nondeterminism -X.png
|
||||
|
97
docker.sh
Executable file
97
docker.sh
Executable file
@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
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"
|
||||
|
||||
usage () {
|
||||
echo "Usage: ${0} [command]"
|
||||
echo "Available commands:"
|
||||
echo " help Show this usage information"
|
||||
echo " build Build all docker images"
|
||||
echo " push Push built docker images to the docker hub"
|
||||
echo " manifest Compile the docker hub manifest (combines arm and x86 tags into one)"
|
||||
echo " tag Tag a release"
|
||||
}
|
||||
|
||||
build () {
|
||||
# build the base images
|
||||
docker build --pull -t openwebrx-base:${ARCHTAG} -f docker/Dockerfiles/Dockerfile-base .
|
||||
docker build --build-arg ARCHTAG=${ARCHTAG} -t openwebrx-soapysdr-base:${ARCHTAG} -f docker/Dockerfiles/Dockerfile-soapysdr .
|
||||
|
||||
for image in ${IMAGES}; do
|
||||
i=${image:10}
|
||||
# "openwebrx" is a special image that gets tag-aliased later on
|
||||
if [[ ! -z "${i}" ]] ; then
|
||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/${image}:${ARCHTAG} -f docker/Dockerfiles/Dockerfile-${i} .
|
||||
fi
|
||||
done
|
||||
|
||||
# tag openwebrx alias image
|
||||
docker tag jketterl/openwebrx-full:${ARCHTAG} jketterl/openwebrx:${ARCHTAG}
|
||||
}
|
||||
|
||||
push () {
|
||||
for image in ${IMAGES}; do
|
||||
docker push jketterl/$image:$ARCHTAG
|
||||
done
|
||||
}
|
||||
|
||||
manifest () {
|
||||
for image in ${IMAGES}; do
|
||||
# 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}"
|
||||
IMAGE_LIST=""
|
||||
for a in $ALL_ARCHS; do
|
||||
IMAGE_LIST="$IMAGE_LIST jketterl/$image:$TAG-$a"
|
||||
done
|
||||
docker manifest create jketterl/$image:$TAG $IMAGE_LIST
|
||||
docker manifest push --purge jketterl/$image:$TAG
|
||||
done
|
||||
}
|
||||
|
||||
tag () {
|
||||
if [[ -x ${1:-} || -z ${2:-} ]] ; then
|
||||
echo "Usage: ${0} tag [SRC_TAG] [TARGET_TAG]"
|
||||
return
|
||||
fi
|
||||
|
||||
local SRC_TAG=${1}
|
||||
local TARGET_TAG=${2}
|
||||
|
||||
for image in ${IMAGES}; do
|
||||
# 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}-${TARGET_TAG}"
|
||||
IMAGE_LIST=""
|
||||
for a in ${ALL_ARCHS}; do
|
||||
docker pull jketterl/${image}:${SRC_TAG}-${a}
|
||||
docker tag jketterl/${image}:${SRC_TAG}-${a} jketterl/${image}:${TARGET_TAG}-${a}
|
||||
docker push jketterl/${image}:${TARGET_TAG}-${a}
|
||||
IMAGE_LIST="${IMAGE_LIST} jketterl/${image}:${TARGET_TAG}-${a}"
|
||||
done
|
||||
docker manifest create jketterl/${image}:${TARGET_TAG} ${IMAGE_LIST}
|
||||
docker manifest push --purge jketterl/${image}:${TARGET_TAG}
|
||||
docker pull jketterl/${image}:${TARGET_TAG}
|
||||
done
|
||||
}
|
||||
|
||||
case ${1:-} in
|
||||
build)
|
||||
build
|
||||
;;
|
||||
push)
|
||||
push
|
||||
;;
|
||||
manifest)
|
||||
manifest
|
||||
;;
|
||||
tag)
|
||||
tag ${@:2}
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
@ -1,7 +1,7 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-airspy.sh /
|
||||
COPY docker/scripts/install-dependencies-airspy.sh /
|
||||
RUN /install-dependencies-airspy.sh &&\
|
||||
rm /install-dependencies-airspy.sh
|
||||
|
||||
|
@ -1,11 +1,16 @@
|
||||
FROM debian:buster-slim
|
||||
|
||||
ADD docker/files/js8call/js8call-hamlib.patch /
|
||||
ADD docker/files/wsjtx/*.patch /
|
||||
ADD docker/scripts/install-dependencies.sh /
|
||||
COPY docker/files/js8call/js8call-hamlib.patch \
|
||||
docker/files/wsjtx/wsjtx.patch \
|
||||
docker/files/wsjtx/wsjtx-hamlib.patch \
|
||||
docker/files/dream/dream.patch \
|
||||
docker/scripts/install-dependencies.sh /
|
||||
RUN /install-dependencies.sh && \
|
||||
rm /install-dependencies.sh && \
|
||||
rm /*.patch
|
||||
COPY docker/scripts/install-owrx-tools.sh /
|
||||
RUN /install-owrx-tools.sh && \
|
||||
rm /install-owrx-tools.sh
|
||||
|
||||
ENTRYPOINT ["/init"]
|
||||
|
||||
|
8
docker/Dockerfiles/Dockerfile-fcdpp
Normal file
8
docker/Dockerfiles/Dockerfile-fcdpp
Normal file
@ -0,0 +1,8 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
COPY docker/scripts/install-dependencies-fcdpp.sh /
|
||||
RUN /install-dependencies-fcdpp.sh &&\
|
||||
rm /install-dependencies-fcdpp.sh
|
||||
|
||||
COPY . /opt/openwebrx
|
@ -1,8 +1,9 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-*.sh /
|
||||
ADD docker/files/sdrplay/install-lib.*.patch /
|
||||
COPY docker/scripts/install-dependencies-*.sh \
|
||||
docker/files/sdrplay/install-lib.*.patch \
|
||||
docker/scripts/install-connectors.sh /
|
||||
|
||||
RUN /install-dependencies-rtlsdr.sh &&\
|
||||
/install-dependencies-soapysdr.sh &&\
|
||||
@ -14,13 +15,15 @@ RUN /install-dependencies-rtlsdr.sh &&\
|
||||
/install-dependencies-limesdr.sh &&\
|
||||
/install-dependencies-soapyremote.sh &&\
|
||||
/install-dependencies-perseus.sh &&\
|
||||
/install-dependencies-fcdpp.sh &&\
|
||||
/install-dependencies-radioberry.sh &&\
|
||||
/install-dependencies-uhd.sh &&\
|
||||
/install-dependencies-redpitaya.sh &&\
|
||||
/install-connectors.sh &&\
|
||||
rm /install-dependencies-*.sh &&\
|
||||
rm /install-lib.*.patch
|
||||
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
RUN /install-connectors.sh &&\
|
||||
rm /install-lib.*.patch && \
|
||||
rm /install-connectors.sh
|
||||
|
||||
ADD docker/files/services/sdrplay /etc/services.d/sdrplay
|
||||
COPY docker/files/services/sdrplay /etc/services.d/sdrplay
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
|
@ -1,8 +1,8 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-hackrf.sh /
|
||||
COPY docker/scripts/install-dependencies-hackrf.sh /
|
||||
RUN /install-dependencies-hackrf.sh &&\
|
||||
rm /install-dependencies-hackrf.sh
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
COPY . /opt/openwebrx
|
||||
|
@ -1,8 +1,8 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-limesdr.sh /
|
||||
COPY docker/scripts/install-dependencies-limesdr.sh /
|
||||
RUN /install-dependencies-limesdr.sh &&\
|
||||
rm /install-dependencies-limesdr.sh
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
COPY . /opt/openwebrx
|
||||
|
@ -1,8 +1,8 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-perseus.sh /
|
||||
COPY docker/scripts/install-dependencies-perseus.sh /
|
||||
RUN /install-dependencies-perseus.sh &&\
|
||||
rm /install-dependencies-perseus.sh
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
COPY . /opt/openwebrx
|
||||
|
@ -1,8 +1,8 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-plutosdr.sh /
|
||||
COPY docker/scripts/install-dependencies-plutosdr.sh /
|
||||
RUN /install-dependencies-plutosdr.sh &&\
|
||||
rm /install-dependencies-plutosdr.sh
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
COPY . /opt/openwebrx
|
||||
|
8
docker/Dockerfiles/Dockerfile-radioberry
Normal file
8
docker/Dockerfiles/Dockerfile-radioberry
Normal file
@ -0,0 +1,8 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
COPY docker/scripts/install-dependencies-radioberry.sh /
|
||||
RUN /install-dependencies-radioberry.sh &&\
|
||||
rm /install-dependencies-radioberry.sh
|
||||
|
||||
COPY . /opt/openwebrx
|
8
docker/Dockerfiles/Dockerfile-redpitaya
Normal file
8
docker/Dockerfiles/Dockerfile-redpitaya
Normal 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
|
@ -1,12 +1,12 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-rtlsdr.sh /
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
COPY docker/scripts/install-dependencies-rtlsdr.sh \
|
||||
docker/scripts/install-connectors.sh /
|
||||
|
||||
RUN /install-dependencies-rtlsdr.sh &&\
|
||||
rm /install-dependencies-rtlsdr.sh &&\
|
||||
/install-connectors.sh &&\
|
||||
rm /install-connectors.sh
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
COPY . /opt/openwebrx
|
||||
|
@ -1,8 +1,8 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-rtlsdr-soapy.sh /
|
||||
COPY docker/scripts/install-dependencies-rtlsdr-soapy.sh /
|
||||
RUN /install-dependencies-rtlsdr-soapy.sh &&\
|
||||
rm /install-dependencies-rtlsdr-soapy.sh
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
COPY . /opt/openwebrx
|
||||
|
9
docker/Dockerfiles/Dockerfile-rtltcp
Normal file
9
docker/Dockerfiles/Dockerfile-rtltcp
Normal file
@ -0,0 +1,9 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-base:$ARCHTAG
|
||||
|
||||
COPY docker/scripts/install-connectors.sh /
|
||||
|
||||
RUN /install-connectors.sh &&\
|
||||
rm /install-connectors.sh
|
||||
|
||||
COPY . /opt/openwebrx
|
@ -1,12 +1,12 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-sdrplay.sh /
|
||||
ADD docker/files/sdrplay/install-lib.*.patch /
|
||||
COPY docker/scripts/install-dependencies-sdrplay.sh \
|
||||
docker/files/sdrplay/install-lib.*.patch /
|
||||
RUN /install-dependencies-sdrplay.sh &&\
|
||||
rm /install-dependencies-sdrplay.sh &&\
|
||||
rm /install-lib.*.patch
|
||||
|
||||
ADD docker/files/services/sdrplay /etc/services.d/sdrplay
|
||||
COPY docker/files/services/sdrplay /etc/services.d/sdrplay
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
COPY . /opt/openwebrx
|
||||
|
@ -1,8 +1,8 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-soapyremote.sh /
|
||||
COPY docker/scripts/install-dependencies-soapyremote.sh /
|
||||
RUN /install-dependencies-soapyremote.sh &&\
|
||||
rm /install-dependencies-soapyremote.sh
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
COPY . /opt/openwebrx
|
||||
|
@ -1,8 +1,8 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-soapysdr.sh /
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
COPY docker/scripts/install-dependencies-soapysdr.sh \
|
||||
docker/scripts/install-connectors.sh /
|
||||
RUN /install-dependencies-soapysdr.sh &&\
|
||||
rm /install-dependencies-soapysdr.sh &&\
|
||||
/install-connectors.sh &&\
|
||||
|
8
docker/Dockerfiles/Dockerfile-uhd
Normal file
8
docker/Dockerfiles/Dockerfile-uhd
Normal file
@ -0,0 +1,8 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
COPY docker/scripts/install-dependencies-uhd.sh /
|
||||
RUN /install-dependencies-uhd.sh &&\
|
||||
rm /install-dependencies-uhd.sh
|
||||
|
||||
COPY . /opt/openwebrx
|
@ -1,5 +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-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"
|
||||
TAG=${TAG:-"latest"}
|
||||
ARCHTAG="$TAG-$ARCH"
|
||||
|
96
docker/files/dream/dream.patch
Normal file
96
docker/files/dream/dream.patch
Normal file
@ -0,0 +1,96 @@
|
||||
--- dream.pro.org 2020-09-04 22:51:51.579926191 +0200
|
||||
+++ dream.pro 2020-09-04 22:52:57.609434707 +0200
|
||||
@@ -70,9 +70,6 @@
|
||||
exists(/opt/local/include/speex/speex_preprocess.h) {
|
||||
CONFIG += speexdsp
|
||||
}
|
||||
- exists(/opt/local/include/hamlib/rig.h) {
|
||||
- CONFIG += hamlib
|
||||
- }
|
||||
contains(QT_VERSION, ^4\\.7.*) {
|
||||
QT += phonon opengl svg
|
||||
DEFINES -= QWT_NO_SVG
|
||||
@@ -138,12 +135,6 @@
|
||||
packagesExist(sndfile) {
|
||||
CONFIG += sndfile
|
||||
}
|
||||
- packagesExist(hamlib) {
|
||||
- CONFIG += hamlib
|
||||
- }
|
||||
- packagesExist(gpsd) {
|
||||
- CONFIG += gps
|
||||
- }
|
||||
packagesExist(pcap) {
|
||||
CONFIG += pcap
|
||||
}
|
||||
@@ -159,14 +150,6 @@
|
||||
exists(/usr/local/include/sndfile.h) {
|
||||
CONFIG += sndfile
|
||||
}
|
||||
- exists(/usr/include/hamlib/rig.h) | \
|
||||
- exists(/usr/local/include/hamlib/rig.h) {
|
||||
- CONFIG += hamlib
|
||||
- }
|
||||
- exists(/usr/include/gps.h) | \
|
||||
- exists(/usr/local/include/gps.h) {
|
||||
- CONFIG += gps
|
||||
- }
|
||||
exists(/usr/include/pcap.h) | \
|
||||
exists(/usr/local/include/pcap.h) {
|
||||
CONFIG += pcap
|
||||
@@ -194,9 +177,6 @@
|
||||
exists($$OUT_PWD/include/speex/speex_preprocess.h) {
|
||||
CONFIG += speexdsp
|
||||
}
|
||||
- exists($$OUT_PWD/include/hamlib/rig.h) {
|
||||
- CONFIG += hamlib
|
||||
- }
|
||||
exists($$OUT_PWD/include/pcap.h) {
|
||||
CONFIG += pcap
|
||||
}
|
||||
@@ -225,7 +205,7 @@
|
||||
LIBS += -lz
|
||||
}
|
||||
}
|
||||
-exists($$OUT_PWD/include/neaacdec.h) {
|
||||
+exists(/usr/include/neaacdec.h) {
|
||||
DEFINES += HAVE_LIBFAAD \
|
||||
USE_FAAD2_LIBRARY
|
||||
LIBS += -lfaad_drm
|
||||
@@ -257,11 +237,6 @@
|
||||
win32:LIBS += libspeexdsp.lib
|
||||
message("with libspeexdsp")
|
||||
}
|
||||
-gps {
|
||||
- DEFINES += HAVE_LIBGPS
|
||||
- unix:LIBS += -lgps
|
||||
- message("with gps")
|
||||
-}
|
||||
pcap {
|
||||
DEFINES += HAVE_LIBPCAP
|
||||
unix:LIBS += -lpcap
|
||||
@@ -269,24 +244,6 @@
|
||||
win32-g++:LIBS += -lwpcap -lpacket
|
||||
message("with pcap")
|
||||
}
|
||||
-hamlib {
|
||||
- DEFINES += HAVE_LIBHAMLIB
|
||||
- macx:LIBS += -framework IOKit
|
||||
- unix:LIBS += -lhamlib
|
||||
- win32:LIBS += libhamlib-2.lib
|
||||
- HEADERS += src/util/Hamlib.h
|
||||
- SOURCES += src/util/Hamlib.cpp
|
||||
- qt {
|
||||
- HEADERS += src/util-QT/Rig.h
|
||||
- SOURCES += src/util-QT/Rig.cpp
|
||||
- }
|
||||
- gui {
|
||||
- HEADERS += src/GUI-QT/RigDlg.h
|
||||
- SOURCES += src/GUI-QT/RigDlg.cpp
|
||||
- FORMS += RigDlg.ui
|
||||
- }
|
||||
- message("with hamlib")
|
||||
-}
|
||||
qwt {
|
||||
DEFINES += QWT_NO_SVG
|
||||
macx {
|
@ -1,6 +1,6 @@
|
||||
diff -ur js8call-orig/CMake/Modules/Findhamlib.cmake js8call/CMake/Modules/Findhamlib.cmake
|
||||
--- js8call-orig/CMake/Modules/Findhamlib.cmake 2020-05-28 00:10:13.386429366 +0200
|
||||
+++ js8call/CMake/Modules/Findhamlib.cmake 2020-05-28 00:10:34.339623106 +0200
|
||||
--- js8call-orig/CMake/Modules/Findhamlib.cmake 2020-07-22 18:14:18.014499840 +0200
|
||||
+++ js8call/CMake/Modules/Findhamlib.cmake 2020-07-22 18:16:07.200375473 +0200
|
||||
@@ -78,4 +78,4 @@
|
||||
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
|
||||
# TRUE if all listed variables are TRUE
|
||||
@ -8,9 +8,9 @@ diff -ur js8call-orig/CMake/Modules/Findhamlib.cmake js8call/CMake/Modules/Findh
|
||||
-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 js8call-orig/CMakeLists.txt js8call/CMakeLists.txt
|
||||
--- js8call-orig/CMakeLists.txt 2020-05-28 00:10:13.393095987 +0200
|
||||
+++ js8call/CMakeLists.txt 2020-05-28 00:12:09.925653037 +0200
|
||||
@@ -683,7 +683,7 @@
|
||||
--- js8call-orig/CMakeLists.txt 2020-07-22 18:14:18.014499840 +0200
|
||||
+++ js8call/CMakeLists.txt 2020-07-22 18:17:55.629633825 +0200
|
||||
@@ -558,7 +558,7 @@
|
||||
#
|
||||
# libhamlib setup
|
||||
#
|
||||
@ -19,7 +19,7 @@ diff -ur js8call-orig/CMakeLists.txt js8call/CMakeLists.txt
|
||||
find_package (hamlib 3 REQUIRED)
|
||||
find_program (RIGCTL_EXE rigctl)
|
||||
find_program (RIGCTLD_EXE rigctld)
|
||||
@@ -1033,55 +1033,6 @@
|
||||
@@ -911,56 +911,6 @@
|
||||
target_link_libraries (js8 wsjt_fort wsjt_cxx Qt5::Core)
|
||||
endif (${OPENMP_FOUND} OR APPLE)
|
||||
|
||||
@ -31,6 +31,7 @@ diff -ur js8call-orig/CMakeLists.txt js8call/CMakeLists.txt
|
||||
- wsjtx.rc
|
||||
- ${WSJTX_ICON_FILE}
|
||||
- ${wsjtx_RESOURCES_RCC}
|
||||
- images.qrc
|
||||
- )
|
||||
-
|
||||
-if (WSJT_CREATE_WINMAIN)
|
||||
@ -75,7 +76,7 @@ diff -ur js8call-orig/CMakeLists.txt js8call/CMakeLists.txt
|
||||
# if (UNIX)
|
||||
# if (NOT WSJT_SKIP_MANPAGES)
|
||||
# add_subdirectory (manpages)
|
||||
@@ -1097,38 +1048,10 @@
|
||||
@@ -976,38 +926,10 @@
|
||||
#
|
||||
# installation
|
||||
#
|
||||
@ -114,7 +115,7 @@ diff -ur js8call-orig/CMakeLists.txt js8call/CMakeLists.txt
|
||||
install (FILES
|
||||
contrib/Ephemeris/JPLEPH
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
|
||||
@@ -1182,32 +1105,6 @@
|
||||
@@ -1061,32 +983,6 @@
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
--- CMakeLists.txt 2020-05-25 19:26:41.423517236 +0200
|
||||
+++ CMakeLists.txt 2020-05-25 19:11:36.116236231 +0200
|
||||
@@ -79,24 +79,6 @@
|
||||
--- CMakeLists.txt.orig 2020-07-21 20:59:55.982026645 +0200
|
||||
+++ CMakeLists.txt 2020-07-21 21:01:25.444836112 +0200
|
||||
@@ -80,24 +80,6 @@
|
||||
|
||||
include (ExternalProject)
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
-ExternalProject_Add (hamlib
|
||||
- GIT_REPOSITORY ${hamlib_repo}
|
||||
- GIT_TAG ${hamlib_TAG}
|
||||
- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/hamlib.tgz
|
||||
- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/${__hamlib_upstream}
|
||||
- URL_HASH MD5=${hamlib_md5sum}
|
||||
- UPDATE_COMMAND ./bootstrap
|
||||
- #UPDATE_COMMAND ${CMAKE_COMMAND} -E env "[ -f ./bootstrap ] && ./bootstrap"
|
||||
- PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch
|
||||
- CONFIGURE_COMMAND <SOURCE_DIR>/configure --prefix=<INSTALL_DIR> --disable-shared --enable-static --without-cxx-binding ${EXTRA_FLAGS} # LIBUSB_LIBS=${USB_LIBRARY}
|
||||
- BUILD_COMMAND $(MAKE) all V=1 # $(MAKE) is ExternalProject_Add() magic to do recursive make
|
||||
@ -25,7 +25,7 @@
|
||||
#
|
||||
# custom target to make a hamlib source tarball
|
||||
#
|
||||
@@ -128,7 +110,6 @@
|
||||
@@ -136,7 +118,6 @@
|
||||
# build and optionally install WSJT-X using the hamlib package built
|
||||
# above
|
||||
#
|
||||
@ -33,7 +33,7 @@
|
||||
ExternalProject_Add (wsjtx
|
||||
GIT_REPOSITORY ${wsjtx_repo}
|
||||
GIT_TAG ${WSJTX_TAG}
|
||||
@@ -152,7 +133,6 @@
|
||||
@@ -160,7 +141,6 @@
|
||||
DEPENDEES build
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
diff -ur wsjtx-orig/CMake/Modules/Findhamlib.cmake wsjtx/CMake/Modules/Findhamlib.cmake
|
||||
--- wsjtx-orig/CMake/Modules/Findhamlib.cmake 2020-05-27 22:41:57.774855748 +0200
|
||||
+++ wsjtx/CMake/Modules/Findhamlib.cmake 2020-05-27 22:42:35.267939882 +0200
|
||||
--- 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
|
||||
@ -8,9 +8,9 @@ diff -ur wsjtx-orig/CMake/Modules/Findhamlib.cmake wsjtx/CMake/Modules/Findhamli
|
||||
-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
|
||||
--- wsjtx-orig/CMakeLists.txt 2020-05-27 22:41:57.774855748 +0200
|
||||
+++ wsjtx/CMakeLists.txt 2020-05-27 22:58:18.905001618 +0200
|
||||
@@ -869,7 +869,7 @@
|
||||
--- wsjtx-orig/CMakeLists.txt 2020-07-21 21:10:43.124810140 +0200
|
||||
+++ wsjtx/CMakeLists.txt 2020-07-21 22:14:04.454639589 +0200
|
||||
@@ -871,7 +871,7 @@
|
||||
#
|
||||
# libhamlib setup
|
||||
#
|
||||
@ -19,7 +19,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
find_package (hamlib 3 REQUIRED)
|
||||
find_program (RIGCTL_EXE rigctl)
|
||||
find_program (RIGCTLD_EXE rigctld)
|
||||
@@ -1326,54 +1326,10 @@
|
||||
@@ -1348,53 +1348,10 @@
|
||||
|
||||
endif(WSJT_BUILD_UTILS)
|
||||
|
||||
@ -51,9 +51,9 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
-
|
||||
-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS})
|
||||
-if (APPLE)
|
||||
- target_link_libraries (wsjtx wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
|
||||
- target_link_libraries (wsjtx Qt5::SerialPort wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
|
||||
-else ()
|
||||
- target_link_libraries (wsjtx wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
|
||||
- target_link_libraries (wsjtx Qt5::SerialPort wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
|
||||
- if (OpenMP_C_FLAGS)
|
||||
- set_target_properties (wsjtx PROPERTIES
|
||||
- COMPILE_FLAGS "${OpenMP_C_FLAGS}"
|
||||
@ -69,12 +69,11 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
- )
|
||||
- endif ()
|
||||
-endif ()
|
||||
-qt5_use_modules (wsjtx SerialPort) # not sure why the interface link library syntax above doesn't work
|
||||
-
|
||||
# make a library for WSJT-X UDP servers
|
||||
# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
|
||||
add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
|
||||
@@ -1417,24 +1373,9 @@
|
||||
@@ -1437,24 +1394,9 @@
|
||||
set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
|
||||
endif (WSJT_CREATE_WINMAIN)
|
||||
|
||||
@ -99,7 +98,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
|
||||
# install (TARGETS wsjtx_udp EXPORT udp
|
||||
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
@@ -1453,12 +1394,7 @@
|
||||
@@ -1473,12 +1415,7 @@
|
||||
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
|
||||
# )
|
||||
|
||||
@ -113,7 +112,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||
)
|
||||
@@ -1471,39 +1407,6 @@
|
||||
@@ -1491,39 +1428,6 @@
|
||||
)
|
||||
endif(WSJT_BUILD_UTILS)
|
||||
|
||||
|
@ -24,7 +24,7 @@ apt-get update
|
||||
apt-get -y install --no-install-recommends $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/jketterl/owrx_connector.git
|
||||
cmakebuild owrx_connector 45ec227b38bb763b0a923a1856740f4ddf74216c
|
||||
cmakebuild owrx_connector 0.3.0
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
@ -25,16 +25,19 @@ apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/airspy/airspyone_host.git
|
||||
cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild airspyone_host 652fd7f1a8f85687641e0bd91f739694d7258ecc
|
||||
|
||||
git clone https://github.com/pothosware/SoapyAirspy.git
|
||||
cmakebuild SoapyAirspy 10d697b209e7f1acc8b2c8d24851d46170ef77e3
|
||||
|
||||
git clone https://github.com/airspy/airspyhf.git
|
||||
cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild airspyhf 8891387edddcd185e2949e9814e9ef35f46f0722
|
||||
|
||||
git clone https://github.com/pothosware/SoapyAirspyHF.git
|
||||
cmakebuild SoapyAirspyHF 81ca737bb044dd930a9de738bced1e4915491f1b
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild SoapyAirspyHF 5488dac5b44f1432ce67b40b915f7e61d3bd4853
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
32
docker/scripts/install-dependencies-fcdpp.sh
Executable file
32
docker/scripts/install-dependencies-fcdpp.sh
Executable file
@ -0,0 +1,32 @@
|
||||
#!/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="libhidapi-hidraw0 libhidapi-libusb0 libasound2"
|
||||
BUILD_PACKAGES="git cmake make gcc g++ libhidapi-dev libasound2-dev"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/pothosware/SoapyFCDPP.git
|
||||
cmakebuild SoapyFCDPP soapy-fcdpp-0.1.1
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
@ -26,13 +26,15 @@ apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/mossmann/hackrf.git
|
||||
cd hackrf
|
||||
git checkout 43e6f99fe8543094d18ff3a6550ed2066c398862
|
||||
# latest from master as of 2020-09-04
|
||||
git checkout 6e5cbda2945c3bab0e6e1510eae418eda60c358e
|
||||
cmakebuild host
|
||||
cd ..
|
||||
rm -rf hackrf
|
||||
|
||||
git clone https://github.com/pothosware/SoapyHackRF.git
|
||||
cmakebuild SoapyHackRF 3c514cecd3dd2fdf4794aebc96c482940c11d7ff
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild SoapyHackRF 7d530872f96c1cbe0ed62617c32c48ce7e103e1d
|
||||
|
||||
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
@ -17,7 +17,8 @@ fi
|
||||
|
||||
git clone https://github.com/myriadrf/LimeSuite.git
|
||||
cd LimeSuite
|
||||
git checkout 0854a51ec06b30b01f19a562149c39461e92f24d
|
||||
# latest from master as of 2020-09-04
|
||||
git checkout 9526621f8b4c9e2a7f638b5ef50c45560dcad22a
|
||||
mkdir builddir
|
||||
cd builddir
|
||||
cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic" ${SIMD_FLAGS}
|
||||
|
@ -12,7 +12,8 @@ apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/Microtelecom/libperseus-sdr.git
|
||||
cd libperseus-sdr
|
||||
git checkout 72ac67c5b7936a1991be0ec97c03a59c1a8ac8f3
|
||||
# latest from master as of 2020-09-04
|
||||
git checkout c2c95daeaa08bf0daed0e8ada970ab17cc264e1b
|
||||
./bootstrap.sh
|
||||
./configure
|
||||
make
|
||||
|
@ -19,19 +19,20 @@ function cmakebuild() {
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb-1.0-0 libxml2"
|
||||
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ libxml2-dev flex bison"
|
||||
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ libxml2-dev flex bison pkg-config"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/analogdevicesinc/libiio.git
|
||||
cmakebuild libiio 5f5af2e417129ad8f4e05fc5c1b730f0694dca12 -DCMAKE_INSTALL_PREFIX=/usr/local
|
||||
cmakebuild libiio v0.21 -DCMAKE_INSTALL_PREFIX=/usr/local
|
||||
|
||||
git clone https://github.com/analogdevicesinc/libad9361-iio.git
|
||||
cmakebuild libad9361-iio 8ac95f3325c18c2e34cd9cfd49c7b63d69a0a9d2
|
||||
cmakebuild libad9361-iio v0.2
|
||||
|
||||
git clone https://github.com/pothosware/SoapyPlutoSDR.git
|
||||
cmakebuild SoapyPlutoSDR c88b7f5bac1e5785f212f9a7c6ce8fef64eb719e
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild SoapyPlutoSDR 93717b32ef052e0dfa717aa2c1a4eb27af16111f
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
37
docker/scripts/install-dependencies-radioberry.sh
Executable file
37
docker/scripts/install-dependencies-radioberry.sh
Executable file
@ -0,0 +1,37 @@
|
||||
#!/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 libfftw3-3 udev"
|
||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/pa3gsb/Radioberry-2.x
|
||||
cd Radioberry-2.x/SBC/rpi-4
|
||||
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild SoapyRadioberrySDR 8d17de6b4dc076e628900a82f05c7cf0b16cbe24
|
||||
cd ../../..
|
||||
rm -rf Radioberry-2.x
|
||||
|
||||
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
32
docker/scripts/install-dependencies-redpitaya.sh
Executable file
32
docker/scripts/install-dependencies-redpitaya.sh
Executable file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
set -euo 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=""
|
||||
BUILD_PACKAGES="git cmake make gcc g++"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/pothosware/SoapyRedPitaya.git
|
||||
cmakebuild SoapyRedPitaya soapy-redpitaya-0.1.1
|
||||
|
||||
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
@ -25,10 +25,11 @@ apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/osmocom/rtl-sdr.git
|
||||
cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild rtl-sdr ed0317e6a58c098874ac58b769cf2e609c18d9a5
|
||||
|
||||
git clone https://github.com/pothosware/SoapyRTLSDR.git
|
||||
cmakebuild SoapyRTLSDR 8ba18f17d64005e43ff2a4e46611f8c710b05007
|
||||
cmakebuild SoapyRTLSDR soapy-rtl-sdr-0.3.1
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
@ -25,7 +25,8 @@ apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/osmocom/rtl-sdr.git
|
||||
cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild rtl-sdr ed0317e6a58c098874ac58b769cf2e609c18d9a5
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
@ -49,7 +49,8 @@ rm -rf sdrplay
|
||||
rm $BINARY
|
||||
|
||||
git clone https://github.com/SDRplay/SoapySDRPlay.git
|
||||
cmakebuild SoapySDRPlay 1c2728a04db5edf8154d02f5cca87e655152d7c1
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild SoapySDRPlay 105f8a6b3d449982d7ef860790c201aa066b8fa9
|
||||
|
||||
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
@ -25,7 +25,7 @@ apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/pothosware/SoapyRemote.git
|
||||
cmakebuild SoapyRemote 6d9bd820da470cfe7b27b2e6946af93cfece448f
|
||||
cmakebuild SoapyRemote soapy-remote-0.5.2
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
@ -25,7 +25,8 @@ apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/pothosware/SoapySDR
|
||||
cmakebuild SoapySDR f722f9ce5b629c3c44401a9bf628b3f8e67a9695
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild SoapySDR 580b94f3dad46899f34ec0a060dbb4534e844e57
|
||||
|
||||
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
60
docker/scripts/install-dependencies-uhd.sh
Executable file
60
docker/scripts/install-dependencies-uhd.sh
Executable file
@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
set -euo 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 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"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/EttusResearch/uhd.git
|
||||
# 3.15.0.0 Release
|
||||
mkdir -p uhd/host/build
|
||||
cd uhd/host/build
|
||||
git checkout v3.15.0.0
|
||||
# see https://github.com/EttusResearch/uhd/issues/350
|
||||
case `uname -m` in
|
||||
arm*)
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_UTILS=OFF -DENABLE_PYTHON_API=OFF -DENABLE_EXAMPLES=OFF -DENABLE_TESTS=OFF -DENABLE_OCTOCLOCK=OFF -DENABLE_MAN_PAGES=OFF -DSTRIP_BINARIES=ON \
|
||||
-DCMAKE_CXX_FLAGS:STRING="-march=armv7-a -mfloat-abi=hard -mfpu=neon -mtune=cortex-a8 -Wno-psabi" \
|
||||
-DCMAKE_C_FLAGS:STRING="-march=armv7-a -mfloat-abi=hard -mfpu=neon -mtune=cortex-a8 -Wno-psabi" \
|
||||
-DCMAKE_ASM_FLAGS:STRING="-march=armv7-a -mfloat-abi=hard -mfpu=neon -mtune=cortex-a8 -g" ..
|
||||
;;
|
||||
aarch64*)
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_UTILS=OFF -DENABLE_PYTHON_API=OFF -DENABLE_EXAMPLES=OFF -DENABLE_TESTS=OFF -DENABLE_OCTOCLOCK=OFF -DENABLE_MAN_PAGES=OFF -DSTRIP_BINARIES=ON \
|
||||
-DCMAKE_CXX_FLAGS:STRING="-march=armv8-a -mtune=cortex-a72 -Wno-psabi" \
|
||||
-DCMAKE_C_FLAGS:STRING="-march=armv8-a -mtune=cortex-a72 -Wno-psabi" \
|
||||
-DCMAKE_ASM_FLAGS:STRING="-march=armv8-a -mtune=cortex-a72 -g" ..
|
||||
;;
|
||||
x86_64)
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_UTILS=OFF -DENABLE_PYTHON_API=OFF -DENABLE_EXAMPLES=OFF -DENABLE_TESTS=OFF -DENABLE_OCTOCLOCK=OFF -DENABLE_MAN_PAGES=OFF -DSTRIP_BINARIES=ON ..
|
||||
;;
|
||||
esac
|
||||
make
|
||||
make install
|
||||
cd ../../..
|
||||
rm -rf uhd
|
||||
|
||||
git clone https://github.com/pothosware/SoapyUHD.git
|
||||
cmakebuild SoapyUHD soapy-uhd-0.4.1
|
||||
|
||||
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
@ -18,9 +18,8 @@ function cmakebuild() {
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates"
|
||||
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev patch xsltproc"
|
||||
|
||||
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-default libfaad-dev libopus-dev"
|
||||
apt-get update
|
||||
apt-get -y install auto-apt-proxy
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
@ -37,53 +36,31 @@ case `uname -m` in
|
||||
;;
|
||||
esac
|
||||
|
||||
pushd /tmp
|
||||
wget https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${PLATFORM}.tar.gz
|
||||
tar xzf s6-overlay-${PLATFORM}.tar.gz -C /
|
||||
rm s6-overlay-${PLATFORM}.tar.gz
|
||||
popd
|
||||
|
||||
git clone https://github.com/jketterl/js8py.git
|
||||
pushd js8py
|
||||
git checkout 888e62be375316882ad2b2ac8e396c3bf857b6fc
|
||||
python3 setup.py install
|
||||
popd
|
||||
rm -rf js8py
|
||||
|
||||
git clone https://git.code.sf.net/p/itpp/git itpp
|
||||
cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d
|
||||
|
||||
git clone https://github.com/jketterl/csdr.git
|
||||
cd csdr
|
||||
git checkout c4d8a8a5590898e8c9e94b88b96a2fdc7cd0493a
|
||||
autoreconf -i
|
||||
./configure
|
||||
make
|
||||
make install
|
||||
cd ..
|
||||
rm -rf csdr
|
||||
|
||||
git clone https://github.com/szechyjs/mbelib.git
|
||||
cmakebuild mbelib 9a04ed5c78176a9965f3d43f7aa1b1f5330e771f
|
||||
|
||||
git clone https://github.com/jketterl/digiham.git
|
||||
cmakebuild digiham 95206501be89b38d0267bf6c29a6898e7c65656f
|
||||
|
||||
git clone https://github.com/f4exb/dsd.git
|
||||
cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7
|
||||
|
||||
JS8CALL_VERSION=2.1.1
|
||||
JS8CALL_DIR=js8call-${JS8CALL_VERSION}
|
||||
JS8CALL_TGZ=${JS8CALL_DIR}.tgz
|
||||
JS8CALL_VERSION=2.2.0
|
||||
JS8CALL_DIR=js8call
|
||||
JS8CALL_TGZ=js8call-${JS8CALL_VERSION}.tgz
|
||||
wget http://files.js8call.com/${JS8CALL_VERSION}/${JS8CALL_TGZ}
|
||||
tar xfz ${JS8CALL_TGZ}
|
||||
# patch allows us to build against the packaged hamlib
|
||||
patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch
|
||||
rm /js8call-hamlib.patch
|
||||
CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_LEGACY_HAMLIB" cmakebuild ${JS8CALL_DIR}
|
||||
CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_HAMLIB_THREE" cmakebuild ${JS8CALL_DIR}
|
||||
rm ${JS8CALL_TGZ}
|
||||
|
||||
WSJT_DIR=wsjtx-2.1.2
|
||||
WSJT_DIR=wsjtx-2.2.2
|
||||
WSJT_TGZ=${WSJT_DIR}.tgz
|
||||
wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
|
||||
tar xfz ${WSJT_TGZ}
|
||||
@ -94,10 +71,40 @@ rm ${WSJT_TGZ}
|
||||
|
||||
git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git
|
||||
cd direwolf
|
||||
make
|
||||
# hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need.
|
||||
# by setting enable_hamlib we prevent direwolf from linking to it, and it can be stripped at the end of the script.
|
||||
make enable_hamlib=
|
||||
make install
|
||||
cd ..
|
||||
rm -rf direwolf
|
||||
# strip lots of generic documentation that will never be read inside a docker container
|
||||
rm /usr/local/share/doc/direwolf/*.pdf
|
||||
# examples are pointless, too
|
||||
rm -rf /usr/local/share/doc/direwolf/examples/
|
||||
|
||||
git clone https://github.com/drowe67/codec2.git
|
||||
cd codec2
|
||||
# latest commit from master as of 2020-10-04
|
||||
git checkout 55d7bb8d1bddf881bdbfcb971a718b83e6344598
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install
|
||||
install -m 0755 src/freedv_rx /usr/local/bin
|
||||
cd ../..
|
||||
rm -rf codec2
|
||||
|
||||
wget https://downloads.sourceforge.net/project/drm/dream/2.1.1/dream-2.1.1-svn808.tar.gz
|
||||
tar xvfz dream-2.1.1-svn808.tar.gz
|
||||
pushd dream
|
||||
patch -Np0 < /dream.patch
|
||||
qmake CONFIG+=console
|
||||
make
|
||||
make install
|
||||
popd
|
||||
rm -rf dream
|
||||
rm dream-2.1.1-svn808.tar.gz
|
||||
|
||||
git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols
|
||||
pushd /opt/aprs-symbols
|
||||
|
48
docker/scripts/install-owrx-tools.sh
Executable file
48
docker/scripts/install-owrx-tools.sh
Executable file
@ -0,0 +1,48 @@
|
||||
#!/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 ${CMAKE_ARGS:-} ..
|
||||
make
|
||||
make install
|
||||
cd ../..
|
||||
rm -rf $1
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libfftw3-bin"
|
||||
BUILD_PACKAGES="git autoconf automake libtool libfftw3-dev pkg-config cmake make gcc g++"
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/jketterl/js8py.git
|
||||
pushd js8py
|
||||
git checkout 0.1.0
|
||||
python3 setup.py install
|
||||
popd
|
||||
rm -rf js8py
|
||||
|
||||
git clone https://github.com/jketterl/csdr.git
|
||||
cd csdr
|
||||
git checkout 0.17.0
|
||||
autoreconf -i
|
||||
./configure
|
||||
make
|
||||
make install
|
||||
cd ..
|
||||
rm -rf csdr
|
||||
|
||||
git clone https://github.com/jketterl/digiham.git
|
||||
cmakebuild digiham 0.3.0
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
@ -6,3 +6,20 @@ html, body
|
||||
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;
|
||||
}
|
||||
}
|
@ -61,7 +61,6 @@
|
||||
#webrx-rx-avatar
|
||||
{
|
||||
background-color: rgba(154, 154, 154, .5);
|
||||
border-radius: 7px;
|
||||
float: left;
|
||||
margin: 7px;
|
||||
|
||||
@ -193,3 +192,44 @@
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.sprite-panel-status {
|
||||
background-position: 0 0;
|
||||
width: 44px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.sprite-panel-log {
|
||||
background-position: -44px 0;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.sprite-panel-receiver {
|
||||
background-position: -82px 0;
|
||||
width: 40px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.sprite-panel-map {
|
||||
background-position: -122px 0;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.sprite-panel-settings {
|
||||
background-position: -160px 0;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.sprite-rx-details-arrow-down {
|
||||
background-position: 0 -65px;
|
||||
width: 43px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.sprite-rx-details-arrow-up {
|
||||
background-position: -43px -65px;
|
||||
width: 43px;
|
||||
height: 12px;
|
||||
}
|
@ -274,6 +274,18 @@ input[type=range]:disabled {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@supports(background-image: -webkit-image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x)) {
|
||||
#webrx-canvas-background {
|
||||
background-image: -webkit-image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x);
|
||||
}
|
||||
}
|
||||
|
||||
@supports(background-image: image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x)) {
|
||||
#webrx-canvas-background {
|
||||
background-image: image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x);
|
||||
}
|
||||
}
|
||||
|
||||
#webrx-canvas-container
|
||||
{
|
||||
position: relative;
|
||||
@ -310,7 +322,9 @@ input[type=range]:disabled {
|
||||
|
||||
@font-face {
|
||||
font-family: 'roboto-mono';
|
||||
src: url('../fonts/RobotoMono-Regular.ttf');
|
||||
src: url('../fonts/RobotoMono-Regular.woff2') format('woff2'),
|
||||
url('../fonts/RobotoMono-Regular.woff') format('woff'),
|
||||
url('../fonts/RobotoMono-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@ -421,7 +435,7 @@ input[type=range]:disabled {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.openwebrx-button:hover, .openwebrx-demodulator-button.highlighted
|
||||
.openwebrx-button:hover, .openwebrx-demodulator-button.highlighted, .openwebrx-button.highlighted
|
||||
{
|
||||
/*background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #3F3F3F), color-stop(1, #777777) );
|
||||
background:-moz-linear-gradient( center top, #373737 5%, #4F4F4F 100% );*/
|
||||
@ -445,7 +459,6 @@ input[type=range]:disabled {
|
||||
|
||||
.openwebrx-demodulator-button
|
||||
{
|
||||
width: 38px;
|
||||
height: 19px;
|
||||
font-size: 12pt;
|
||||
text-align: center;
|
||||
@ -620,6 +633,31 @@ img.openwebrx-mirror-img
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.openwebrx-modes-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: -5px -5px 0 0;
|
||||
}
|
||||
|
||||
.openwebrx-modes-grid .openwebrx-demodulator-button {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
flex: 1 0 38px;
|
||||
margin: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
@supports(gap: 5px) {
|
||||
.openwebrx-modes-grid {
|
||||
margin: 0;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.openwebrx-modes-grid .openwebrx-demodulator-button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#openwebrx-smeter-outer
|
||||
{
|
||||
border-color: #888;
|
||||
@ -1126,3 +1164,75 @@ img.openwebrx-mirror-img
|
||||
margin: -10px;
|
||||
}
|
||||
|
||||
.sprite-zoom-in {
|
||||
background-position: 0 -38px;
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.sprite-zoom-out {
|
||||
background-position: -27px -38px;
|
||||
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;
|
||||
}
|
||||
|
||||
.sprite-trashcan {
|
||||
background-position: -145px -38px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.sprite-speaker {
|
||||
width: 14px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
#openwebrx-mute-on .sprite-speaker {
|
||||
background-position: -117px -38px;
|
||||
}
|
||||
|
||||
#openwebrx-mute-off .sprite-speaker {
|
||||
background-position: -103px -38px;
|
||||
}
|
||||
|
||||
.sprite-squelch {
|
||||
background-position: -131px -38px;
|
||||
width: 14px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.sprite-waterfall-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.
BIN
htdocs/fonts/RobotoMono-Regular.woff
Normal file
BIN
htdocs/fonts/RobotoMono-Regular.woff
Normal file
Binary file not shown.
BIN
htdocs/fonts/RobotoMono-Regular.woff2
Normal file
BIN
htdocs/fonts/RobotoMono-Regular.woff2
Normal file
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 9.9 KiB |
BIN
htdocs/gfx/openwebrx-background-cool-blue.webp
Normal file
BIN
htdocs/gfx/openwebrx-background-cool-blue.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
htdocs/gfx/openwebrx-sprites-2x.png
Normal file
BIN
htdocs/gfx/openwebrx-sprites-2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
htdocs/gfx/openwebrx-sprites.png
Normal file
BIN
htdocs/gfx/openwebrx-sprites.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
@ -9,14 +9,14 @@
|
||||
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
|
||||
</div>
|
||||
<div id="openwebrx-rx-details-arrow">
|
||||
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger" style="display: none;"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a>
|
||||
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a>
|
||||
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger" style="display: none;"><span class="sprite sprite-rx-details-arrow-up"></span></a>
|
||||
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><span class="sprite sprite-rx-details-arrow-down"></span></a>
|
||||
</div>
|
||||
<section id="openwebrx-main-buttons">
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" alt="Status"/><br/>Status</div>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" alt="Log"/><br/>Log</div>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" alt="Receiver"/><br/>Receiver</div>
|
||||
<a class="button" href="map" target="openwebrx-map"><img src="static/gfx/openwebrx-panel-map.png" alt="Map"/><br/>Map</a>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-status"><span class="sprite sprite-panel-status"></span><br/>Status</div>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-log"><span class="sprite sprite-panel-log"></span><br/>Log</div>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><span class="sprite sprite-panel-receiver"></span><br/>Receiver</div>
|
||||
<a class="button" href="map" target="openwebrx-map"><span class="sprite sprite-panel-map"></span><br/>Map</a>
|
||||
${settingslink}
|
||||
</section>
|
||||
</div>
|
||||
|
@ -149,7 +149,7 @@
|
||||
<div class="webrx-mouse-freq"></div>
|
||||
</div>
|
||||
<div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark...">
|
||||
<img src="static/gfx/openwebrx-bookmark.png">
|
||||
<span class="sprite sprite-bookmark"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
@ -158,22 +158,22 @@
|
||||
</div>
|
||||
<div class="openwebrx-modes openwebrx-panel-line"></div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
|
||||
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><span class="sprite sprite-speaker openwebrx-sliderbtn-img"></span></div>
|
||||
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
|
||||
<div title="Auto-adjust waterfall colors" id="openwebrx-waterfall-colors-auto" class="openwebrx-button" onclick="waterfall_measure_minmax_now=true;"><img src="static/gfx/openwebrx-waterfall-auto.png" class="openwebrx-sliderbtn-img"></div>
|
||||
<div title="Auto-adjust waterfall colors (right-click for continuous)" id="openwebrx-waterfall-colors-auto" class="openwebrx-button"><span class="sprite sprite-waterfall-auto openwebrx-sliderbtn-img"></span></div>
|
||||
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div title="Auto-set squelch level" class="openwebrx-squelch-default openwebrx-button"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
|
||||
<div title="Auto-set squelch level" class="openwebrx-squelch-default openwebrx-button"><span class="sprite sprite-squelch openwebrx-sliderbtn-img"></span></div>
|
||||
<input title="Squelch" class="openwebrx-squelch-slider openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1">
|
||||
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
|
||||
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><span class="sprite sprite-waterfall-default openwebrx-sliderbtn-img"></span></div>
|
||||
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInOneStep();" title="Zoom in one step"> <img src="static/gfx/openwebrx-zoom-in.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutOneStep();" title="Zoom out one step"> <img src="static/gfx/openwebrx-zoom-out.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInTotal();" title="Zoom in totally"><img src="static/gfx/openwebrx-zoom-in-total.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutTotal();" title="Zoom out totally"><img src="static/gfx/openwebrx-zoom-out-total.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInOneStep();" title="Zoom in one step"><span class="sprite sprite-zoom-in"></span></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutOneStep();" title="Zoom out one step"><span class="sprite sprite-zoom-out"></span></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInTotal();" title="Zoom in totally"><span class="sprite sprite-zoom-in-total"></span></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutTotal();" title="Zoom out totally"><span class="sprite sprite-zoom-out-total"></span></div>
|
||||
<div id="openwebrx-smeter-db">0 dB</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
|
@ -10,125 +10,153 @@ function AudioEngine(maxBufferLength, audioReporter) {
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
this.audioContext = new ctx();
|
||||
this.allowed = this.audioContext.state === 'running';
|
||||
|
||||
this.onStartCallbacks = [];
|
||||
|
||||
this.started = false;
|
||||
this.audioContext = new ctx();
|
||||
var me = this;
|
||||
this.audioContext.onstatechange = function() {
|
||||
if (me.audioContext.state !== 'running') return;
|
||||
me._start();
|
||||
}
|
||||
|
||||
this.audioCodec = new ImaAdpcmCodec();
|
||||
this.compression = 'none';
|
||||
|
||||
this.setupResampling();
|
||||
this.resampler = new Interpolator(this.resamplingFactor);
|
||||
this.hdResampler = new Interpolator(this.hdResamplingFactor);
|
||||
|
||||
this.maxBufferSize = maxBufferLength * this.getSampleRate();
|
||||
}
|
||||
|
||||
AudioEngine.prototype.start = function(callback) {
|
||||
AudioEngine.prototype.resume = function(){
|
||||
this.audioContext.resume();
|
||||
}
|
||||
|
||||
AudioEngine.prototype._start = function() {
|
||||
var me = this;
|
||||
if (me.resamplingFactor === 0) return; //if failed to find a valid resampling factor...
|
||||
|
||||
// if failed to find a valid resampling factor...
|
||||
if (me.resamplingFactor === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// been started before?
|
||||
if (me.started) {
|
||||
if (callback) callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
me.audioContext.resume().then(function(){
|
||||
me.allowed = me.audioContext.state === 'running';
|
||||
if (!me.allowed) {
|
||||
if (callback) callback(false);
|
||||
return;
|
||||
}
|
||||
me.started = true;
|
||||
// are we allowed to play audio?
|
||||
if (!me.isAllowed()) {
|
||||
return;
|
||||
}
|
||||
me.started = true;
|
||||
|
||||
me.gainNode = me.audioContext.createGain();
|
||||
me.gainNode.connect(me.audioContext.destination);
|
||||
var runCallbacks = function(workletType) {
|
||||
var callbacks = me.onStartCallbacks;
|
||||
me.onStartCallbacks = false;
|
||||
callbacks.forEach(function(c) { c(workletType); });
|
||||
};
|
||||
|
||||
if (useAudioWorklets && me.audioContext.audioWorklet) {
|
||||
me.audioContext.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){
|
||||
me.audioNode = new AudioWorkletNode(me.audioContext, 'openwebrx-audio-processor', {
|
||||
numberOfInputs: 0,
|
||||
numberOfOutputs: 1,
|
||||
outputChannelCount: [1],
|
||||
processorOptions: {
|
||||
maxBufferSize: me.maxBufferSize
|
||||
}
|
||||
});
|
||||
me.audioNode.connect(me.gainNode);
|
||||
me.audioNode.port.addEventListener('message', function(m){
|
||||
var json = JSON.parse(m.data);
|
||||
if (typeof(json.buffersize) !== 'undefined') {
|
||||
me.audioReporter({
|
||||
buffersize: json.buffersize
|
||||
});
|
||||
}
|
||||
if (typeof(json.samplesProcessed) !== 'undefined') {
|
||||
me.audioSamples.add(json.samplesProcessed);
|
||||
}
|
||||
});
|
||||
me.audioNode.port.start();
|
||||
if (callback) callback(true, 'AudioWorklet');
|
||||
me.gainNode = me.audioContext.createGain();
|
||||
me.gainNode.connect(me.audioContext.destination);
|
||||
|
||||
if (useAudioWorklets && me.audioContext.audioWorklet) {
|
||||
me.audioContext.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){
|
||||
me.audioNode = new AudioWorkletNode(me.audioContext, 'openwebrx-audio-processor', {
|
||||
numberOfInputs: 0,
|
||||
numberOfOutputs: 1,
|
||||
outputChannelCount: [1],
|
||||
processorOptions: {
|
||||
maxBufferSize: me.maxBufferSize
|
||||
}
|
||||
});
|
||||
} else {
|
||||
me.audioBuffers = [];
|
||||
|
||||
if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does
|
||||
AudioBuffer.prototype.copyToChannel = function (input, channel) //input is Float32Array
|
||||
{
|
||||
var cd = this.getChannelData(channel);
|
||||
for (var i = 0; i < input.length; i++) cd[i] = input[i];
|
||||
}
|
||||
}
|
||||
|
||||
var bufferSize;
|
||||
if (me.audioContext.sampleRate < 44100 * 2)
|
||||
bufferSize = 4096;
|
||||
else if (me.audioContext.sampleRate >= 44100 * 2 && me.audioContext.sampleRate < 44100 * 4)
|
||||
bufferSize = 4096 * 2;
|
||||
else if (me.audioContext.sampleRate > 44100 * 4)
|
||||
bufferSize = 4096 * 4;
|
||||
|
||||
|
||||
function audio_onprocess(e) {
|
||||
var total = 0;
|
||||
var out = new Float32Array(bufferSize);
|
||||
while (me.audioBuffers.length) {
|
||||
var b = me.audioBuffers.shift();
|
||||
// not enough space to fit all data, so splice and put back in the queue
|
||||
if (total + b.length > bufferSize) {
|
||||
var spaceLeft = bufferSize - total;
|
||||
var tokeep = b.subarray(0, spaceLeft);
|
||||
out.set(tokeep, total);
|
||||
var tobuffer = b.subarray(spaceLeft, b.length);
|
||||
me.audioBuffers.unshift(tobuffer);
|
||||
total += spaceLeft;
|
||||
break;
|
||||
} else {
|
||||
out.set(b, total);
|
||||
total += b.length;
|
||||
}
|
||||
}
|
||||
|
||||
e.outputBuffer.copyToChannel(out, 0);
|
||||
me.audioSamples.add(total);
|
||||
|
||||
}
|
||||
|
||||
//on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor
|
||||
var method = 'createScriptProcessor';
|
||||
if (me.audioContext.createJavaScriptNode) {
|
||||
method = 'createJavaScriptNode';
|
||||
}
|
||||
me.audioNode = me.audioContext[method](bufferSize, 0, 1);
|
||||
me.audioNode.onaudioprocess = audio_onprocess;
|
||||
me.audioNode.connect(me.gainNode);
|
||||
if (callback) callback(true, 'ScriptProcessorNode');
|
||||
me.audioNode.port.addEventListener('message', function(m){
|
||||
var json = JSON.parse(m.data);
|
||||
if (typeof(json.buffersize) !== 'undefined') {
|
||||
me.audioReporter({
|
||||
buffersize: json.buffersize
|
||||
});
|
||||
}
|
||||
if (typeof(json.samplesProcessed) !== 'undefined') {
|
||||
me.audioSamples.add(json.samplesProcessed);
|
||||
}
|
||||
});
|
||||
me.audioNode.port.start();
|
||||
runCallbacks('AudioWorklet');
|
||||
});
|
||||
} else {
|
||||
me.audioBuffers = [];
|
||||
|
||||
if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does
|
||||
AudioBuffer.prototype.copyToChannel = function (input, channel) //input is Float32Array
|
||||
{
|
||||
var cd = this.getChannelData(channel);
|
||||
for (var i = 0; i < input.length; i++) cd[i] = input[i];
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(me.reportStats.bind(me), 1000);
|
||||
});
|
||||
var bufferSize;
|
||||
if (me.audioContext.sampleRate < 44100 * 2)
|
||||
bufferSize = 4096;
|
||||
else if (me.audioContext.sampleRate >= 44100 * 2 && me.audioContext.sampleRate < 44100 * 4)
|
||||
bufferSize = 4096 * 2;
|
||||
else if (me.audioContext.sampleRate > 44100 * 4)
|
||||
bufferSize = 4096 * 4;
|
||||
|
||||
|
||||
function audio_onprocess(e) {
|
||||
var total = 0;
|
||||
var out = new Float32Array(bufferSize);
|
||||
while (me.audioBuffers.length) {
|
||||
var b = me.audioBuffers.shift();
|
||||
// not enough space to fit all data, so splice and put back in the queue
|
||||
if (total + b.length > bufferSize) {
|
||||
var spaceLeft = bufferSize - total;
|
||||
var tokeep = b.subarray(0, spaceLeft);
|
||||
out.set(tokeep, total);
|
||||
var tobuffer = b.subarray(spaceLeft, b.length);
|
||||
me.audioBuffers.unshift(tobuffer);
|
||||
total += spaceLeft;
|
||||
break;
|
||||
} else {
|
||||
out.set(b, total);
|
||||
total += b.length;
|
||||
}
|
||||
}
|
||||
|
||||
e.outputBuffer.copyToChannel(out, 0);
|
||||
me.audioSamples.add(total);
|
||||
|
||||
}
|
||||
|
||||
//on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor
|
||||
var method = 'createScriptProcessor';
|
||||
if (me.audioContext.createJavaScriptNode) {
|
||||
method = 'createJavaScriptNode';
|
||||
}
|
||||
me.audioNode = me.audioContext[method](bufferSize, 0, 1);
|
||||
me.audioNode.onaudioprocess = audio_onprocess;
|
||||
me.audioNode.connect(me.gainNode);
|
||||
runCallbacks('ScriptProcessorNode')
|
||||
}
|
||||
|
||||
setInterval(me.reportStats.bind(me), 1000);
|
||||
};
|
||||
|
||||
AudioEngine.prototype.onStart = function(callback) {
|
||||
if (this.onStartCallbacks) {
|
||||
this.onStartCallbacks.push(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
AudioEngine.prototype.isAllowed = function() {
|
||||
return this.allowed;
|
||||
return this.audioContext.state === 'running';
|
||||
};
|
||||
|
||||
AudioEngine.prototype.reportStats = function() {
|
||||
@ -165,35 +193,57 @@ AudioEngine.prototype.resetStats = function() {
|
||||
};
|
||||
|
||||
AudioEngine.prototype.setupResampling = function() { //both at the server and the client
|
||||
var output_range_max = 12000;
|
||||
var output_range_min = 8000;
|
||||
var audio_params = this.findRate(8000, 12000);
|
||||
if (!audio_params) {
|
||||
this.resamplingFactor = 0;
|
||||
this.outputRate = 0;
|
||||
divlog('Your audio card sampling rate (' + targetRate + ') is not supported.<br />Please change your operating system default settings in order to fix this.', 1);
|
||||
} else {
|
||||
this.resamplingFactor = audio_params.resamplingFactor;
|
||||
this.outputRate = audio_params.outputRate;
|
||||
}
|
||||
|
||||
var hd_audio_params = this.findRate(36000, 48000);
|
||||
if (!hd_audio_params) {
|
||||
this.hdResamplingFactor = 0;
|
||||
this.hdOutputRate = 0;
|
||||
divlog('Your audio card sampling rate (' + targetRate + ') is not supported for HD audio<br />Please change your operating system default settings in order to fix this.', 1);
|
||||
} else {
|
||||
this.hdResamplingFactor = hd_audio_params.resamplingFactor;
|
||||
this.hdOutputRate = hd_audio_params.outputRate;
|
||||
}
|
||||
};
|
||||
|
||||
AudioEngine.prototype.findRate = function(low, high) {
|
||||
var targetRate = this.audioContext.sampleRate;
|
||||
var i = 1;
|
||||
while (true) {
|
||||
var audio_server_output_rate = Math.floor(targetRate / i);
|
||||
if (audio_server_output_rate < output_range_min) {
|
||||
this.resamplingFactor = 0;
|
||||
this.outputRate = 0;
|
||||
divlog('Your audio card sampling rate (' + targetRate + ') is not supported.<br />Please change your operating system default settings in order to fix this.', 1);
|
||||
break;
|
||||
} else if (audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) {
|
||||
this.resamplingFactor = i;
|
||||
this.outputRate = audio_server_output_rate;
|
||||
break; //okay, we're done
|
||||
if (audio_server_output_rate < low) {
|
||||
return;
|
||||
} else if (audio_server_output_rate >= low && audio_server_output_rate <= high) {
|
||||
return {
|
||||
resamplingFactor: i,
|
||||
outputRate: audio_server_output_rate
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
AudioEngine.prototype.getOutputRate = function() {
|
||||
return this.outputRate;
|
||||
};
|
||||
|
||||
AudioEngine.prototype.getHdOutputRate = function() {
|
||||
return this.hdOutputRate;
|
||||
}
|
||||
|
||||
AudioEngine.prototype.getSampleRate = function() {
|
||||
return this.audioContext.sampleRate;
|
||||
};
|
||||
|
||||
AudioEngine.prototype.pushAudio = function(data) {
|
||||
AudioEngine.prototype.processAudio = function(data, resampler) {
|
||||
if (!this.audioNode) return;
|
||||
this.audioBytes.add(data.byteLength);
|
||||
var buffer;
|
||||
@ -203,7 +253,7 @@ AudioEngine.prototype.pushAudio = function(data) {
|
||||
} else {
|
||||
buffer = new Int16Array(data);
|
||||
}
|
||||
buffer = this.resampler.process(buffer);
|
||||
buffer = resampler.process(buffer);
|
||||
if (this.audioNode.port) {
|
||||
// AudioWorklets supported
|
||||
this.audioNode.port.postMessage(buffer);
|
||||
@ -213,8 +263,16 @@ AudioEngine.prototype.pushAudio = function(data) {
|
||||
this.audioBuffers.push(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AudioEngine.prototype.pushAudio = function(data) {
|
||||
this.processAudio(data, this.resampler);
|
||||
};
|
||||
|
||||
AudioEngine.prototype.pushHdAudio = function(data) {
|
||||
this.processAudio(data, this.hdResampler);
|
||||
}
|
||||
|
||||
AudioEngine.prototype.setCompression = function(compression) {
|
||||
this.compression = compression;
|
||||
};
|
||||
|
@ -33,7 +33,10 @@ class OwrxAudioProcessor extends AudioWorkletProcessor {
|
||||
this.port.start();
|
||||
}
|
||||
process(inputs, outputs) {
|
||||
if (this.remaining() < 128) return true;
|
||||
if (this.remaining() < 128) {
|
||||
outputs[0].forEach(output => output.fill(0));
|
||||
return true;
|
||||
}
|
||||
outputs[0].forEach((output) => {
|
||||
output.set(this.audioBuffer.subarray(this.outPos, this.outPos + 128));
|
||||
});
|
||||
|
@ -87,8 +87,8 @@ BookmarkBar.prototype.render = function(){
|
||||
var $bookmark = $(
|
||||
'<div class="bookmark" data-source="' + b.source + '"' + (b.editable?' editable="editable"':'') + '>' +
|
||||
'<div class="bookmark-actions">' +
|
||||
'<div class="openwebrx-button action" data-action="edit"><img src="static/gfx/openwebrx-edit.png"></div>' +
|
||||
'<div class="openwebrx-button action" data-action="delete"><img src="static/gfx/openwebrx-trashcan.png"></div>' +
|
||||
'<div class="openwebrx-button action" data-action="edit"><span class="sprite sprite-edit"></span></div>' +
|
||||
'<div class="openwebrx-button action" data-action="delete"><span class="sprite sprite-trashcan"><span></div>' +
|
||||
'</div>' +
|
||||
'<div class="bookmark-content">' + b.name + '</div>' +
|
||||
'</div>'
|
||||
|
@ -7,6 +7,10 @@ Filter.prototype.getLimits = function() {
|
||||
var max_bw;
|
||||
if (this.demodulator.get_secondary_demod() === 'pocsag') {
|
||||
max_bw = 12500;
|
||||
} else if (this.demodulator.get_modulation() === 'wfm') {
|
||||
max_bw = 80000;
|
||||
} else if (this.demodulator.get_modulation() === 'drm') {
|
||||
max_bw = 100000;
|
||||
} else {
|
||||
max_bw = (audioEngine.getOutputRate() / 2) - 1;
|
||||
}
|
||||
|
@ -57,19 +57,9 @@ DemodulatorPanel.prototype.render = function() {
|
||||
);
|
||||
});
|
||||
|
||||
var index = 0;
|
||||
var arrayLength = buttons.length;
|
||||
var chunks = [];
|
||||
|
||||
for (index = 0; index < arrayLength; index += 5) {
|
||||
chunks.push(buttons.slice(index, index + 5));
|
||||
}
|
||||
|
||||
html.push.apply(html, chunks.map(function(chunk){
|
||||
$line = $('<div class="openwebrx-panel-line openwebrx-panel-flex-line"></div>');
|
||||
$line.append.apply($line, chunk);
|
||||
return $line
|
||||
}));
|
||||
var $modegrid = $('<div class="openwebrx-modes-grid"></div>');
|
||||
$modegrid.append.apply($modegrid, buttons);
|
||||
html.push($modegrid);
|
||||
|
||||
html.push($(
|
||||
'<div class="openwebrx-panel-line openwebrx-panel-flex-line">' +
|
||||
@ -129,7 +119,9 @@ DemodulatorPanel.prototype.setMode = function(requestedModulation) {
|
||||
this.demodulator.on("frequencychange", updateFrequency);
|
||||
updateFrequency(this.demodulator.get_offset_frequency());
|
||||
var updateSquelch = function(squelch) {
|
||||
self.el.find('.openwebrx-squelch-slider').val(squelch);
|
||||
self.el.find('.openwebrx-squelch-slider')
|
||||
.val(squelch)
|
||||
.attr('title', 'Squelch (' + squelch + ' dB)');
|
||||
self.updateHash();
|
||||
};
|
||||
this.demodulator.on('squelchchange', updateSquelch);
|
||||
@ -189,7 +181,7 @@ DemodulatorPanel.prototype.collectParams = function() {
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.startDemodulator = function() {
|
||||
if (!Modes.initComplete()) return;
|
||||
if (!Modes.initComplete() || !this.center_freq) return;
|
||||
var params = this.collectParams();
|
||||
this._apply(params);
|
||||
};
|
||||
@ -259,12 +251,17 @@ DemodulatorPanel.prototype.updateButtons = function() {
|
||||
}
|
||||
|
||||
DemodulatorPanel.prototype.setCenterFrequency = function(center_freq) {
|
||||
if (this.center_freq === center_freq) {
|
||||
return;
|
||||
var me = this;
|
||||
if (me.centerFreqTimeout) {
|
||||
clearTimeout(me.centerFreqTimeout);
|
||||
me.centerFreqTimeout = false;
|
||||
}
|
||||
this.stopDemodulator();
|
||||
this.center_freq = center_freq;
|
||||
this.startDemodulator();
|
||||
this.centerFreqTimeout = setTimeout(function() {
|
||||
me.stopDemodulator();
|
||||
me.center_freq = center_freq;
|
||||
me.startDemodulator();
|
||||
me.centerFreqTimeout = false;
|
||||
}, 50);
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.parseHash = function() {
|
||||
@ -286,7 +283,7 @@ DemodulatorPanel.prototype.validateHash = function(params) {
|
||||
var self = this;
|
||||
params = Object.keys(params).filter(function(key) {
|
||||
if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') {
|
||||
return params.freq && Math.abs(params.freq - self.center_freq) < bandwidth;
|
||||
return params.freq && Math.abs(params.freq - self.center_freq) < bandwidth / 2;
|
||||
}
|
||||
return true;
|
||||
}).reduce(function(p, key) {
|
||||
|
@ -58,7 +58,7 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
||||
var index = me.digitContainer.find('.digit').index(e.target);
|
||||
if (index < 0) return;
|
||||
|
||||
var delta = 10 ** (Math.floor(Math.log10(me.frequency)) - index);
|
||||
var delta = 10 ** (Math.floor(Math.max(6, Math.log10(me.frequency))) - index);
|
||||
if (e.originalEvent.deltaY > 0) delta *= -1;
|
||||
var newFrequency = me.frequency + delta;
|
||||
|
||||
|
@ -85,7 +85,7 @@ AudioSpeedProgressBar.prototype.getDefaultText = function() {
|
||||
};
|
||||
|
||||
AudioSpeedProgressBar.prototype.setSpeed = function(speed) {
|
||||
this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false);
|
||||
this.set(speed / 1000000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false);
|
||||
};
|
||||
|
||||
AudioOutputProgressBar = function(el, sampleRate) {
|
||||
|
@ -44,13 +44,11 @@ function toggleMute() {
|
||||
if (mute) {
|
||||
mute = false;
|
||||
e("openwebrx-mute-on").id = "openwebrx-mute-off";
|
||||
e("openwebrx-mute-img").src = "static/gfx/openwebrx-speaker.png";
|
||||
e("openwebrx-panel-volume").disabled = false;
|
||||
e("openwebrx-panel-volume").value = volumeBeforeMute;
|
||||
} else {
|
||||
mute = true;
|
||||
e("openwebrx-mute-off").id = "openwebrx-mute-on";
|
||||
e("openwebrx-mute-img").src = "static/gfx/openwebrx-speaker-muted.png";
|
||||
e("openwebrx-panel-volume").disabled = true;
|
||||
volumeBeforeMute = e("openwebrx-panel-volume").value;
|
||||
e("openwebrx-panel-volume").value = 0;
|
||||
@ -79,31 +77,75 @@ var waterfall_min_level;
|
||||
var waterfall_max_level;
|
||||
var waterfall_min_level_default;
|
||||
var waterfall_max_level_default;
|
||||
var waterfall_colors;
|
||||
var waterfall_colors = buildWaterfallColors(['#000', '#FFF']);
|
||||
var waterfall_auto_level_margin;
|
||||
|
||||
function buildWaterfallColors(input) {
|
||||
return chroma.scale(input).colors(256, 'rgb')
|
||||
}
|
||||
|
||||
function updateWaterfallColors(which) {
|
||||
var wfmax = e("openwebrx-waterfall-color-max");
|
||||
var wfmin = e("openwebrx-waterfall-color-min");
|
||||
if (parseInt(wfmin.value) >= parseInt(wfmax.value)) {
|
||||
if (!which) wfmin.value = (parseInt(wfmax.value) - 1).toString();
|
||||
else wfmax.value = (parseInt(wfmin.value) + 1).toString();
|
||||
var $wfmax = $("#openwebrx-waterfall-color-max");
|
||||
var $wfmin = $("#openwebrx-waterfall-color-min");
|
||||
waterfall_max_level = parseInt($wfmax.val());
|
||||
waterfall_min_level = parseInt($wfmin.val());
|
||||
if (waterfall_min_level >= waterfall_max_level) {
|
||||
if (!which) {
|
||||
waterfall_min_level = waterfall_max_level -1;
|
||||
} else {
|
||||
waterfall_max_level = waterfall_min_level + 1;
|
||||
}
|
||||
}
|
||||
waterfall_min_level = parseInt(wfmin.value);
|
||||
waterfall_max_level = parseInt(wfmax.value);
|
||||
updateWaterfallSliders();
|
||||
}
|
||||
|
||||
function updateWaterfallSliders() {
|
||||
$('#openwebrx-waterfall-color-max')
|
||||
.val(waterfall_max_level)
|
||||
.attr('title', 'Waterfall maximum level (' + Math.round(waterfall_max_level) + ' dB)');
|
||||
$('#openwebrx-waterfall-color-min')
|
||||
.val(waterfall_min_level)
|
||||
.attr('title', 'Waterfall minimum level (' + Math.round(waterfall_min_level) + ' dB)');
|
||||
}
|
||||
|
||||
function waterfallColorsDefault() {
|
||||
waterfall_min_level = waterfall_min_level_default;
|
||||
waterfall_max_level = waterfall_max_level_default;
|
||||
e("openwebrx-waterfall-color-min").value = waterfall_min_level.toString();
|
||||
e("openwebrx-waterfall-color-max").value = waterfall_max_level.toString();
|
||||
updateWaterfallSliders();
|
||||
waterfallColorsContinuousReset();
|
||||
}
|
||||
|
||||
function waterfallColorsAuto() {
|
||||
e("openwebrx-waterfall-color-min").value = (waterfall_measure_minmax_min - waterfall_auto_level_margin.min).toString();
|
||||
e("openwebrx-waterfall-color-max").value = (waterfall_measure_minmax_max + waterfall_auto_level_margin.max).toString();
|
||||
updateWaterfallColors(0);
|
||||
function waterfallColorsAuto(levels) {
|
||||
var min_level = levels.min - waterfall_auto_level_margin.min;
|
||||
var max_level = levels.max + waterfall_auto_level_margin.max;
|
||||
max_level = Math.max(min_level + (waterfall_auto_level_margin.min_range || 0), max_level);
|
||||
waterfall_min_level = min_level;
|
||||
waterfall_max_level = max_level;
|
||||
updateWaterfallSliders();
|
||||
}
|
||||
|
||||
var waterfall_continuous = {
|
||||
min: -150,
|
||||
max: 0
|
||||
};
|
||||
|
||||
function waterfallColorsContinuousReset() {
|
||||
waterfall_continuous.min = waterfall_min_level;
|
||||
waterfall_continuous.max = waterfall_max_level;
|
||||
}
|
||||
|
||||
function waterfallColorsContinuous(levels) {
|
||||
if (levels.max > waterfall_continuous.max + 1) {
|
||||
waterfall_continuous.max += 1;
|
||||
} else if (levels.max < waterfall_continuous.max - 1) {
|
||||
waterfall_continuous.max -= .1;
|
||||
}
|
||||
if (levels.min < waterfall_continuous.min - 1) {
|
||||
waterfall_continuous.min -= 1;
|
||||
} else if (levels.min > waterfall_continuous.min + 1) {
|
||||
waterfall_continuous.min += .1;
|
||||
}
|
||||
waterfallColorsAuto(waterfall_continuous);
|
||||
}
|
||||
|
||||
function setSmeterRelativeValue(value) {
|
||||
@ -135,10 +177,6 @@ function getLogSmeterValue(value) {
|
||||
return 10 * Math.log10(value);
|
||||
}
|
||||
|
||||
function getLinearSmeterValue(db_value) {
|
||||
return Math.pow(10, db_value / 10);
|
||||
}
|
||||
|
||||
function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch_and_smeter_cc`
|
||||
{
|
||||
var logValue = getLogSmeterValue(value);
|
||||
@ -172,7 +210,7 @@ function getDemodulators() {
|
||||
].filter(function(d) {
|
||||
return !!d;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function mkenvelopes(visible_range) //called from mkscale
|
||||
{
|
||||
@ -182,7 +220,7 @@ function mkenvelopes(visible_range) //called from mkscale
|
||||
demodulators[i].envelope.draw(visible_range);
|
||||
}
|
||||
if (demodulators.length) {
|
||||
var bandpass = demodulators[0].getBandpass()
|
||||
var bandpass = demodulators[0].getBandpass();
|
||||
secondary_demod_waterfall_set_zoom(bandpass.low_cut, bandpass.high_cut);
|
||||
}
|
||||
}
|
||||
@ -662,7 +700,7 @@ function on_ws_recv(evt) {
|
||||
switch (json.type) {
|
||||
case "config":
|
||||
var config = json['value'];
|
||||
waterfall_colors = config['waterfall_colors'];
|
||||
waterfall_colors = buildWaterfallColors(config['waterfall_colors']);
|
||||
waterfall_min_level_default = config['waterfall_min_level'];
|
||||
waterfall_max_level_default = config['waterfall_max_level'];
|
||||
waterfall_auto_level_margin = config['waterfall_auto_level_margin'];
|
||||
@ -686,8 +724,8 @@ function on_ws_recv(evt) {
|
||||
|
||||
waterfall_init();
|
||||
var demodulatorPanel = $('#openwebrx-panel-receiver').demodulatorPanel();
|
||||
demodulatorPanel.setInitialParams(initial_demodulator_params);
|
||||
demodulatorPanel.setCenterFrequency(center_freq);
|
||||
demodulatorPanel.setInitialParams(initial_demodulator_params);
|
||||
bookmarks.loadLocalBookmarks();
|
||||
|
||||
waterfall_clear();
|
||||
@ -829,6 +867,10 @@ function on_ws_recv(evt) {
|
||||
secondary_demod_waterfall_add(waterfall_f32);
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
// hd audio data
|
||||
audioEngine.pushHdAudio(data);
|
||||
break;
|
||||
default:
|
||||
console.warn('unknown type of binary message: ' + type)
|
||||
}
|
||||
@ -1039,17 +1081,17 @@ function clear_metadata() {
|
||||
$(".openwebrx-dmr-timeslot-panel").removeClass("muted");
|
||||
}
|
||||
|
||||
var waterfall_measure_minmax = false;
|
||||
var waterfall_measure_minmax_now = false;
|
||||
var waterfall_measure_minmax_min = 1e100;
|
||||
var waterfall_measure_minmax_max = -1e100;
|
||||
var waterfall_measure_minmax_continuous = false;
|
||||
|
||||
function waterfall_measure_minmax_do(what) {
|
||||
// this is based on an oversampling factor of about 1,25
|
||||
var ignored = .1 * what.length;
|
||||
var data = what.slice(ignored, -ignored);
|
||||
waterfall_measure_minmax_min = Math.min(waterfall_measure_minmax_min, Math.min.apply(Math, data));
|
||||
waterfall_measure_minmax_max = Math.max(waterfall_measure_minmax_max, Math.max.apply(Math, data));
|
||||
return {
|
||||
min: Math.min.apply(Math, data),
|
||||
max: Math.max.apply(Math, data)
|
||||
};
|
||||
}
|
||||
|
||||
function on_ws_opened() {
|
||||
@ -1067,7 +1109,10 @@ function on_ws_opened() {
|
||||
reconnect_timeout = false;
|
||||
ws.send(JSON.stringify({
|
||||
"type": "connectionproperties",
|
||||
"params": {"output_rate": audioEngine.getOutputRate()}
|
||||
"params": {
|
||||
"output_rate": audioEngine.getOutputRate(),
|
||||
"hd_output_rate": audioEngine.getHdOutputRate()
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@ -1092,9 +1137,11 @@ var mute = false;
|
||||
// Optimalise these if audio lags or is choppy:
|
||||
var audio_buffer_maximal_length_sec = 1; //actual number of samples are calculated from sample rate
|
||||
|
||||
function onAudioStart(success, apiType){
|
||||
function onAudioStart(apiType){
|
||||
divlog('Web Audio API succesfully initialized, using ' + apiType + ' API, sample rate: ' + audioEngine.getSampleRate() + " Hz");
|
||||
|
||||
hideOverlay();
|
||||
|
||||
// canvas_container is set after waterfall_init() has been called. we cannot initialize before.
|
||||
//if (canvas_container) synchronize_demodulator_init();
|
||||
|
||||
@ -1160,25 +1207,22 @@ function open_websocket() {
|
||||
}
|
||||
|
||||
function waterfall_mkcolor(db_value, waterfall_colors_arg) {
|
||||
if (typeof waterfall_colors_arg === 'undefined') waterfall_colors_arg = waterfall_colors;
|
||||
if (db_value < waterfall_min_level) db_value = waterfall_min_level;
|
||||
if (db_value > waterfall_max_level) db_value = waterfall_max_level;
|
||||
var full_scale = waterfall_max_level - waterfall_min_level;
|
||||
var relative_value = db_value - waterfall_min_level;
|
||||
var value_percent = relative_value / full_scale;
|
||||
var percent_for_one_color = 1 / (waterfall_colors_arg.length - 1);
|
||||
var index = Math.floor(value_percent / percent_for_one_color);
|
||||
var remain = (value_percent - percent_for_one_color * index) / percent_for_one_color;
|
||||
return color_between(waterfall_colors_arg[index + 1], waterfall_colors_arg[index], remain);
|
||||
}
|
||||
waterfall_colors_arg = waterfall_colors_arg || waterfall_colors;
|
||||
var value_percent = (db_value - waterfall_min_level) / (waterfall_max_level - waterfall_min_level);
|
||||
value_percent = Math.max(0, Math.min(1, value_percent));
|
||||
|
||||
var scaled = value_percent * (waterfall_colors_arg.length - 1);
|
||||
var index = Math.floor(scaled);
|
||||
var remain = scaled - index;
|
||||
if (remain === 0) return waterfall_colors_arg[index];
|
||||
return color_between(waterfall_colors_arg[index], waterfall_colors_arg[index + 1], remain);}
|
||||
|
||||
function color_between(first, second, percent) {
|
||||
var output = 0;
|
||||
for (var i = 0; i < 4; i++) {
|
||||
var add = ((((first & (0xff << (i * 8))) >>> 0) * percent) + (((second & (0xff << (i * 8))) >>> 0) * (1 - percent))) & (0xff << (i * 8));
|
||||
output |= add >>> 0;
|
||||
}
|
||||
return output >>> 0;
|
||||
return [
|
||||
first[0] + percent * (second[0] - first[0]),
|
||||
first[1] + percent * (second[1] - first[1]),
|
||||
first[2] + percent * (second[2] - first[2])
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -1249,19 +1293,24 @@ function waterfall_add(data) {
|
||||
if (!waterfall_setup_done) return;
|
||||
var w = fft_size;
|
||||
|
||||
if (waterfall_measure_minmax) waterfall_measure_minmax_do(data);
|
||||
if (waterfall_measure_minmax_now) {
|
||||
waterfall_measure_minmax_do(data);
|
||||
var levels = waterfall_measure_minmax_do(data);
|
||||
waterfall_measure_minmax_now = false;
|
||||
waterfallColorsAuto();
|
||||
waterfallColorsAuto(levels);
|
||||
waterfallColorsContinuousReset();
|
||||
}
|
||||
|
||||
if (waterfall_measure_minmax_continuous) {
|
||||
var level = waterfall_measure_minmax_do(data);
|
||||
waterfallColorsContinuous(level);
|
||||
}
|
||||
|
||||
//Add line to waterfall image
|
||||
var oneline_image = canvas_context.createImageData(w, 1);
|
||||
for (var x = 0; x < w; x++) {
|
||||
var color = waterfall_mkcolor(data[x]);
|
||||
for (i = 0; i < 4; i++)
|
||||
oneline_image.data[x * 4 + i] = ((color >>> 0) >> ((3 - i) * 8)) & 0xff;
|
||||
for (i = 0; i < 3; i++) oneline_image.data[x * 4 + i] = color[i];
|
||||
oneline_image.data[x * 4 + 3] = 255;
|
||||
}
|
||||
|
||||
//Draw image
|
||||
@ -1313,11 +1362,12 @@ var audioEngine;
|
||||
function openwebrx_init() {
|
||||
audioEngine = new AudioEngine(audio_buffer_maximal_length_sec, audioReporter);
|
||||
$overlay = $('#openwebrx-autoplay-overlay');
|
||||
$overlay.on('click', playButtonClick);
|
||||
$overlay.on('click', function(){
|
||||
audioEngine.resume();
|
||||
});
|
||||
audioEngine.onStart(onAudioStart);
|
||||
if (!audioEngine.isAllowed()) {
|
||||
$overlay.show();
|
||||
} else {
|
||||
audioEngine.start(onAudioStart);
|
||||
}
|
||||
fft_codec = new ImaAdpcmCodec();
|
||||
initProgressBars();
|
||||
@ -1344,6 +1394,18 @@ function initSliders() {
|
||||
$slider.val(val + step);
|
||||
$slider.trigger('change');
|
||||
});
|
||||
|
||||
var waterfallAutoButton = $('#openwebrx-waterfall-colors-auto');
|
||||
waterfallAutoButton.on('click', function() {
|
||||
waterfall_measure_minmax_now=true;
|
||||
}).on('contextmenu', function(){
|
||||
waterfall_measure_minmax_continuous = !waterfall_measure_minmax_continuous;
|
||||
waterfallColorsContinuousReset();
|
||||
waterfallAutoButton[waterfall_measure_minmax_continuous ? 'addClass' : 'removeClass']('highlighted');
|
||||
$('#openwebrx-waterfall-color-min, #openwebrx-waterfall-color-max').prop('disabled', waterfall_measure_minmax_continuous);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function digimodes_init() {
|
||||
@ -1363,9 +1425,7 @@ function update_dmr_timeslot_filtering() {
|
||||
$('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().setDmrFilter(filter);
|
||||
}
|
||||
|
||||
function playButtonClick() {
|
||||
//On iOS, we can only start audio from a click or touch event.
|
||||
audioEngine.start(onAudioStart);
|
||||
function hideOverlay() {
|
||||
var $overlay = $('#openwebrx-autoplay-overlay');
|
||||
$overlay.css('opacity', 0);
|
||||
$overlay.on('transitionend', function() {
|
||||
@ -1463,9 +1523,8 @@ function initPanels() {
|
||||
|___/
|
||||
*/
|
||||
|
||||
var secondary_demod_fft_offset_db = 30; //need to calculate that later
|
||||
var secondary_demod_fft_offset_db = 18; //need to calculate that later
|
||||
var secondary_demod_canvases_initialized = false;
|
||||
var secondary_demod_listbox_updating = false;
|
||||
var secondary_demod_channel_freq = 1000;
|
||||
var secondary_demod_waiting_for_set = false;
|
||||
var secondary_demod_low_cut;
|
||||
@ -1557,7 +1616,8 @@ function secondary_demod_waterfall_add(data) {
|
||||
var oneline_image = secondary_demod_current_canvas_context.createImageData(w, 1);
|
||||
for (var x = 0; x < w; x++) {
|
||||
var color = waterfall_mkcolor(data[x] + secondary_demod_fft_offset_db);
|
||||
for (var i = 0; i < 4; i++) oneline_image.data[x * 4 + i] = ((color >>> 0) >> ((3 - i) * 8)) & 0xff;
|
||||
for (var i = 0; i < 3; i++) oneline_image.data[x * 4 + i] = color[i];
|
||||
oneline_image.data[x * 4 + 3] = 255;
|
||||
}
|
||||
|
||||
//Draw image
|
||||
@ -1571,8 +1631,8 @@ function secondary_demod_waterfall_add(data) {
|
||||
}
|
||||
|
||||
function secondary_demod_update_marker() {
|
||||
var width = Math.max((secondary_bw / (if_samp_rate / 2)) * secondary_demod_canvas_width, 5);
|
||||
var center_at = (secondary_demod_channel_freq / (if_samp_rate / 2)) * secondary_demod_canvas_width + secondary_demod_canvas_left;
|
||||
var width = Math.max((secondary_bw / if_samp_rate) * secondary_demod_canvas_width, 5);
|
||||
var center_at = ((secondary_demod_channel_freq - secondary_demod_low_cut) / if_samp_rate) * secondary_demod_canvas_width;
|
||||
var left = center_at - width / 2;
|
||||
$("#openwebrx-digimode-select-channel").width(width).css("left", left + "px")
|
||||
}
|
||||
@ -1620,26 +1680,17 @@ function secondary_demod_canvas_container_mouseup(evt) {
|
||||
|
||||
function secondary_demod_waterfall_set_zoom(low_cut, high_cut) {
|
||||
if (!secondary_demod_canvases_initialized) return;
|
||||
if (low_cut < 0 && high_cut < 0) {
|
||||
var hctmp = high_cut;
|
||||
var lctmp = low_cut;
|
||||
low_cut = -hctmp;
|
||||
high_cut = -lctmp;
|
||||
}
|
||||
else if (low_cut < 0 && high_cut > 0) {
|
||||
high_cut = Math.max(Math.abs(high_cut), Math.abs(low_cut));
|
||||
low_cut = 0;
|
||||
}
|
||||
secondary_demod_low_cut = low_cut;
|
||||
secondary_demod_high_cut = high_cut;
|
||||
var shown_bw = high_cut - low_cut;
|
||||
secondary_demod_canvas_width = $(secondary_demod_canvas_container).width() * (if_samp_rate / 2) / shown_bw;
|
||||
secondary_demod_canvas_left = -secondary_demod_canvas_width * (low_cut / (if_samp_rate / 2));
|
||||
//console.log("setzoom", secondary_demod_canvas_width, secondary_demod_canvas_left, low_cut, high_cut);
|
||||
secondary_demod_canvas_width = $(secondary_demod_canvas_container).width() * (if_samp_rate) / shown_bw;
|
||||
secondary_demod_canvas_left = (-secondary_demod_canvas_width / 2) - (low_cut / if_samp_rate) * secondary_demod_canvas_width;
|
||||
secondary_demod_canvases.map(function (x) {
|
||||
$(x).css("left", secondary_demod_canvas_left + "px").css("width", secondary_demod_canvas_width + "px");
|
||||
})
|
||||
;
|
||||
$(x).css({
|
||||
left: secondary_demod_canvas_left + "px",
|
||||
width: secondary_demod_canvas_width + "px"
|
||||
});
|
||||
});
|
||||
secondary_demod_update_channel_freq_from_event();
|
||||
}
|
||||
|
||||
|
15
manifest.sh
15
manifest.sh
@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
. docker/env
|
||||
|
||||
for image in ${IMAGES}; do
|
||||
# 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}"
|
||||
IMAGE_LIST=""
|
||||
for a in $ALL_ARCHS; do
|
||||
IMAGE_LIST="$IMAGE_LIST jketterl/$image:$TAG-$a"
|
||||
done
|
||||
docker manifest create jketterl/$image:$TAG $IMAGE_LIST
|
||||
docker manifest push --purge jketterl/$image:$TAG
|
||||
docker pull jketterl/$image:$TAG
|
||||
done
|
@ -50,7 +50,7 @@ Support and info: https://groups.io/g/openwebrx
|
||||
if not featureDetector.is_available("core"):
|
||||
logger.error(
|
||||
"you are missing required dependencies to run openwebrx. "
|
||||
"please check that the following core requirements are installed:"
|
||||
"please check that the following core requirements are installed and up to date:"
|
||||
)
|
||||
logger.error(", ".join(featureDetector.get_requirements("core")))
|
||||
return
|
||||
|
43
owrx/aprs.py
43
owrx/aprs.py
@ -33,6 +33,9 @@ thirdpartyeRegex = re.compile("^([a-zA-Z0-9-]+)>((([a-zA-Z0-9-]+\\*?,)*)([a-zA-Z
|
||||
# regex for getting the message id out of message
|
||||
messageIdRegex = re.compile("^(.*){([0-9]{1,5})$")
|
||||
|
||||
# regex to filter pseudo "WIDE" path elements
|
||||
widePattern = re.compile("^WIDE[0-9]-[0-9]$")
|
||||
|
||||
|
||||
def decodeBase91(input):
|
||||
base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0
|
||||
@ -154,23 +157,33 @@ class AprsParser(Parser):
|
||||
super().__init__(handler)
|
||||
self.ax25parser = Ax25Parser()
|
||||
self.deframer = KissDeframer()
|
||||
self.metric = self.getMetric()
|
||||
self.metrics = {}
|
||||
|
||||
def setDialFrequency(self, freq):
|
||||
super().setDialFrequency(freq)
|
||||
self.metric = self.getMetric()
|
||||
self.metrics = {}
|
||||
|
||||
def getMetric(self):
|
||||
band = "unknown"
|
||||
if self.band is not None:
|
||||
band = self.band.getName()
|
||||
name = "aprs.decodes.{band}.aprs".format(band=band)
|
||||
metrics = Metrics.getSharedInstance()
|
||||
metric = metrics.getMetric(name)
|
||||
if metric is None:
|
||||
metric = CounterMetric()
|
||||
metrics.addMetric(name, metric)
|
||||
return metric
|
||||
def getMetric(self, category):
|
||||
if category not in self.metrics:
|
||||
band = "unknown"
|
||||
if self.band is not None:
|
||||
band = self.band.getName()
|
||||
name = "aprs.decodes.{band}.aprs.{category}".format(band=band, category=category)
|
||||
metrics = Metrics.getSharedInstance()
|
||||
self.metrics[category] = metrics.getMetric(name)
|
||||
if self.metrics[category] is None:
|
||||
self.metrics[category] = CounterMetric()
|
||||
metrics.addMetric(name, self.metrics[category])
|
||||
return self.metrics[category]
|
||||
|
||||
def isDirect(self, aprsData):
|
||||
if "path" in aprsData and len(aprsData["path"]) > 0:
|
||||
hops = [host for host in aprsData["path"] if widePattern.match(host) is None]
|
||||
if len(hops) > 0:
|
||||
return False
|
||||
if "type" in aprsData and aprsData["type"] in ["thirdparty", "item", "object"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def parse(self, raw):
|
||||
for frame in self.deframer.parse(raw):
|
||||
@ -182,7 +195,9 @@ class AprsParser(Parser):
|
||||
|
||||
logger.debug("decoded APRS data: %s", aprsData)
|
||||
self.updateMap(aprsData)
|
||||
self.metric.inc()
|
||||
self.getMetric("total").inc()
|
||||
if self.isDirect(aprsData):
|
||||
self.getMetric("direct").inc()
|
||||
self.handler.write_aprs_data(aprsData)
|
||||
except Exception:
|
||||
logger.exception("exception while parsing aprs data")
|
||||
|
@ -183,9 +183,14 @@ class AudioWriter(object):
|
||||
stdout=subprocess.PIPE,
|
||||
cwd=self.tmp_dir,
|
||||
close_fds=True,
|
||||
)
|
||||
for line in decoder.stdout:
|
||||
self.outputWriter.send((job.freq, line))
|
||||
)
|
||||
try:
|
||||
for line in decoder.stdout:
|
||||
self.outputWriter.send((job.freq, line))
|
||||
except OSError:
|
||||
decoder.stdout.flush()
|
||||
# TODO uncouple parsing from the output so that decodes can still go to the map and the spotters
|
||||
logger.debug("output has gone away while decoding job.")
|
||||
try:
|
||||
rc = decoder.wait(timeout=10)
|
||||
if rc != 0:
|
||||
@ -204,13 +209,30 @@ class AudioWriter(object):
|
||||
self.switchingLock.release()
|
||||
|
||||
def stop(self):
|
||||
self.outputReader.close()
|
||||
self.outputWriter.close()
|
||||
self.outputWriter = None
|
||||
|
||||
# drain messages left in the queue so that the queue can be successfully closed
|
||||
# this is necessary since python keeps the file descriptors open otherwise
|
||||
try:
|
||||
while True:
|
||||
self.outputReader.recv()
|
||||
except EOFError:
|
||||
pass
|
||||
self.outputReader.close()
|
||||
self.outputReader = None
|
||||
|
||||
self.cancelTimer()
|
||||
try:
|
||||
self.wavefile.close()
|
||||
except Exception:
|
||||
logger.exception("error closing wave file")
|
||||
try:
|
||||
os.unlink(self.wavefilename)
|
||||
except Exception:
|
||||
logger.exception("error removing undecoded file")
|
||||
self.wavefile = None
|
||||
self.wavefilename = None
|
||||
|
||||
|
||||
class AudioChopper(threading.Thread, metaclass=ABCMeta):
|
||||
@ -225,7 +247,11 @@ class AudioChopper(threading.Thread, metaclass=ABCMeta):
|
||||
for w in self.writers:
|
||||
w.start()
|
||||
while self.doRun:
|
||||
data = self.source.read(256)
|
||||
data = None
|
||||
try:
|
||||
data = self.source.read(256)
|
||||
except ValueError:
|
||||
pass
|
||||
if data is None or (isinstance(data, bytes) and len(data) == 0):
|
||||
self.doRun = False
|
||||
else:
|
||||
@ -240,5 +266,5 @@ class AudioChopper(threading.Thread, metaclass=ABCMeta):
|
||||
try:
|
||||
readers = wait([w.outputReader for w in self.writers])
|
||||
return [r.recv() for r in readers]
|
||||
except EOFError:
|
||||
except (EOFError, OSError):
|
||||
return None
|
||||
|
@ -1,5 +1,4 @@
|
||||
from owrx.config import Config
|
||||
from owrx.metrics import Metrics, DirectMetric
|
||||
import threading
|
||||
|
||||
import logging
|
||||
@ -24,7 +23,6 @@ class ClientRegistry(object):
|
||||
|
||||
def __init__(self):
|
||||
self.clients = []
|
||||
Metrics.getSharedInstance().addMetric("openwebrx.users", DirectMetric(self.clientCount))
|
||||
super().__init__()
|
||||
|
||||
def broadcast(self):
|
||||
|
@ -72,3 +72,8 @@ class Option(CommandMapping):
|
||||
def setSpacer(self, spacer):
|
||||
self.spacer = spacer
|
||||
return self
|
||||
|
||||
|
||||
class Argument(CommandMapping):
|
||||
def map(self, value):
|
||||
return value
|
||||
|
@ -49,11 +49,19 @@ class ConfigMigratorVersion1(ConfigMigrator):
|
||||
return config
|
||||
|
||||
|
||||
class ConfigMigratorVersion2(ConfigMigrator):
|
||||
def migrate(self, config):
|
||||
if "waterfall_colors" in config and any(v > 0xFFFFFF for v in config["waterfall_colors"]):
|
||||
config["waterfall_colors"] = [v >> 8 for v in config["waterfall_colors"]]
|
||||
return config
|
||||
|
||||
|
||||
class Config:
|
||||
sharedConfig = None
|
||||
currentVersion = 2
|
||||
currentVersion = 3
|
||||
migrators = {
|
||||
1: ConfigMigratorVersion1()
|
||||
1: ConfigMigratorVersion1(),
|
||||
2: ConfigMigratorVersion2(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@ -130,5 +138,7 @@ class Config:
|
||||
return config
|
||||
|
||||
logger.debug("migrating config from version %i", version)
|
||||
migrator = Config.migrators[version]
|
||||
return migrator.migrate(config)
|
||||
migrators = [Config.migrators[i] for i in range(version, Config.currentVersion)]
|
||||
for migrator in migrators:
|
||||
config = migrator.migrate(config)
|
||||
return config
|
||||
|
@ -3,7 +3,7 @@ from owrx.details import ReceiverDetails
|
||||
from owrx.dsp import DspManager
|
||||
from owrx.cpu import CpuUsageThread
|
||||
from owrx.sdr import SdrService
|
||||
from owrx.source import SdrSource
|
||||
from owrx.source import SdrSource, SdrSourceEventClient
|
||||
from owrx.client import ClientRegistry, TooManyClientsException
|
||||
from owrx.feature import FeatureDetector
|
||||
from owrx.version import openwebrx_version
|
||||
@ -12,8 +12,7 @@ from owrx.bookmarks import Bookmarks
|
||||
from owrx.map import Map
|
||||
from owrx.property import PropertyStack
|
||||
from owrx.modes import Modes, DigitalMode
|
||||
from multiprocessing import Queue
|
||||
from queue import Full
|
||||
from queue import Queue, Full, Empty
|
||||
from js8py import Js8Frame
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
import json
|
||||
@ -23,33 +22,59 @@ import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PoisonPill = object()
|
||||
|
||||
|
||||
class Client(ABC):
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.multiprocessingPipe = Queue(100)
|
||||
self.multithreadingQueue = Queue(100)
|
||||
|
||||
def mp_passthru():
|
||||
run = True
|
||||
while run:
|
||||
try:
|
||||
data = self.multiprocessingPipe.get()
|
||||
self.send(data)
|
||||
except (EOFError, OSError):
|
||||
data = self.multithreadingQueue.get()
|
||||
if data is PoisonPill:
|
||||
run = False
|
||||
else:
|
||||
self.send(data)
|
||||
self.multithreadingQueue.task_done()
|
||||
except (EOFError, OSError, ValueError):
|
||||
run = False
|
||||
except Exception:
|
||||
logger.exception("Exception on client multithreading queue")
|
||||
|
||||
threading.Thread(target=mp_passthru).start()
|
||||
# unset the queue object to free shared memory file descriptors
|
||||
self.multithreadingQueue = None
|
||||
|
||||
threading.Thread(target=mp_passthru, name="connection_mp_passthru").start()
|
||||
|
||||
def send(self, data):
|
||||
self.conn.send(data)
|
||||
try:
|
||||
self.conn.send(data)
|
||||
except IOError:
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self.multithreadingQueue is not None:
|
||||
while True:
|
||||
try:
|
||||
self.multithreadingQueue.get(block=False)
|
||||
except Empty:
|
||||
break
|
||||
try:
|
||||
self.multithreadingQueue.put(PoisonPill, block=False)
|
||||
except Full:
|
||||
# this shouldn't happen, we just emptied the queue, but it's not worth risking the exception
|
||||
logger.exception("impossible queue state: Full after Empty")
|
||||
self.conn.close()
|
||||
self.multiprocessingPipe.close()
|
||||
|
||||
def mp_send(self, data):
|
||||
if self.multithreadingQueue is None:
|
||||
return
|
||||
try:
|
||||
self.multiprocessingPipe.put(data, block=False)
|
||||
self.multithreadingQueue.put(data, block=False)
|
||||
except Full:
|
||||
self.close()
|
||||
|
||||
@ -82,7 +107,7 @@ class OpenWebRxClient(Client, metaclass=ABCMeta):
|
||||
self.send({"type": "receiver_details", "value": details})
|
||||
|
||||
|
||||
class OpenWebRxReceiverClient(OpenWebRxClient):
|
||||
class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
|
||||
config_keys = [
|
||||
"waterfall_colors",
|
||||
"waterfall_min_level",
|
||||
@ -127,6 +152,23 @@ class OpenWebRxReceiverClient(OpenWebRxClient):
|
||||
|
||||
CpuUsageThread.getSharedInstance().add_client(self)
|
||||
|
||||
def onStateChange(self, state):
|
||||
if state == SdrSource.STATE_RUNNING:
|
||||
self.handleSdrAvailable()
|
||||
elif state == SdrSource.STATE_FAILED:
|
||||
self.handleSdrFailed()
|
||||
|
||||
def handleSdrFailed(self):
|
||||
logger.warning('SDR device "%s" has failed, selecting new device', self.sdr.getName())
|
||||
self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
|
||||
self.setSdr()
|
||||
|
||||
def onBusyStateChange(self, state):
|
||||
pass
|
||||
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_USER
|
||||
|
||||
def __sendProfiles(self):
|
||||
profiles = [
|
||||
{"name": s.getName() + " " + p["name"], "id": sid + "|" + pid}
|
||||
@ -175,39 +217,35 @@ class OpenWebRxReceiverClient(OpenWebRxClient):
|
||||
logger.warning("message is not json: {0}".format(message))
|
||||
|
||||
def setSdr(self, id=None):
|
||||
while True:
|
||||
next = None
|
||||
if id is not None:
|
||||
next = SdrService.getSource(id)
|
||||
if next is None:
|
||||
next = SdrService.getFirstSource()
|
||||
if next is None:
|
||||
# exit condition: no sdrs available
|
||||
logger.warning("no more SDR devices available")
|
||||
self.handleNoSdrsAvailable()
|
||||
return
|
||||
next = None
|
||||
if id is not None:
|
||||
next = SdrService.getSource(id)
|
||||
if next is None:
|
||||
next = SdrService.getFirstSource()
|
||||
|
||||
# exit condition: no change
|
||||
if next == self.sdr:
|
||||
return
|
||||
# exit condition: no change
|
||||
if next == self.sdr and next is not None:
|
||||
return
|
||||
|
||||
self.stopDsp()
|
||||
self.stopDsp()
|
||||
|
||||
if self.configSub is not None:
|
||||
self.configSub.cancel()
|
||||
self.configSub = None
|
||||
if self.configSub is not None:
|
||||
self.configSub.cancel()
|
||||
self.configSub = None
|
||||
|
||||
self.sdr = next
|
||||
if self.sdr is not None:
|
||||
self.sdr.removeClient(self)
|
||||
|
||||
self.getDsp()
|
||||
if next is None:
|
||||
# exit condition: no sdrs available
|
||||
logger.warning("no more SDR devices available")
|
||||
self.handleNoSdrsAvailable()
|
||||
return
|
||||
|
||||
# found a working sdr, exit the loop
|
||||
if self.sdr.getState() != SdrSource.STATE_FAILED:
|
||||
break
|
||||
|
||||
logger.warning('SDR device "%s" has failed, selecing new device', self.sdr.getName())
|
||||
self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
|
||||
self.sdr = next
|
||||
self.sdr.addClient(self)
|
||||
|
||||
def handleSdrAvailable(self):
|
||||
# send initial config
|
||||
self.getDsp().setProperties(self.connectionProperties)
|
||||
|
||||
@ -244,6 +282,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient):
|
||||
self.getDsp().start()
|
||||
|
||||
def close(self):
|
||||
if self.sdr is not None:
|
||||
self.sdr.removeClient(self)
|
||||
self.stopDsp()
|
||||
CpuUsageThread.getSharedInstance().remove_client(self)
|
||||
ClientRegistry.getSharedInstance().removeClient(self)
|
||||
@ -289,6 +329,9 @@ class OpenWebRxReceiverClient(OpenWebRxClient):
|
||||
def write_dsp_data(self, data):
|
||||
self.send(bytes([0x02]) + data)
|
||||
|
||||
def write_hd_audio(self, data):
|
||||
self.send(bytes([0x04]) + data)
|
||||
|
||||
def write_s_meter_level(self, level):
|
||||
self.send({"type": "smeter", "value": level})
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
class Controller(object):
|
||||
@ -14,9 +14,9 @@ class Controller(object):
|
||||
if content_type is not None:
|
||||
headers["Content-Type"] = content_type
|
||||
if last_modified is not None:
|
||||
headers["Last-Modified"] = last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||
headers["Last-Modified"] = last_modified.astimezone(tz=timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||
if max_age is not None:
|
||||
headers["Cache-Control"] = "max-age: {0}".format(max_age)
|
||||
headers["Cache-Control"] = "max-age={0}".format(max_age)
|
||||
for key, value in headers.items():
|
||||
self.handler.send_header(key, value)
|
||||
self.handler.end_headers()
|
||||
|
@ -1,15 +1,66 @@
|
||||
from . import Controller
|
||||
from owrx.config import Config
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
import mimetypes
|
||||
import os
|
||||
import pkg_resources
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import gzip
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AssetsController(Controller, metaclass=ABCMeta):
|
||||
class GzipMixin(object):
|
||||
def send_response(self, content, headers=None, content_type="text/html", *args, **kwargs):
|
||||
if self.zipable(content_type) and "accept-encoding" in self.request.headers:
|
||||
accepted = [s.strip().lower() for s in self.request.headers['accept-encoding'].split(",")]
|
||||
if "gzip" in accepted:
|
||||
if type(content) == str:
|
||||
content = content.encode()
|
||||
content = self.gzip(content)
|
||||
if headers is None:
|
||||
headers = {}
|
||||
headers["Content-Encoding"] = "gzip"
|
||||
super().send_response(content, headers=headers, content_type=content_type, *args, **kwargs)
|
||||
|
||||
def zipable(self, content_type):
|
||||
types = [
|
||||
"application/javascript",
|
||||
"text/css",
|
||||
"text/html"
|
||||
]
|
||||
return content_type in types
|
||||
|
||||
def gzip(self, content):
|
||||
return gzip.compress(content)
|
||||
|
||||
|
||||
class ModificationAwareController(Controller, metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def getModified(self, file):
|
||||
return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)))
|
||||
pass
|
||||
|
||||
def wasModified(self, file):
|
||||
try:
|
||||
modified = self.getModified(file).replace(microsecond=0)
|
||||
|
||||
if modified is not None and "If-Modified-Since" in self.handler.headers:
|
||||
client_modified = datetime.strptime(
|
||||
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
|
||||
).replace(tzinfo=timezone.utc)
|
||||
if modified <= client_modified:
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AssetsController(GzipMixin, ModificationAwareController, metaclass=ABCMeta):
|
||||
def getModified(self, file):
|
||||
return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)), timezone.utc)
|
||||
|
||||
def openFile(self, file):
|
||||
return open(self.getFilePath(file), "rb")
|
||||
@ -22,13 +73,9 @@ class AssetsController(Controller, metaclass=ABCMeta):
|
||||
try:
|
||||
modified = self.getModified(file)
|
||||
|
||||
if modified is not None and "If-Modified-Since" in self.handler.headers:
|
||||
client_modified = datetime.strptime(
|
||||
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
|
||||
)
|
||||
if modified <= client_modified:
|
||||
self.send_response("", code=304)
|
||||
return
|
||||
if not self.wasModified(file):
|
||||
self.send_response("", code=304)
|
||||
return
|
||||
|
||||
f = self.openFile(file)
|
||||
data = f.read()
|
||||
@ -63,9 +110,10 @@ class AprsSymbolsController(AssetsController):
|
||||
return self.path + file
|
||||
|
||||
|
||||
class CompiledAssetsController(Controller):
|
||||
class CompiledAssetsController(GzipMixin, ModificationAwareController):
|
||||
profiles = {
|
||||
"receiver.js": [
|
||||
"lib/chroma.min.js",
|
||||
"openwebrx.js",
|
||||
"lib/jquery-3.2.1.min.js",
|
||||
"lib/jquery.nanoscroller.js",
|
||||
@ -107,13 +155,9 @@ class CompiledAssetsController(Controller):
|
||||
|
||||
modified = self.getModified(files)
|
||||
|
||||
if modified is not None and "If-Modified-Since" in self.handler.headers:
|
||||
client_modified = datetime.strptime(
|
||||
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
|
||||
)
|
||||
if modified <= client_modified:
|
||||
self.send_response("", code=304)
|
||||
return
|
||||
if not self.wasModified(files):
|
||||
self.send_response("", code=304)
|
||||
return
|
||||
|
||||
contents = [self.getContents(f) for f in files]
|
||||
|
||||
@ -125,5 +169,5 @@ class CompiledAssetsController(Controller):
|
||||
return f.read()
|
||||
|
||||
def getModified(self, files):
|
||||
modified = [datetime.fromtimestamp(os.path.getmtime(f)) for f in files]
|
||||
return max(*modified)
|
||||
modified = [os.path.getmtime(f) for f in files]
|
||||
return datetime.fromtimestamp(max(*modified), timezone.utc)
|
||||
|
22
owrx/controllers/receiverid.py
Normal file
22
owrx/controllers/receiverid.py
Normal file
@ -0,0 +1,22 @@
|
||||
from owrx.controllers import Controller
|
||||
from owrx.receiverid import ReceiverId
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ReceiverIdController(Controller):
|
||||
def __init__(self, handler, request, options):
|
||||
super().__init__(handler, request, options)
|
||||
self.authHeader = None
|
||||
|
||||
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None):
|
||||
if self.authHeader is not None:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
headers['Authorization'] = self.authHeader
|
||||
super().send_response(content, code=code, content_type=content_type, last_modified=last_modified, max_age=max_age, headers=headers)
|
||||
pass
|
||||
|
||||
def handle_request(self):
|
||||
if "Authorization" in self.request.headers:
|
||||
self.authHeader = ReceiverId.getResponseHeader(self.request.headers['Authorization'])
|
||||
super().handle_request()
|
@ -1,8 +1,7 @@
|
||||
from . import Controller
|
||||
from .receiverid import ReceiverIdController
|
||||
from owrx.version import openwebrx_version
|
||||
from owrx.sdr import SdrService
|
||||
from owrx.config import Config
|
||||
from owrx.receiverid import ReceiverId, KeyException
|
||||
import json
|
||||
|
||||
import logging
|
||||
@ -10,7 +9,7 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StatusController(Controller):
|
||||
class StatusController(ReceiverIdController):
|
||||
def getProfileStats(self, profile):
|
||||
return {
|
||||
"name": profile["name"],
|
||||
@ -29,12 +28,6 @@ class StatusController(Controller):
|
||||
|
||||
def indexAction(self):
|
||||
pm = Config.get()
|
||||
headers = None
|
||||
if "Authorization" in self.request.headers:
|
||||
try:
|
||||
headers = ReceiverId.getResponseHeader(self.request.headers["Authorization"])
|
||||
except KeyException:
|
||||
logger.exception("error processing authorization header")
|
||||
status = {
|
||||
"receiver": {
|
||||
"name": pm["receiver_name"],
|
||||
@ -47,4 +40,4 @@ class StatusController(Controller):
|
||||
"version": openwebrx_version,
|
||||
"sdrs": [self.getReceiverStats(r) for r in SdrService.getSources().values()]
|
||||
}
|
||||
self.send_response(json.dumps(status), content_type="application/json", headers=headers)
|
||||
self.send_response(json.dumps(status), content_type="application/json")
|
||||
|
@ -23,7 +23,7 @@ class WebpageController(TemplateController):
|
||||
settingslink = ""
|
||||
pm = Config.get()
|
||||
if "webadmin_enabled" in pm and pm["webadmin_enabled"]:
|
||||
settingslink = """<a class="button" href="settings" target="openwebrx-settings"><img src="static/gfx/openwebrx-panel-settings.png" alt="Settings"/><br/>Settings</a>"""
|
||||
settingslink = """<a class="button" href="settings" target="openwebrx-settings"><span class="sprite sprite-panel-settings"></span><br/>Settings</a>"""
|
||||
header = self.render_template("include/header.include.html", settingslink=settingslink)
|
||||
return {"header": header}
|
||||
|
||||
|
13
owrx/dsp.py
13
owrx/dsp.py
@ -3,7 +3,7 @@ from owrx.wsjt import WsjtParser
|
||||
from owrx.js8 import Js8Parser
|
||||
from owrx.aprs import AprsParser
|
||||
from owrx.pocsag import PocsagParser
|
||||
from owrx.source import SdrSource
|
||||
from owrx.source import SdrSource, SdrSourceEventClient
|
||||
from owrx.property import PropertyStack, PropertyLayer
|
||||
from owrx.modes import Modes
|
||||
from csdr import csdr
|
||||
@ -14,7 +14,7 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DspManager(csdr.output):
|
||||
class DspManager(csdr.output, SdrSourceEventClient):
|
||||
def __init__(self, handler, sdrSource):
|
||||
self.handler = handler
|
||||
self.sdrSource = sdrSource
|
||||
@ -30,6 +30,7 @@ class DspManager(csdr.output):
|
||||
# local demodulator properties not forwarded to the sdr
|
||||
self.props.addLayer(0, PropertyLayer().filter(
|
||||
"output_rate",
|
||||
"hd_output_rate",
|
||||
"squelch_level",
|
||||
"secondary_mod",
|
||||
"low_cut",
|
||||
@ -54,6 +55,7 @@ class DspManager(csdr.output):
|
||||
"center_freq",
|
||||
"start_mod",
|
||||
"start_freq",
|
||||
"wfm_deemphasis_tau",
|
||||
))
|
||||
|
||||
self.dsp = csdr.dsp(self)
|
||||
@ -70,6 +72,8 @@ class DspManager(csdr.output):
|
||||
self.dsp.set_bpf(*bpf)
|
||||
|
||||
def set_dial_freq(key, value):
|
||||
if self.props["center_freq"] is None or self.props["offset_freq"] is None:
|
||||
return
|
||||
freq = self.props["center_freq"] + self.props["offset_freq"]
|
||||
for parser in self.parsers.values():
|
||||
parser.setDialFrequency(freq)
|
||||
@ -94,6 +98,7 @@ class DspManager(csdr.output):
|
||||
self.props.wireProperty("digimodes_fft_size", self.dsp.set_secondary_fft_size),
|
||||
self.props.wireProperty("samp_rate", self.dsp.set_samp_rate),
|
||||
self.props.wireProperty("output_rate", self.dsp.set_output_rate),
|
||||
self.props.wireProperty("hd_output_rate", self.dsp.set_hd_output_rate),
|
||||
self.props.wireProperty("offset_freq", self.dsp.set_offset_freq),
|
||||
self.props.wireProperty("center_freq", self.dsp.set_center_freq),
|
||||
self.props.wireProperty("squelch_level", self.dsp.set_squelch_level),
|
||||
@ -103,6 +108,7 @@ class DspManager(csdr.output):
|
||||
self.props.wireProperty("digital_voice_unvoiced_quality", self.dsp.set_unvoiced_quality),
|
||||
self.props.wireProperty("dmr_filter", self.dsp.set_dmr_filter),
|
||||
self.props.wireProperty("temporary_directory", self.dsp.set_temporary_directory),
|
||||
self.props.wireProperty("wfm_deemphasis_tau", self.dsp.set_wfm_deemphasis_tau),
|
||||
self.props.filter("center_freq", "offset_freq").wire(set_dial_freq),
|
||||
]
|
||||
|
||||
@ -146,6 +152,7 @@ class DspManager(csdr.output):
|
||||
logger.debug("adding new output of type %s", t)
|
||||
writers = {
|
||||
"audio": self.handler.write_dsp_data,
|
||||
"hd_audio": self.handler.write_hd_audio,
|
||||
"smeter": self.handler.write_s_meter_level,
|
||||
"secondary_fft": self.handler.write_secondary_fft,
|
||||
"secondary_demod": self.handler.write_secondary_demod,
|
||||
@ -155,7 +162,7 @@ class DspManager(csdr.output):
|
||||
|
||||
write = writers[t]
|
||||
|
||||
threading.Thread(target=self.pump(read_fn, write)).start()
|
||||
threading.Thread(target=self.pump(read_fn, write), name="dsp_pump_{}".format(t)).start()
|
||||
|
||||
def stop(self):
|
||||
self.dsp.stop()
|
||||
|
124
owrx/feature.py
124
owrx/feature.py
@ -1,11 +1,13 @@
|
||||
import subprocess
|
||||
from functools import reduce
|
||||
from operator import and_, or_
|
||||
from operator import and_
|
||||
import re
|
||||
from distutils.version import LooseVersion
|
||||
import inspect
|
||||
from owrx.config import Config
|
||||
import shlex
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
@ -16,6 +18,35 @@ class UnknownFeatureException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FeatureCache(object):
|
||||
sharedInstance = None
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
if FeatureCache.sharedInstance is None:
|
||||
FeatureCache.sharedInstance = FeatureCache()
|
||||
return FeatureCache.sharedInstance
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {}
|
||||
self.cachetime = timedelta(hours=2)
|
||||
|
||||
def has(self, feature):
|
||||
if feature not in self.cache:
|
||||
return False
|
||||
now = datetime.now()
|
||||
if self.cache[feature]["valid_to"] < now:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get(self, feature):
|
||||
return self.cache[feature]["value"]
|
||||
|
||||
def set(self, feature, value):
|
||||
valid_to = datetime.now() + self.cachetime
|
||||
self.cache[feature] = {"value": value, "valid_to": valid_to}
|
||||
|
||||
|
||||
class FeatureDetector(object):
|
||||
features = {
|
||||
# core features; we won't start without these
|
||||
@ -23,6 +54,7 @@ class FeatureDetector(object):
|
||||
# different types of sdrs and their requirements
|
||||
"rtl_sdr": ["rtl_connector"],
|
||||
"rtl_sdr_soapy": ["soapy_connector", "soapy_rtl_sdr"],
|
||||
"rtl_tcp": ["rtl_tcp_connector"],
|
||||
"sdrplay": ["soapy_connector", "soapy_sdrplay"],
|
||||
"hackrf": ["soapy_connector", "soapy_hackrf"],
|
||||
"perseussdr": ["perseustest"],
|
||||
@ -35,13 +67,16 @@ class FeatureDetector(object):
|
||||
"uhd": ["soapy_connector", "soapy_uhd"],
|
||||
"red_pitaya": ["soapy_connector", "soapy_red_pitaya"],
|
||||
"radioberry": ["soapy_connector", "soapy_radioberry"],
|
||||
"fcdpp": ["soapy_connector", "soapy_fcdpp"],
|
||||
# optional features and their requirements
|
||||
"digital_voice_digiham": ["digiham", "sox"],
|
||||
"digital_voice_dsd": ["dsd", "sox", "digiham"],
|
||||
"digital_voice_freedv": ["freedv_rx", "sox"],
|
||||
"wsjt-x": ["wsjtx", "sox"],
|
||||
"packet": ["direwolf", "sox"],
|
||||
"pocsag": ["digiham", "sox"],
|
||||
"js8call": ["js8", "sox"],
|
||||
"drm": ["dream", "sox"],
|
||||
}
|
||||
|
||||
def feature_availability(self):
|
||||
@ -88,22 +123,36 @@ class FeatureDetector(object):
|
||||
return None
|
||||
|
||||
def has_requirement(self, requirement):
|
||||
cache = FeatureCache.getSharedInstance()
|
||||
if cache.has(requirement):
|
||||
return cache.get(requirement)
|
||||
|
||||
method = self._get_requirement_method(requirement)
|
||||
result = False
|
||||
if method is not None:
|
||||
return method()
|
||||
result = method()
|
||||
else:
|
||||
logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement))
|
||||
return False
|
||||
|
||||
cache.set(requirement, result)
|
||||
return result
|
||||
|
||||
def get_requirement_description(self, requirement):
|
||||
return inspect.getdoc(self._get_requirement_method(requirement))
|
||||
|
||||
def command_is_runnable(self, command):
|
||||
def command_is_runnable(self, command, expected_result=None):
|
||||
tmp_dir = Config.get()["temporary_directory"]
|
||||
cmd = shlex.split(command)
|
||||
env = os.environ.copy()
|
||||
# prevent X11 programs from opening windows if called from a GUI shell
|
||||
env.pop("DISPLAY", None)
|
||||
try:
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=tmp_dir)
|
||||
return process.wait() != 32512
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=tmp_dir, env=env)
|
||||
rc = process.wait()
|
||||
if expected_result is None:
|
||||
return rc != 32512
|
||||
else:
|
||||
return rc == expected_result
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
@ -112,7 +161,20 @@ class FeatureDetector(object):
|
||||
OpenWebRX uses the demodulator and pipeline tools provided by the csdr project. Please check out [the project
|
||||
page on github](https://github.com/jketterl/csdr) for further details and installation instructions.
|
||||
"""
|
||||
return self.command_is_runnable("csdr")
|
||||
required_version = LooseVersion("0.17.0")
|
||||
|
||||
csdr_version_regex = re.compile("^csdr version (.*)$")
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(["csdr", "version"], stderr=subprocess.PIPE)
|
||||
matches = csdr_version_regex.match(process.stderr.readline().decode())
|
||||
if matches is None:
|
||||
return False
|
||||
version = LooseVersion(matches.group(1))
|
||||
process.wait(1)
|
||||
return version >= required_version
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def has_nmux(self):
|
||||
"""
|
||||
@ -192,7 +254,7 @@ class FeatureDetector(object):
|
||||
)
|
||||
|
||||
def _check_connector(self, command):
|
||||
required_version = LooseVersion("0.2")
|
||||
required_version = LooseVersion("0.3")
|
||||
|
||||
owrx_connector_version_regex = re.compile("^owrx-connector version (.*)$")
|
||||
|
||||
@ -216,6 +278,15 @@ class FeatureDetector(object):
|
||||
"""
|
||||
return self._check_connector("rtl_connector")
|
||||
|
||||
def has_rtl_tcp_connector(self):
|
||||
"""
|
||||
The owrx_connector package offers direct interfacing between your hardware and openwebrx. It allows quicker
|
||||
frequency switching, uses less CPU and can even provide more stability in some cases.
|
||||
|
||||
You can get it [here](https://github.com/jketterl/owrx_connector).
|
||||
"""
|
||||
return self._check_connector("rtl_tcp_connector")
|
||||
|
||||
def has_soapy_connector(self):
|
||||
"""
|
||||
The owrx_connector package offers direct interfacing between your hardware and openwebrx. It allows quicker
|
||||
@ -330,6 +401,14 @@ class FeatureDetector(object):
|
||||
"""
|
||||
return self._has_soapy_driver("hackrf")
|
||||
|
||||
def has_soapy_fcdpp(self):
|
||||
"""
|
||||
The SoapyFCDPP module allows the use of the Funcube Dongle Pro+.
|
||||
|
||||
You can get it [here](https://github.com/pothosware/SoapyFCDPP).
|
||||
"""
|
||||
return self._has_soapy_driver("fcdpp")
|
||||
|
||||
def has_dsd(self):
|
||||
"""
|
||||
The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version
|
||||
@ -392,3 +471,32 @@ class FeatureDetector(object):
|
||||
You can find instructions and downloads [here](https://o28.sischa.net/fifisdr/trac/wiki/De%3Arockprog).
|
||||
"""
|
||||
return self.command_is_runnable("rockprog")
|
||||
|
||||
def has_freedv_rx(self):
|
||||
"""
|
||||
The "freedv\_rx" executable is required to demodulate FreeDV digital transmissions. It comes together with the
|
||||
codec2 library, but it's only a supplemental part and not installed by default or contained in its packages.
|
||||
To install it, you will need to compile codec2 from source and manually install freedv\_rx.
|
||||
|
||||
You can find the codec2 source code [here](https://github.com/drowe67/codec2).
|
||||
"""
|
||||
return self.command_is_runnable("freedv_rx")
|
||||
|
||||
def has_dream(self):
|
||||
"""
|
||||
In order to be able to decode DRM broadcasts, OpenWebRX needs the "dream" DRM decoder. You can get it
|
||||
[here](https://sourceforge.net/projects/drm/files/dream/).
|
||||
|
||||
Note: Please use version 2.1.1, the latest version (2.2 at the time of writing) has been reported to cause
|
||||
problems.
|
||||
|
||||
The version supplied by most distributions will not work properly on the command line, so compiling from source
|
||||
with a custom set of commands is recommended:
|
||||
|
||||
```
|
||||
qmake CONFIG+=console
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
"""
|
||||
return self.command_is_runnable("dream --help", 0)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from owrx.config import Config
|
||||
from csdr import csdr
|
||||
import threading
|
||||
from owrx.source import SdrSource
|
||||
from owrx.source import SdrSource, SdrSourceEventClient
|
||||
from owrx.property import PropertyStack
|
||||
|
||||
import logging
|
||||
@ -9,7 +9,7 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpectrumThread(csdr.output):
|
||||
class SpectrumThread(csdr.output, SdrSourceEventClient):
|
||||
def __init__(self, sdrSource):
|
||||
self.sdrSource = sdrSource
|
||||
super().__init__()
|
||||
|
@ -49,7 +49,7 @@ class Map(object):
|
||||
loops = 0
|
||||
time.sleep(60)
|
||||
|
||||
threading.Thread(target=removeLoop, daemon=True).start()
|
||||
threading.Thread(target=removeLoop, daemon=True, name="map_removeloop").start()
|
||||
super().__init__()
|
||||
|
||||
def broadcast(self, update):
|
||||
|
@ -1,4 +1,5 @@
|
||||
import threading
|
||||
from owrx.client import ClientRegistry
|
||||
|
||||
|
||||
class Metric(object):
|
||||
@ -38,6 +39,7 @@ class Metrics(object):
|
||||
|
||||
def __init__(self):
|
||||
self.metrics = {}
|
||||
self.addMetric("openwebrx.users", DirectMetric(ClientRegistry.getSharedInstance().clientCount))
|
||||
|
||||
def addMetric(self, name, metric):
|
||||
self.metrics[name] = metric
|
||||
|
@ -40,14 +40,17 @@ class DigitalMode(Mode):
|
||||
class Modes(object):
|
||||
mappings = [
|
||||
AnalogMode("nfm", "FM", bandpass=Bandpass(-4000, 4000)),
|
||||
AnalogMode("wfm", "WFM", bandpass=Bandpass(-50000, 50000)),
|
||||
AnalogMode("am", "AM", bandpass=Bandpass(-4000, 4000)),
|
||||
AnalogMode("lsb", "LSB", bandpass=Bandpass(-3000, -300)),
|
||||
AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)),
|
||||
AnalogMode("cw", "CW", bandpass=Bandpass(700, 900)),
|
||||
AnalogMode("dmr", "DMR", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False),
|
||||
AnalogMode("dstar", "DStar", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False),
|
||||
AnalogMode("dstar", "D-Star", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False),
|
||||
AnalogMode("nxdn", "NXDN", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False),
|
||||
AnalogMode("ysf", "YSF", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False),
|
||||
AnalogMode("freedv", "FreeDV", bandpass=Bandpass(300, 3000), requirements=["digital_voice_freedv"], squelch=False),
|
||||
AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False),
|
||||
DigitalMode("bpsk31", "BPSK31", underlying=["usb"]),
|
||||
DigitalMode("bpsk63", "BPSK63", underlying=["usb"]),
|
||||
DigitalMode("ft8", "FT8", underlying=["usb"], requirements=["wsjt-x"], service=True),
|
||||
|
@ -2,7 +2,7 @@ import re
|
||||
import logging
|
||||
import hashlib
|
||||
import hmac
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from owrx.config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -38,8 +38,19 @@ class KeyChallenge(object):
|
||||
|
||||
|
||||
class KeyResponse(object):
|
||||
def __init__(self, source, id, time, signature):
|
||||
self.source = source
|
||||
self.id = id
|
||||
self.time = time
|
||||
self.signature = signature
|
||||
|
||||
def __str__(self):
|
||||
return "TODO"
|
||||
return "{source}-{id}-{time}-{signature}".format(
|
||||
source=self.source,
|
||||
id=self.id,
|
||||
time=self.time,
|
||||
signature=self.signature,
|
||||
)
|
||||
|
||||
|
||||
class ReceiverId(object):
|
||||
@ -48,15 +59,16 @@ class ReceiverId(object):
|
||||
matches = headerRegex.match(requestHeader)
|
||||
if not matches:
|
||||
raise KeyException("invalid authorization header")
|
||||
challenge = KeyChallenge(matches.group(1))
|
||||
key = ReceiverId.findKey(challenge)
|
||||
if key is None:
|
||||
return {}
|
||||
time, signature = ReceiverId.signChallenge(challenge, key)
|
||||
return {
|
||||
"Signature": signature,
|
||||
"Time": time,
|
||||
}
|
||||
challenges = [KeyChallenge(i) for i in matches.group(1).split(",")]
|
||||
|
||||
def signChallenge(challenge):
|
||||
key = ReceiverId.findKey(challenge)
|
||||
if key is None:
|
||||
return
|
||||
return ReceiverId.signChallenge(challenge, key)
|
||||
|
||||
responses = [signChallenge(c) for c in challenges]
|
||||
return ",".join(str(r) for r in responses if r is not None)
|
||||
|
||||
@staticmethod
|
||||
def findKey(challenge):
|
||||
@ -74,8 +86,9 @@ class ReceiverId(object):
|
||||
|
||||
@staticmethod
|
||||
def signChallenge(challenge, key):
|
||||
now = datetime.utcnow().isoformat()
|
||||
now = datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc)
|
||||
now_bytes = int(now.timestamp()).to_bytes(4, byteorder="big")
|
||||
m = hmac.new(bytes.fromhex(key.secret), digestmod=hashlib.sha256)
|
||||
m.update(bytes.fromhex(challenge.challenge))
|
||||
m.update(now.encode('utf8'))
|
||||
return now, m.hexdigest()
|
||||
m.update(now_bytes)
|
||||
return KeyResponse(challenge.source, challenge.id, now_bytes.hex(), m.hexdigest())
|
||||
|
@ -1,5 +1,5 @@
|
||||
import threading
|
||||
from owrx.source import SdrSource
|
||||
from owrx.source import SdrSource, SdrSourceEventClient
|
||||
from owrx.sdr import SdrService
|
||||
from owrx.bands import Bandplan
|
||||
from csdr.csdr import dsp, output
|
||||
@ -32,7 +32,7 @@ class ServiceOutput(output, metaclass=ABCMeta):
|
||||
parser = self.getParser()
|
||||
parser.setDialFrequency(self.frequency)
|
||||
target = self.pump(read_fn, parser.parse)
|
||||
threading.Thread(target=target).start()
|
||||
threading.Thread(target=target, name="service_output_receive").start()
|
||||
|
||||
|
||||
class WsjtServiceOutput(ServiceOutput):
|
||||
@ -59,9 +59,9 @@ class Js8ServiceOutput(ServiceOutput):
|
||||
return t == "js8_demod"
|
||||
|
||||
|
||||
class ServiceHandler(object):
|
||||
class ServiceHandler(SdrSourceEventClient):
|
||||
def __init__(self, source):
|
||||
self.lock = threading.Lock()
|
||||
self.lock = threading.RLock()
|
||||
self.services = []
|
||||
self.source = source
|
||||
self.startupTimer = None
|
||||
@ -124,30 +124,28 @@ class ServiceHandler(object):
|
||||
self.startupTimer.start()
|
||||
|
||||
def updateServices(self):
|
||||
logger.debug("re-scheduling services due to sdr changes")
|
||||
self.stopServices()
|
||||
if not self.source.isAvailable():
|
||||
logger.debug("sdr source is unavailable")
|
||||
return
|
||||
cf = self.source.getProps()["center_freq"]
|
||||
sr = self.source.getProps()["samp_rate"]
|
||||
srh = sr / 2
|
||||
frequency_range = (cf - srh, cf + srh)
|
||||
|
||||
dials = [
|
||||
dial
|
||||
for dial in Bandplan.getSharedInstance().collectDialFrequencies(
|
||||
frequency_range
|
||||
)
|
||||
if self.isSupported(dial["mode"])
|
||||
]
|
||||
|
||||
if not dials:
|
||||
logger.debug("no services available")
|
||||
return
|
||||
|
||||
with self.lock:
|
||||
self.services = []
|
||||
logger.debug("re-scheduling services due to sdr changes")
|
||||
self.stopServices()
|
||||
if not self.source.isAvailable():
|
||||
logger.debug("sdr source is unavailable")
|
||||
return
|
||||
cf = self.source.getProps()["center_freq"]
|
||||
sr = self.source.getProps()["samp_rate"]
|
||||
srh = sr / 2
|
||||
frequency_range = (cf - srh, cf + srh)
|
||||
|
||||
dials = [
|
||||
dial
|
||||
for dial in Bandplan.getSharedInstance().collectDialFrequencies(
|
||||
frequency_range
|
||||
)
|
||||
if self.isSupported(dial["mode"])
|
||||
]
|
||||
|
||||
if not dials:
|
||||
logger.debug("no services available")
|
||||
return
|
||||
|
||||
groups = self.optimizeResampling(dials, sr)
|
||||
if groups is None:
|
||||
|
@ -1,5 +1,5 @@
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from owrx.source import SdrSource
|
||||
from owrx.source import SdrSource, SdrSourceEventClient
|
||||
from owrx.config import Config
|
||||
import threading
|
||||
import math
|
||||
@ -204,7 +204,7 @@ class DaylightSchedule(TimerangeSchedule):
|
||||
return entries
|
||||
|
||||
|
||||
class ServiceScheduler(object):
|
||||
class ServiceScheduler(SdrSourceEventClient):
|
||||
def __init__(self, source):
|
||||
self.source = source
|
||||
self.selectionTimer = None
|
||||
|
@ -16,6 +16,19 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SdrSourceEventClient(ABC):
|
||||
@abstractmethod
|
||||
def onStateChange(self, state):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def onBusyStateChange(self, state):
|
||||
pass
|
||||
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_INACTIVE
|
||||
|
||||
|
||||
class SdrSource(ABC):
|
||||
STATE_STOPPED = 0
|
||||
STATE_STARTING = 1
|
||||
@ -54,6 +67,7 @@ class SdrSource(ABC):
|
||||
self.clients = []
|
||||
self.spectrumClients = []
|
||||
self.spectrumThread = None
|
||||
self.spectrumLock = threading.Lock()
|
||||
self.process = None
|
||||
self.modificationLock = threading.Lock()
|
||||
self.failed = False
|
||||
@ -143,6 +157,9 @@ class SdrSource(ABC):
|
||||
if self.monitor:
|
||||
return
|
||||
|
||||
if self.isFailed():
|
||||
return
|
||||
|
||||
try:
|
||||
self.preStart()
|
||||
except Exception:
|
||||
@ -170,12 +187,17 @@ class SdrSource(ABC):
|
||||
rc = self.process.wait()
|
||||
logger.debug("shut down with RC={0}".format(rc))
|
||||
self.monitor = None
|
||||
if self.getState() == SdrSource.STATE_RUNNING:
|
||||
self.failed = True
|
||||
self.setState(SdrSource.STATE_FAILED)
|
||||
else:
|
||||
self.setState(SdrSource.STATE_STOPPED)
|
||||
|
||||
self.monitor = threading.Thread(target=wait_for_process_to_end)
|
||||
self.monitor = threading.Thread(target=wait_for_process_to_end, name="source_monitor")
|
||||
self.monitor.start()
|
||||
|
||||
retries = 1000
|
||||
while retries > 0:
|
||||
while retries > 0 and not self.isFailed():
|
||||
retries -= 1
|
||||
if self.monitor is None:
|
||||
break
|
||||
@ -231,53 +253,58 @@ class SdrSource(ABC):
|
||||
if self.monitor:
|
||||
self.monitor.join()
|
||||
|
||||
self.setState(SdrSource.STATE_STOPPED)
|
||||
|
||||
def hasClients(self, *args):
|
||||
clients = [c for c in self.clients if c.getClientClass() in args]
|
||||
return len(clients) > 0
|
||||
|
||||
def addClient(self, c):
|
||||
def addClient(self, c: SdrSourceEventClient):
|
||||
self.clients.append(c)
|
||||
c.onStateChange(self.getState())
|
||||
hasUsers = self.hasClients(SdrSource.CLIENT_USER)
|
||||
hasBackgroundTasks = self.hasClients(SdrSource.CLIENT_BACKGROUND)
|
||||
if hasUsers or hasBackgroundTasks:
|
||||
self.start()
|
||||
self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE)
|
||||
|
||||
def removeClient(self, c):
|
||||
def removeClient(self, c: SdrSourceEventClient):
|
||||
try:
|
||||
self.clients.remove(c)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
hasUsers = self.hasClients(SdrSource.CLIENT_USER)
|
||||
self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE)
|
||||
|
||||
# no need to check for users if we are always-on
|
||||
if self.isAlwaysOn():
|
||||
return
|
||||
|
||||
hasUsers = self.hasClients(SdrSource.CLIENT_USER)
|
||||
hasBackgroundTasks = self.hasClients(SdrSource.CLIENT_BACKGROUND)
|
||||
self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE)
|
||||
if not hasUsers and not hasBackgroundTasks:
|
||||
self.stop()
|
||||
|
||||
def addSpectrumClient(self, c):
|
||||
self.spectrumClients.append(c)
|
||||
if self.spectrumThread is None:
|
||||
# local import due to circular depencency
|
||||
from owrx.fft import SpectrumThread
|
||||
if c in self.spectrumClients:
|
||||
return
|
||||
|
||||
self.spectrumThread = SpectrumThread(self)
|
||||
self.spectrumThread.start()
|
||||
# local import due to circular depencency
|
||||
from owrx.fft import SpectrumThread
|
||||
|
||||
self.spectrumClients.append(c)
|
||||
with self.spectrumLock:
|
||||
if self.spectrumThread is None:
|
||||
self.spectrumThread = SpectrumThread(self)
|
||||
self.spectrumThread.start()
|
||||
|
||||
def removeSpectrumClient(self, c):
|
||||
try:
|
||||
self.spectrumClients.remove(c)
|
||||
except ValueError:
|
||||
pass
|
||||
if not self.spectrumClients and self.spectrumThread is not None:
|
||||
self.spectrumThread.stop()
|
||||
self.spectrumThread = None
|
||||
with self.spectrumLock:
|
||||
if not self.spectrumClients and self.spectrumThread is not None:
|
||||
self.spectrumThread.stop()
|
||||
self.spectrumThread = None
|
||||
|
||||
def writeSpectrumData(self, data):
|
||||
for c in self.spectrumClients:
|
||||
|
6
owrx/source/fcdpp.py
Normal file
6
owrx/source/fcdpp.py
Normal file
@ -0,0 +1,6 @@
|
||||
from owrx.source.soapy import SoapyConnectorSource
|
||||
|
||||
|
||||
class FcdppSource(SoapyConnectorSource):
|
||||
def getDriver(self):
|
||||
return "fcdpp"
|
16
owrx/source/rtl_tcp.py
Normal file
16
owrx/source/rtl_tcp.py
Normal file
@ -0,0 +1,16 @@
|
||||
from .connector import ConnectorSource
|
||||
from owrx.command import Flag, Option, Argument
|
||||
|
||||
|
||||
class RtlTcpSource(ConnectorSource):
|
||||
def getCommandMapper(self):
|
||||
return (
|
||||
super()
|
||||
.getCommandMapper()
|
||||
.setBase("rtl_tcp_connector")
|
||||
.setMappings({
|
||||
"bias_tee": Flag("-b"),
|
||||
"direct_sampling": Option("-e"),
|
||||
"remote": Argument(),
|
||||
})
|
||||
)
|
@ -1,5 +1,5 @@
|
||||
from distutils.version import StrictVersion
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
_versionstring = "0.19.1"
|
||||
strictversion = StrictVersion(_versionstring)
|
||||
openwebrx_version = "v{0}".format(strictversion)
|
||||
_versionstring = "0.20.0"
|
||||
looseversion = LooseVersion(_versionstring)
|
||||
openwebrx_version = "v{0}".format(looseversion)
|
||||
|
@ -16,7 +16,7 @@ OPCODE_PING = 0x09
|
||||
OPCODE_PONG = 0x0A
|
||||
|
||||
|
||||
class WebSocketException(Exception):
|
||||
class WebSocketException(IOError):
|
||||
pass
|
||||
|
||||
|
||||
@ -146,6 +146,9 @@ class WebSocketConnection(object):
|
||||
self.close()
|
||||
|
||||
def interrupt(self):
|
||||
if self.interruptPipeSend is None:
|
||||
logger.debug("interrupt with closed pipe")
|
||||
return
|
||||
self.interruptPipeSend.send(bytes(0x00))
|
||||
|
||||
def handle(self):
|
||||
@ -227,6 +230,18 @@ class WebSocketConnection(object):
|
||||
logger.exception("OSError while reading data; closing connection")
|
||||
self.open = False
|
||||
|
||||
self.interruptPipeSend.close()
|
||||
self.interruptPipeSend = None
|
||||
# drain messages left in the queue so that the queue can be successfully closed
|
||||
# this is necessary since python keeps the file descriptors open otherwise
|
||||
try:
|
||||
while True:
|
||||
self.interruptPipeRecv.recv()
|
||||
except EOFError:
|
||||
pass
|
||||
self.interruptPipeRecv.close()
|
||||
self.interruptPipeRecv = None
|
||||
|
||||
def close(self):
|
||||
self.open = False
|
||||
self.interrupt()
|
||||
|
@ -151,7 +151,7 @@ class Decoder(ABC):
|
||||
|
||||
|
||||
class Jt9Decoder(Decoder):
|
||||
locator_pattern = re.compile("[A-Z0-9]+\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$")
|
||||
locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$")
|
||||
|
||||
def parse(self, msg, dial_freq):
|
||||
# ft8 sample
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user