206 Commits

Author SHA1 Message Date
477b457be9 update the version 2021-01-26 16:53:22 +01:00
58b35ec0f9 update changelogs for 0.20.3 2021-01-26 16:28:56 +01:00
ae0748952f remove unused import, too 2021-01-25 19:40:06 +01:00
f81cf3570a don't check the type since older python doesn't have re.Pattern 2021-01-25 19:36:55 +01:00
b2e8fc5ad5 release version 0.20.2 2021-01-24 23:52:20 +01:00
b997e83095 update changelog 2021-01-24 23:51:01 +01:00
366f7247f2 code style 2021-01-24 22:54:58 +01:00
7e60efeae2 validate all parameters sent to dsp, refs #215 2021-01-24 22:29:23 +01:00
15940d0a2e extend StringValidator instead 2021-01-24 22:28:48 +01:00
d126c3acef allow regexes only on strings 2021-01-24 22:28:00 +01:00
a880b1f6f9 add regex validator 2021-01-24 22:03:53 +01:00
49577953c6 fix events 2021-01-24 21:58:15 +01:00
4b03ced1f7 add more validators 2021-01-24 21:58:02 +01:00
66dc4e5772 get validator by string 2021-01-24 21:25:26 +01:00
ad0a5c27db introduce PropertyValidator (wrapper) 2021-01-24 21:19:45 +01:00
40e531c0da start implementing a validation layer, refs #215 2021-01-24 20:53:51 +01:00
8b52988dcd add a test that makes sure that writing to a filtered property fails 2021-01-24 20:15:02 +01:00
862a251295 allow only limited parameters to be set on the dsp 2021-01-24 20:10:37 +01:00
8710a2a1d3 update version and changelog 2020-11-30 18:30:33 +01:00
b3fbf89f57 remove OSM fallback since it's broken 2020-11-30 18:10:34 +01:00
ad5e610cec update changelog for 0.20.0 2020-10-11 23:13:14 +02:00
a37aec3bdf reduce sample rate on 2m 2020-10-11 18:55:03 +02:00
1cec386c18 release version 0.20.0 2020-10-11 15:03:39 +02:00
ce39de14e8 display squelch on mouseover, too 2020-10-11 00:46:41 +02:00
3975073efd defer demodulator startup until center_freq is set 2020-10-11 00:25:13 +02:00
f31685e4e7 fix some exceptions due to None values 2020-10-11 00:15:09 +02:00
a856c27fe4 cache requirements, not features, for even better results 2020-10-10 23:00:05 +02:00
0435225a29 add feature detection cache to improve client load times 2020-10-10 22:08:35 +02:00
be757c7968 change default rtlsdr gain to 29 to avoid e4000 problems 2020-10-10 13:38:26 +02:00
9b977ac878 combine docker operations into docker.sh 2020-10-05 17:03:34 +02:00
37344c0cb8 don't pull result 2020-10-05 16:25:09 +02:00
ff25fa25dd fix missing pkg-config for plutosdr 2020-10-04 23:48:42 +02:00
ac0e44857c rtltcp docker image 2020-10-04 22:57:03 +02:00
9f17f1bc17 make wfm deemphasis tau configurable 2020-10-04 21:56:35 +02:00
1faa61ad50 allow wfm deemphasis tau to be set from the outside 2020-10-04 21:46:58 +02:00
815831b1ed update dependencies in docker 2020-10-04 21:02:46 +02:00
6c70e19c63 separate metric for direct aprs messages 2020-10-02 17:45:48 +02:00
4a8e9472ab create metric dynamically 2020-10-02 17:16:16 +02:00
5d4f3b8d90 update owrx_connector in docker 2020-09-26 01:46:25 +02:00
f37c7baefb update connectors 2020-09-21 18:31:49 +02:00
efca3520ab update connectors 2020-09-21 14:54:59 +02:00
cc385f851f remove unused import 2020-09-20 19:55:08 +02:00
349604ac50 fix some javascript errors 2020-09-20 19:53:13 +02:00
eaaa214dc9 add more details about dream; prevent X11 windows during feature
detection
2020-09-20 12:41:11 +02:00
e3e94ad14e update changelog with the respective type 2020-09-20 12:26:57 +02:00
c1347de1f0 optimize waterfall color generation 2020-09-19 21:53:29 +02:00
71a2352d2b let the client initiate the dsp again 2020-09-19 21:35:14 +02:00
34414de4e5 only re-start dsp if dsp has been started before 2020-09-19 21:17:00 +02:00
ff34e793a0 handle failure of sdr devices asynchronously 2020-09-19 20:45:23 +02:00
31295efbff restore linear interpolation to improve performance 2020-09-19 15:51:54 +02:00
a3285d5943 make the secondary fft run faster again 2020-09-17 22:57:40 +02:00
b9e19421c1 activate fft averaging on the secondary fft 2020-09-17 22:43:39 +02:00
6a6d4a3c9b secondary fft is now complex, better display for digimodes based on FM 2020-09-17 22:21:49 +02:00
82825fee41 fix sequence according to dependencies 2020-09-17 21:33:11 +02:00
2018dd444f start off with black & white to avoid javascript errors 2020-09-17 21:13:42 +02:00
35243fb62e anticipate problems with old color schemes; counter with new config
version
2020-09-17 20:59:16 +02:00
fa08f1e2cf use chroma.js to calculate waterfall colors 2020-09-17 20:10:01 +02:00
e10a52b39e handle full queue better by draining 2020-09-15 22:04:53 +02:00
c947204356 adopt the frontend regex (matches better), closes #170 2020-09-13 22:30:57 +02:00
994bf7439b update changelog 2020-09-13 20:22:26 +02:00
97f3642262 fix mouse wheel tuning for frequencies < 1MHz 2020-09-13 15:35:32 +02:00
0e8aece991 display current waterfall values on mouse over 2020-09-13 13:57:12 +02:00
39a473c8c2 disable waterfall sliders in auto mode 2020-09-13 13:38:44 +02:00
b9e6ffe03d first attempt at an automatically calibrating waterfall 2020-09-12 22:06:12 +02:00
9f9a5ceaa3 implement minimum waterfall range 2020-09-12 20:36:10 +02:00
36cf6097b3 fine-tune colors 2020-09-12 19:54:25 +02:00
45c0d05fec include turbo color map (by google ai) 2020-09-12 19:49:22 +02:00
3cd6af9ef9 add fonts in newer, better compressed formats 2020-09-12 19:01:51 +02:00
d12af6d203 strip non-essential parts from direwolf in docker 2020-09-12 00:21:46 +02:00
5f5cafe5ca optimizes uhd build further 2020-09-11 23:34:52 +02:00
d45cc207ad use sprites scaled to specific resolution (better performance, less
scaling headaches)
2020-09-11 22:12:01 +02:00
6e3a13e0d2 slim down uhd build; enable usb devices 2020-09-11 01:31:00 +02:00
0d6e9a5b9f missed file during sprite commits 2020-09-11 00:19:46 +02:00
7d509eeb48 explicitly fill buffer with 0s to avoid noise on newer chrome versions 2020-09-11 00:19:04 +02:00
87ba4ea524 fix audioworklet callbacks 2020-09-11 00:09:07 +02:00
42f975a926 use sprites for bookmark button, too 2020-09-11 00:02:45 +02:00
63c31eba22 use sprites to reduce the number of requests 2020-09-10 22:29:01 +02:00
626fa7681b improvise compiler flags for arm cpus 2020-09-10 21:07:49 +02:00
d412d482b2 add build instructions for new images 2020-09-10 20:55:12 +02:00
cf2f7377ab remove obsolete image 2020-09-10 20:54:16 +02:00
6c8cadace6 add docker builds for uhd and red pitaya 2020-09-10 18:25:18 +02:00
320f64a611 fix copy target 2020-09-06 23:35:00 +02:00
bfc3684d75 actually uncouple base and owrx layer 2020-09-06 23:33:45 +02:00
19a4a37144 update csdr with bugfix 2020-09-06 21:07:55 +02:00
f2d284989b add exceptional bandwidth for drm 2020-09-05 22:10:23 +02:00
3f01fc6d67 update changelog 2020-09-05 19:50:14 +02:00
d4396cc61a Merge branch 'develop' into drm 2020-09-05 01:10:50 +02:00
298da694ca compress background image with webp 2020-09-05 01:10:08 +02:00
a5bc7850a0 update csdr 2020-09-05 00:41:06 +02:00
f6e0cf2b71 patch dream to avoid hamlib and link with faad2 correctly 2020-09-05 00:40:36 +02:00
9a5286ca24 use complex fractional decimator 2020-09-04 22:02:23 +02:00
e10143b6db add dream to docker builds 2020-09-04 21:20:27 +02:00
6fe41f8e02 add compilation instructions 2020-09-04 20:27:12 +02:00
e8068a8795 fix dream audio output 2020-09-04 19:14:16 +02:00
e8ee94d13b fix detection 2020-09-04 18:11:36 +02:00
2411929455 implement DRM mode with dream 2020-09-04 18:09:02 +02:00
bec02795b8 implement gzip compression for assets 2020-09-04 15:44:25 +02:00
b5bc63e76b fix cache-control header 2020-09-04 14:46:27 +02:00
1aa487ff1a update owrx_connector in docker 2020-09-01 23:33:14 +02:00
f47ebb2adb docker optimization
* move openwebrx project tools to a separate layer for lower download
  volume and faster builds
* use COPY instead of ADD
* COPY multiple files at once to reduce number of layers
2020-09-01 23:30:48 +02:00
f90670f477 erase waterfall calibration memory 2020-08-31 21:48:02 +02:00
95ac5aeb7d detect device failure 2020-08-30 23:48:05 +02:00
9be0664e14 explicit typing of the source event interface 2020-08-30 23:47:04 +02:00
805039ec02 Merge branch 'develop' of github.com:jketterl/openwebrx into develop 2020-08-30 23:26:45 +02:00
322ebb1baa Merge pull request #171 from jwt27/jwt27/bpsk
s/psk/bpsk/g in bands.json
2020-08-30 23:20:51 +02:00
32105538c5 lock on the spectrum thread to avoid double start 2020-08-30 17:35:53 +02:00
820ca16cd9 update codec2 in docker 2020-08-30 14:43:05 +02:00
45e3c910da s/psk/bpsk/g in bands.json 2020-08-30 04:56:48 +02:00
d609acc6aa freedv agc fine-tuning 2020-08-29 21:32:21 +02:00
02b4822be8 update csdr in docker 2020-08-28 22:35:23 +02:00
c16a1b4726 fine-tune dsd agc; remove limiter (included in agc now) 2020-08-28 22:05:00 +02:00
d1cea95eb4 use 16bit agc for freedv and dsd modes, refs #126 2020-08-27 22:35:49 +02:00
53eefa7c80 fix last_decimation 2020-08-27 22:35:12 +02:00
b06732dbf5 fine-tuning of dsd audio agc 2020-08-27 19:41:46 +02:00
22feb8dd1c moderate agc for NFM 2020-08-27 19:28:20 +02:00
56f976e495 let's try without the minor version 2020-08-27 00:12:18 +02:00
f830c7efa6 update csdr dependency to 0.17.0 2020-08-27 00:08:50 +02:00
04d6515337 let's try this way 2020-08-26 23:17:40 +02:00
f78a68d53f update dependency versions 2020-08-26 23:13:50 +02:00
c8687f2f8d update wording on github 2020-08-26 21:08:50 +02:00
1884b89a6e update changelog 2020-08-26 21:07:50 +02:00
008787a938 update csdr in docker 2020-08-26 21:05:29 +02:00
f41814c6ca add csdr version requirement 2020-08-26 20:07:58 +02:00
055269504b use the new agc parameters 2020-08-26 19:45:21 +02:00
dea5b15656 new gain parameters for digital modes 2020-08-26 00:43:49 +02:00
6650438d2f slow agc parameters for AM 2020-08-25 21:28:18 +02:00
4204e4d9e2 Merge branch 'develop' into agc_work 2020-08-24 00:03:55 +02:00
9e41d49d46 refactor audio startup so it will autostart on firefox, if allowed 2020-08-23 17:56:13 +02:00
6aa25760c5 update the issues page wordings 2020-08-22 18:07:54 +02:00
1bff6d1289 update connectors to latest version 2020-08-20 11:50:56 +02:00
23c69fb5a3 add "remote" mapping 2020-08-16 23:22:46 +02:00
b158e0d17d add the ability to add literal command-line arguments 2020-08-16 23:19:37 +02:00
c9dd33ba57 add a new source for rtl_tcp and rtl_tcp_connector 2020-08-16 21:49:52 +02:00
bc000451cc update make call 2020-08-15 17:39:13 +02:00
47da9a9d70 use unix dir separators 2020-08-15 17:01:32 +02:00
66703cb5e1 include radioberry in full build 2020-08-15 16:53:10 +02:00
0066b4dbfd make script executable 2020-08-15 16:52:55 +02:00
18d8b81f70 add git revision 2020-08-15 16:46:16 +02:00
8d52bde6b0 Merge pull request #165 from pa3gsb/develop
radioberry added to docker setup
2020-08-15 16:43:28 +02:00
dd3bf121c1 fix start_freq not working on neighboring profiles 2020-08-15 16:05:50 +02:00
cfc3f926fe clone added 2020-08-15 14:24:48 +02:00
6f8c8a3b66 radioberry added to docker setup 2020-08-15 14:02:20 +02:00
1c2125f969 prevent direwolf from using hamlib, refs #164 2020-08-14 21:08:35 +02:00
0030c6d656 thread names to aid debugging 2020-08-14 20:22:25 +02:00
7e5ea6e065 improve read pipe opening 2020-08-14 20:20:07 +02:00
49383e757f extract pipes to separate file 2020-08-14 19:54:07 +02:00
0cd0a1085a uncouple reading pipes, too, and select makes the threads time out 2020-08-14 00:17:09 +02:00
5bc69b6fa4 use id of pipe to avoid file system collisions 2020-08-13 23:51:11 +02:00
ddb5fe51b3 open pipes in non-blocking loops, preventing thread leaks 2020-08-13 23:35:49 +02:00
56debcd08a provide a fallback for browsers not supporting css gaps 2020-08-13 19:39:56 +02:00
de34856d57 let's stick with flexbox, but use native wrapping 2020-08-12 22:14:02 +02:00
80c25f459c use the space, modes! 2020-08-12 21:41:06 +02:00
ccb322016e re-arrange demodulator buttons in a dynamic grid 2020-08-12 19:44:33 +02:00
08ba0c7b02 shut down multiprocessing queue explicitly using a poison pill 2020-08-11 22:14:36 +02:00
7f57e4f45c compensate oversampling with the prefilter of csdr 2020-08-08 22:51:03 +02:00
f0b3a50c23 increase maximum audio speed indication (uncompressed hd is about
700kbps)
2020-08-08 22:23:34 +02:00
e51dbac2c5 update changelog 2020-08-08 22:06:33 +02:00
f4c43ffab6 fine-tune 2020-08-08 22:04:28 +02:00
69a12650d2 permit increased bandwidth for WFM 2020-08-08 22:04:10 +02:00
8c5a7a087f compensate WFM frequency deviation, at least preliminary 2020-08-08 21:56:35 +02:00
5a938b8c0b simplify 2020-08-08 21:35:15 +02:00
448e266097 implement wfm demodulator chain 2020-08-08 21:29:25 +02:00
da3f59fb9b determine hd audio rate and send it to the server 2020-08-08 20:45:03 +02:00
ef2ec1e1c5 catch exception on closed inputs 2020-08-08 20:43:29 +02:00
031c937c0c actually build fcdpp image 2020-08-08 00:11:44 +02:00
c6ec21747b add log to issue template 2020-08-08 00:11:11 +02:00
b54be3384d add docker build for funcube 2020-08-07 23:28:36 +02:00
62ee2ca445 add documentation about freedv_rx 2020-08-07 22:58:24 +02:00
03b2f83981 add groups.io link 2020-08-07 19:43:04 +02:00
20f0a5cd6c Update issue templates 2020-08-07 19:23:35 +02:00
640f438c4c Merge pull request #161 from jketterl/openwebrx-bug-template
Update issue templates
2020-08-07 19:15:21 +02:00
b068fb5756 Update issue templates 2020-08-07 17:07:09 +02:00
645ace75c3 protect against erroneous reads 2020-08-06 20:06:04 +02:00
0518ff9358 provide information to the queue which entries are done 2020-08-05 20:04:41 +02:00
a65fd7916e drain connections, close wave files refs #146 2020-08-05 20:03:38 +02:00
a77108dd0c drain subprocess pipes to free up file descriptors, refs #146 2020-08-05 20:01:57 +02:00
7234ff4309 use normal queue since we're not even multiprocessing 2020-08-05 19:07:55 +02:00
7ea8c8f7c6 use better locking for the service startup/shutdown 2020-07-30 21:35:31 +02:00
c8e5b4f822 let's try this again with a more recent version... 2020-07-30 18:05:56 +02:00
780d51286a update changelog 2020-07-29 21:45:24 +02:00
2252547fc1 add freedv to docker container 2020-07-29 21:44:10 +02:00
7e5409160e initial work on freedv / codec2 support, refs #126 2020-07-28 00:28:20 +02:00
9b187140ff catch exception and replace with a debug message, refs #22 2020-07-27 21:18:24 +02:00
77ae13723d remove link to outdated instructions, closes #157 2020-07-27 20:27:25 +02:00
9efc839128 update to js8call 2.2.0 2020-07-22 18:45:13 +02:00
660301a43b update to wsjt-x 2.2.2 2020-07-21 22:51:12 +02:00
11fd918d62 handle more errors the right way, refs #144 2020-07-21 20:33:48 +02:00
de67d36cd6 update changelog 2020-07-21 20:03:33 +02:00
1f8b2f7909 always send busy state event, even when always-on, closes #147 2020-07-21 19:57:23 +02:00
d9bc03d1fc clear the multiprocessing queue to get rid of more file descriptors,
refs #146
2020-07-20 23:09:38 +02:00
369a61ec59 shut down pipes correctly, refs #146 2020-07-19 19:42:18 +02:00
c54f19282a improved error handling, refs #146 #22 2020-07-19 19:00:26 +02:00
174e9afa7b correctly close iqtee / iqtee2 pipes 2020-07-18 20:00:49 +02:00
e53f1f60eb multi-key signing implementation 2020-07-09 21:32:57 +02:00
7eb0a8cf7e add fcdpp support 2020-07-09 15:39:33 +02:00
0e6518915d * refactor receiverid into a separate controller base
* allow multiple headers to prepare for checking multiple claims
2020-07-04 21:47:56 +02:00
e0129fd0f7 move timezone to initialization instead of implicit localization 2020-07-01 19:10:46 +02:00
929cf5e230 makefiles use tabs... weird 2020-06-23 19:48:00 +02:00
d6512e0a86 prevent debian packaging from modifying png files 2020-06-23 19:39:26 +02:00
480b728c06 move metrics initialization to have initial metrics 2020-06-23 00:08:59 +02:00
9e323a08ff remove duplicate css declaration 2020-06-22 00:07:16 +02:00
75f4f0bfe0 fix timezones in all places 2020-06-21 22:35:40 +02:00
2eece08d27 correct timezone for last-modified header 2020-06-21 21:42:32 +02:00
b930bb432d add -dev flag to clearly distinguish development versions 2020-06-14 21:58:35 +02:00
83ff417f4d post-release cleanup 2020-06-13 19:25:15 +02:00
4f36df6324 some work on the agc (doesn't work for all stations) 2020-01-24 11:42:20 +01:00
117 changed files with 2447 additions and 733 deletions

29
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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
View 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

View 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, ...)

View File

@ -1,3 +1,27 @@
**0.20.3**
- Fix a compatibility issue with python versions <= 3.6
**0.20.2**
- Fix a security problem that allowed arbitrary commands to be executed on the receiver
([See github issue #215](https://github.com/jketterl/openwebrx/issues/215))
**0.20.1**
- Remove broken OSM map fallback
**0.20.0**
- 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

View File

@ -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,

View File

@ -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 .

View File

@ -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"]]

View File

@ -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 = 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
max_gain = 0.005
chain += [
"digitalvoice_filter -f",
"CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain),
"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
View File

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

38
debian/changelog vendored
View File

@ -1,3 +1,41 @@
openwebrx (0.20.3) buster focal; urgency=low
* Fix a compatibility issue with python versions <= 3.6
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 26 Jan 2021 15:28:00 +0000
openwebrx (0.20.2) buster focal; urgency=high
* Fix a security problem that allowed arbitrary commands to be executed on
the receiver (See github issue #215:
https://github.com/jketterl/openwebrx/issues/215)
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 24 Jan 2021 22:50:00 +0000
openwebrx (0.20.1) buster focal; urgency=low
* Remove broken OSM map fallback
-- Jakob Ketterl <jakob.ketterl@gmx.de> Mon, 30 Nov 2020 17:29:00 +0000
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
View File

@ -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
View File

@ -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
View 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

View File

@ -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

View File

@ -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"]

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

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

View File

@ -1,12 +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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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 &&\

View 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

View File

@ -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"

View 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 {

View File

@ -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"
)

View File

@ -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
)

View File

@ -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)

View File

@ -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

View File

@ -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

View 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/*

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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

View 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/*

View 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/*

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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/*

View File

@ -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

View 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/*

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -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>

View File

@ -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">

View File

@ -10,35 +10,56 @@ 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 (me.started) {
if (callback) callback(false);
// if failed to find a valid resampling factor...
if (me.resamplingFactor === 0) {
return;
}
me.audioContext.resume().then(function(){
me.allowed = me.audioContext.state === 'running';
if (!me.allowed) {
if (callback) callback(false);
// been started before?
if (me.started) {
return;
}
// are we allowed to play audio?
if (!me.isAllowed()) {
return;
}
me.started = true;
var runCallbacks = function(workletType) {
var callbacks = me.onStartCallbacks;
me.onStartCallbacks = false;
callbacks.forEach(function(c) { c(workletType); });
};
me.gainNode = me.audioContext.createGain();
me.gainNode.connect(me.audioContext.destination);
@ -65,7 +86,7 @@ AudioEngine.prototype.start = function(callback) {
}
});
me.audioNode.port.start();
if (callback) callback(true, 'AudioWorklet');
runCallbacks('AudioWorklet');
});
} else {
me.audioBuffers = [];
@ -120,15 +141,22 @@ AudioEngine.prototype.start = function(callback) {
me.audioNode = me.audioContext[method](bufferSize, 0, 1);
me.audioNode.onaudioprocess = audio_onprocess;
me.audioNode.connect(me.gainNode);
if (callback) callback(true, 'ScriptProcessorNode');
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;
};

View File

@ -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));
});

View File

@ -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>'

View File

@ -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;
}

View File

@ -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) {

View File

@ -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;

View File

@ -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) {

View File

@ -223,26 +223,14 @@
case "config":
var config = json.value;
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
var mapTypeId = config.google_maps_api_key ? 'roadmap' : 'OSM';
map = new google.maps.Map($('.openwebrx-map')[0], {
center: {
lat: config.receiver_gps.lat,
lng: config.receiver_gps.lon
},
zoom: 5,
mapTypeId: mapTypeId
});
map.mapTypes.set("OSM", new google.maps.ImageMapType({
getTileUrl: function(coord, zoom) {
return "https://maps.wikimedia.org/osm-intl/" + zoom + "/" + coord.x + "/" + coord.y + ".png";
},
tileSize: new google.maps.Size(256, 256),
name: "OpenStreetMap",
maxZoom: 18
}));
$.getScript("static/lib/nite-overlay.js").done(function(){
nite.init(map);
setInterval(function() { nite.refresh() }, 10000); // every 10s

View File

@ -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();
}

View File

@ -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

View File

@ -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

View File

@ -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):
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".format(band=band)
name = "aprs.decodes.{band}.aprs.{category}".format(band=band, category=category)
metrics = Metrics.getSharedInstance()
metric = metrics.getMetric(name)
if metric is None:
metric = CounterMetric()
metrics.addMetric(name, metric)
return metric
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")

View File

@ -184,8 +184,13 @@ class AudioWriter(object):
cwd=self.tmp_dir,
close_fds=True,
)
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 = 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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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):
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,20 +217,14 @@ 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
# exit condition: no change
if next == self.sdr:
if next == self.sdr and next is not None:
return
self.stopDsp()
@ -197,17 +233,19 @@ class OpenWebRxReceiverClient(OpenWebRxClient):
self.configSub.cancel()
self.configSub = None
if self.sdr is not None:
self.sdr.removeClient(self)
if next is None:
# exit condition: no sdrs available
logger.warning("no more SDR devices available")
self.handleNoSdrsAvailable()
return
self.sdr = next
self.sdr.addClient(self)
self.getDsp()
# 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()))
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})

View File

@ -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()

View File

@ -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,11 +73,7 @@ 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:
if not self.wasModified(file):
self.send_response("", code=304)
return
@ -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,11 +155,7 @@ 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:
if not self.wasModified(files):
self.send_response("", code=304)
return
@ -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)

View 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()

View File

@ -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")

View File

@ -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}

View File

@ -3,18 +3,29 @@ 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.property import PropertyStack, PropertyLayer
from owrx.source import SdrSource, SdrSourceEventClient
from owrx.property import PropertyStack, PropertyLayer, PropertyValidator
from owrx.property.validators import OrValidator, RegexValidator, BoolValidator
from owrx.modes import Modes
from csdr import csdr
import threading
import re
import logging
logger = logging.getLogger(__name__)
class DspManager(csdr.output):
class ModulationValidator(OrValidator):
"""
This validator only allows alphanumeric characters and numbers, but no spaces or special characters
"""
def __init__(self):
super().__init__(BoolValidator(), RegexValidator(re.compile("^[a-z0-9]+$")))
class DspManager(csdr.output, SdrSourceEventClient):
def __init__(self, handler, sdrSource):
self.handler = handler
self.sdrSource = sdrSource
@ -27,18 +38,25 @@ class DspManager(csdr.output):
}
self.props = PropertyStack()
# local demodulator properties not forwarded to the sdr
self.props.addLayer(0, PropertyLayer().filter(
"output_rate",
"squelch_level",
"secondary_mod",
"low_cut",
"high_cut",
"offset_freq",
"mod",
"secondary_offset_freq",
"dmr_filter",
))
# ensure strict validation since these can be set from the client
# and are used to build executable commands
validators = {
"output_rate": "int",
"hd_output_rate": "int",
"squelch_level": "num",
"secondary_mod": ModulationValidator(),
"low_cut": "num",
"high_cut": "num",
"offset_freq": "int",
"mod": ModulationValidator(),
"secondary_offset_freq": "int",
"dmr_filter": "int",
}
self.localProps = PropertyValidator(PropertyLayer().filter(*validators.keys()), validators)
self.props.addLayer(0, self.localProps)
# properties that we inherit from the sdr
self.props.addLayer(1, self.sdrSource.getProps().filter(
"audio_compression",
@ -54,6 +72,7 @@ class DspManager(csdr.output):
"center_freq",
"start_mod",
"start_freq",
"wfm_deemphasis_tau",
))
self.dsp = csdr.dsp(self)
@ -70,6 +89,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 +115,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 +125,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 +169,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 +179,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()
@ -170,7 +194,7 @@ class DspManager(csdr.output):
self.setProperty(k, v)
def setProperty(self, prop, value):
self.props[prop] = value
self.localProps[prop] = value
def getClientClass(self):
return SdrSource.CLIENT_USER

View File

@ -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)

View File

@ -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__()

View File

@ -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):

View File

@ -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

View File

@ -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),

View File

@ -1,4 +1,5 @@
from abc import ABC, abstractmethod
from owrx.property.validators import Validator
import logging
logger = logging.getLogger(__name__)
@ -148,6 +149,50 @@ class PropertyFilter(PropertyManager):
return [k for k in self.pm.keys() if k in self.props]
class PropertyValidationError(Exception):
def __init__(self, key, value):
super().__init__('Invalid value for property "{key}": "{value}"'.format(key=key, value=str(value)))
class PropertyValidator(PropertyManager):
def __init__(self, pm: PropertyManager, validators=None):
self.pm = pm
self.pm.wire(self._fireCallbacks)
if validators is None:
self.validators = {}
else:
self.validators = {k: Validator.of(v) for k, v in validators.items()}
super().__init__()
def validate(self, key, value):
if key not in self.validators:
return
if not self.validators[key].isValid(value):
raise PropertyValidationError(key, value)
def setValidator(self, key, validator):
self.validators[key] = Validator.of(validator)
def __getitem__(self, item):
return self.pm.__getitem__(item)
def __setitem__(self, key, value):
self.validate(key, value)
return self.pm.__setitem__(key, value)
def __contains__(self, item):
return self.pm.__contains__(item)
def __dict__(self):
return self.pm.__dict__()
def __delitem__(self, key):
return self.pm.__delitem__(key)
def keys(self):
return self.pm.keys()
class PropertyStack(PropertyManager):
def __init__(self):
super().__init__()

View File

@ -0,0 +1,97 @@
from abc import ABC, abstractmethod
from functools import reduce
from operator import or_
class ValidatorException(Exception):
pass
class Validator(ABC):
@staticmethod
def of(x):
if isinstance(x, Validator):
return x
if callable(x):
return LambdaValidator(x)
if x in validator_types:
return validator_types[x]()
raise ValidatorException("Cannot create validator")
@abstractmethod
def isValid(self, value):
pass
class LambdaValidator(Validator):
def __init__(self, c):
self.callable = c
def isValid(self, value):
return self.callable(value)
class TypeValidator(Validator):
def __init__(self, type):
self.type = type
super().__init__()
def isValid(self, value):
return isinstance(value, self.type)
class IntegerValidator(TypeValidator):
def __init__(self):
super().__init__(int)
class FloatValidator(TypeValidator):
def __init__(self):
super().__init__(float)
class StringValidator(TypeValidator):
def __init__(self):
super().__init__(str)
class BoolValidator(TypeValidator):
def __init__(self):
super().__init__(bool)
class OrValidator(Validator):
def __init__(self, *validators):
self.validators = validators
super().__init__()
def isValid(self, value):
return reduce(
or_,
[v.isValid(value) for v in self.validators],
False
)
class NumberValidator(OrValidator):
def __init__(self):
super().__init__(IntegerValidator(), FloatValidator())
class RegexValidator(StringValidator):
def __init__(self, regex):
self.regex = regex
super().__init__()
def isValid(self, value):
return super().isValid(value) and self.regex.match(value) is not None
validator_types = {
"string": StringValidator,
"str": StringValidator,
"integer": IntegerValidator,
"int": IntegerValidator,
"number": NumberValidator,
"num": NumberValidator,
}

View File

@ -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))
challenges = [KeyChallenge(i) for i in matches.group(1).split(",")]
def signChallenge(challenge):
key = ReceiverId.findKey(challenge)
if key is None:
return {}
time, signature = ReceiverId.signChallenge(challenge, key)
return {
"Signature": signature,
"Time": time,
}
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())

View File

@ -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,6 +124,7 @@ class ServiceHandler(object):
self.startupTimer.start()
def updateServices(self):
with self.lock:
logger.debug("re-scheduling services due to sdr changes")
self.stopServices()
if not self.source.isAvailable():
@ -146,9 +147,6 @@ class ServiceHandler(object):
logger.debug("no services available")
return
with self.lock:
self.services = []
groups = self.optimizeResampling(dials, sr)
if groups is None:
for dial in dials:

View File

@ -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

View File

@ -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,42 +253,46 @@ 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:
if c in self.spectrumClients:
return
# 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()
@ -275,6 +301,7 @@ class SdrSource(ABC):
self.spectrumClients.remove(c)
except ValueError:
pass
with self.spectrumLock:
if not self.spectrumClients and self.spectrumThread is not None:
self.spectrumThread.stop()
self.spectrumThread = None

6
owrx/source/fcdpp.py Normal file
View 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
View 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(),
})
)

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