389 Commits

Author SHA1 Message Date
bead51db69 fix the date 2020-06-13 18:51:01 +02:00
bf171bbfda add release targets 2020-06-13 18:48:18 +02:00
8ca068c98f update changelogs to reflect release 2020-06-13 18:47:17 +02:00
a696cc4ed8 next release version 2020-06-13 18:31:49 +02:00
0a2a28cb34 remove debugging 2020-06-13 18:26:27 +02:00
0f20f1fcdc update changelog 2020-06-13 18:21:22 +02:00
9a61f90fec parse hex string for hmac 2020-06-11 20:55:05 +02:00
5a88856825 fix array syntax 2020-06-11 00:01:47 +02:00
0e4f772c69 perform actual hmac signature 2020-06-11 00:00:16 +02:00
8278ece803 add receiver keys to configuration 2020-06-10 23:34:09 +02:00
eebe33f896 implement signature algorithm 2020-06-10 22:50:16 +02:00
61d03b38b9 receiver receiverid challenge and find corresponding key 2020-06-10 20:09:40 +02:00
c0f447ca20 fix rockprog integration 2020-06-07 22:53:31 +02:00
81465d69cc introduce next version to develop branch 2020-06-02 21:18:05 +02:00
1e84ced9a9 resture "under construction" notice 2020-06-02 21:12:25 +02:00
3479148b86 more tuning of the default configuration 2020-06-01 22:52:35 +02:00
017ad818ef fix up default configuration 2020-06-01 22:43:58 +02:00
09caae2fcc update changelog 2020-06-01 21:44:02 +02:00
ae295d72ae remove "under construction" notice 2020-06-01 19:41:38 +02:00
16c59c3245 release versions 2020-06-01 19:05:09 +02:00
ea65ef0100 update changelog 2020-06-01 18:34:54 +02:00
379e39aa3e Merge branch 'develop' into radioberry 2020-06-01 18:27:44 +02:00
835501a5f4 update changelog 2020-06-01 18:15:03 +02:00
c87cfed525 remove old status urls 2020-06-01 16:03:22 +02:00
ebd1e04414 remove sdr.hu parts 2020-06-01 15:58:15 +02:00
1019ed5793 fill gain input with values 2020-05-31 21:24:07 +02:00
adcac7b54a hackrf gain settings 2020-05-31 20:52:45 +02:00
d3a3078504 soapy gain input box for airspyhf 2020-05-31 20:43:12 +02:00
ac18a76c14 split stuff into separate files 2020-05-31 20:25:41 +02:00
66b5f17d38 implement soapy gain input 2020-05-31 19:57:20 +02:00
9763f302f3 switch to csdr and owrx_connector development versions 2020-05-31 15:04:09 +02:00
1359da5b14 limit SIMD flags to x86 only 2020-05-31 01:04:57 +02:00
063d22f88c build with lime SIMD limited to SSE3 for better portability, refs #38 2020-05-31 00:55:21 +02:00
7681830256 add soapy module for hackrf 2020-05-30 23:41:30 +02:00
3371697e18 add bias_tee mapping 2020-05-30 23:03:43 +02:00
bfe6c00f90 add debian changelog entry, too 2020-05-30 22:59:45 +02:00
e90973bcd4 switch hackrf to soapy 2020-05-30 22:58:31 +02:00
e0648d63ad reduce image size by excluding wsjt-x and js8call frontend binaries 2020-05-28 00:45:27 +02:00
564c1e26b6 let's try auto-apt-proxy to cut down build times 2020-05-25 20:38:42 +02:00
27d6802dfc include wsjt-x patches 2020-05-25 20:31:42 +02:00
d2a4f2bc46 patch wsjt-x to use packaged hamlib, too 2020-05-25 20:30:53 +02:00
d24abd436e install s6 overlay during normal dependency setup 2020-05-25 20:10:03 +02:00
305adc94fa install s6 overlay for the right platform 2020-05-24 21:45:08 +02:00
d9db693aec add changelog 2020-05-24 18:02:45 +02:00
d64f08490a use the old syntax 2020-05-24 18:00:14 +02:00
a982c86794 update sdrplay patches; fix sdrplay service 2020-05-24 17:28:48 +02:00
6c307d885f integrate s6 service layer for sdrplay 2020-05-24 16:00:36 +02:00
048210d7da update to latest versions from the homepage 2020-05-24 14:59:11 +02:00
d2be712de8 include sdrplay lib from sdrplay repo 2020-05-24 14:46:17 +02:00
3a8256e3bc update to the sdrplay repository version 2020-05-24 14:43:25 +02:00
385c241858 Merge branch 'develop' into sdrplay_v3 2020-05-24 14:05:36 +02:00
a1da591218 rtl_connector optimization 2020-05-24 13:50:28 +02:00
f1d9a4a28c switch to shift_addfast_cc for better performance 2020-05-24 03:04:20 +02:00
29b3f530d2 update again, latest fixes for aarch64 2020-05-24 02:44:55 +02:00
e1f83727b7 update csdr to latest 2020-05-24 00:42:47 +02:00
17f4f671a6 add a changelog entry about docker debian rebuild 2020-05-24 00:41:18 +02:00
4b8ef29775 add the fmv-optimized owrx_connector in docker, too, refs #38 2020-05-23 22:55:00 +02:00
5377087848 don't install unnecessary dependencies 2020-05-23 22:53:12 +02:00
1fedd0e50f limesdr requires libatomic 2020-05-23 22:52:22 +02:00
6cac3b4d39 restore startup 2020-05-23 22:51:46 +02:00
d9292587ec part 2: all the image builds 2020-05-23 19:59:31 +02:00
cf4f1dce32 rebuild docker containers with debian, stage 1: base 2020-05-23 18:06:46 +02:00
1299f5e9cc update csdr in docker to the latest version 2020-05-22 21:25:22 +02:00
48b177defa provision for a custom gain control 2020-05-17 21:21:37 +02:00
63475dda78 implement field sorting 2020-05-17 20:25:49 +02:00
9dd7a7e653 remove the remnants of the templating configuration 2020-05-17 18:51:36 +02:00
b624bef345 add broadcast bands 2020-05-17 18:45:01 +02:00
a03176223a add a bit more dynamic content 2020-05-17 18:44:26 +02:00
98cb1a8389 use the new version without FMV 2020-05-17 11:19:22 +02:00
ddbc844954 update csdr 2020-05-17 01:38:08 +02:00
d22ab23771 set package build flag to disable optimizations 2020-05-16 21:29:16 +02:00
0a60b505b8 update dependencies, refs #38 2020-05-16 19:03:48 +02:00
2b4799591f initialize logging early since there may be messages happening in
imports
2020-05-14 22:57:09 +02:00
048aab682f include changed wsjt keys in config migration 2020-05-14 22:56:49 +02:00
e557d46c0d apply darkly theme 2020-05-14 22:31:54 +02:00
10d6309608 add rockprog as a dependency 2020-05-14 21:40:28 +02:00
7d41fc8b06 pass temporary directory to services, too 2020-05-11 23:45:44 +02:00
2483398b0f clean up .wav files on exception, refs #107 2020-05-11 23:20:03 +02:00
a94209a2bc apply some alt tags to images 2020-05-11 20:31:21 +02:00
db7b4f195e fix for offset_freq when demodulator is exactly on center_freq 2020-05-11 15:04:24 +02:00
b0f7fd5d00 ability to add more config keys 2020-05-10 22:42:09 +02:00
96b1de1856 register different input types 2020-05-10 20:34:34 +02:00
9366d67218 dynamic sdr device settings 2020-05-10 20:18:42 +02:00
8df885b727 download receiver details via rest api 2020-05-10 17:27:46 +02:00
11cf2a96e2 create a receiver details route for use in the header 2020-05-10 17:12:42 +02:00
f62bd8be36 Merge branch 'develop' into radioberry 2020-05-10 17:03:58 +02:00
813474b5d6 make the header work on all pages 2020-05-10 17:03:30 +02:00
508ea2cf96 create a javascript profile for the map, too 2020-05-10 17:03:30 +02:00
a37e5ac93f header is now collapsed by default; simpler javascript 2020-05-10 17:03:30 +02:00
2c1ec7df74 make the header work on all pages 2020-05-10 16:23:05 +02:00
4971bee67c create a javascript profile for the map, too 2020-05-10 16:12:37 +02:00
eaa41c3256 header is now collapsed by default; simpler javascript 2020-05-10 16:07:14 +02:00
5606646064 implement basic support for radioberry 2020-05-10 00:03:14 +02:00
59a7842c6d fix map info window popping up after close 2020-05-09 01:18:51 +02:00
149ad8dcc6 move rx_photo code to header 2020-05-09 01:03:43 +02:00
3a5e227ab5 integrate feature report 2020-05-09 00:27:42 +02:00
3202f48f8e header details on map, too 2020-05-09 00:20:38 +02:00
3a455a0452 start collecting header routines 2020-05-09 00:11:20 +02:00
f2288ceb49 let's work with frame targets 2020-05-08 23:53:50 +02:00
dba4f91c77 include homepage 2020-05-08 23:49:02 +02:00
1f565355ec change available mode highlighting, refs #95 2020-05-08 23:34:34 +02:00
af1cfee754 allow switching underlying modulation (if available) refs #95 2020-05-08 22:56:02 +02:00
9563adacf7 more jquery magic for progressbars 2020-05-08 21:35:45 +02:00
fc7188145b use jquery to store progressbar objects 2020-05-08 21:18:03 +02:00
ceafcbf850 fix secondary demod being false 2020-05-06 23:00:57 +02:00
7fbd024ed5 fix sql=0 parameter 2020-05-06 22:52:48 +02:00
66a4f29911 let's try pre-loading the pipes to improve dsp initialization 2020-05-06 19:54:55 +02:00
eab3bf780e fix problems with sdr device failover detection 2020-05-04 20:36:17 +02:00
efa9771ad7 let's move some logic to the dialog 2020-05-04 00:20:01 +02:00
e2cacc1fa0 only available ones 2020-05-03 23:58:12 +02:00
93b8f75cc3 automatically load modes into bookmark dialog 2020-05-03 23:56:22 +02:00
a6a29b7032 actually, it's better to catch the exception inside 2020-05-03 21:50:40 +02:00
981d3b6673 ignore keyerrors in this case 2020-05-03 21:28:37 +02:00
8e313517d1 initialize frequeny correctly 2020-05-03 21:26:11 +02:00
beed0c1a70 improve squelch handling
squelch is now included in the URL hash
some modes now have the squelch visually disabled, refs #65
2020-05-03 19:55:48 +02:00
d98abe42bc fix configurable_keys exception 2020-05-03 17:50:37 +02:00
52367e53f5 remove the debugging 2020-05-03 17:46:32 +02:00
acb392e56c reset & stabilize modes 2020-05-03 13:10:54 +02:00
ac136313cb keep back changes until start command is given 2020-05-03 13:10:25 +02:00
e92a91663d restart demodulator based on modes 2020-05-03 12:48:25 +02:00
26ba8ca999 update bandpass for secondary modes 2020-05-03 12:23:23 +02:00
e409c37158 add remark about js8 binary location 2020-05-03 12:09:36 +02:00
2f2d52df85 re-wire digital voice meta panels 2020-05-03 12:09:18 +02:00
0868e643c9 return after sending 404 2020-05-02 16:59:27 +02:00
1bfe768601 hash handling fits better into here now 2020-05-02 15:17:09 +02:00
3405bc485b fix profile switching 2020-05-02 15:07:47 +02:00
6ff1b7d20a fix reconnection behavior 2020-05-02 14:51:00 +02:00
3504c8b54e update changelog 2020-05-02 14:40:01 +02:00
e01a12a945 just a comma 2020-05-02 13:57:19 +02:00
8c8445eb3b improve receiver load times by concating javascript 2020-05-02 13:35:42 +02:00
7a3043559f initialize mouseover display 2020-05-02 02:35:55 +02:00
54812f0de1 fix band changes 2020-05-02 02:32:49 +02:00
28c1425a8f fix digimode init from hash 2020-05-02 02:13:16 +02:00
a96690c8bd fft_fps isn't even used in the frontend 2020-05-02 01:36:12 +02:00
e5196c6af9 prevent starting demodulator if already started 2020-05-02 01:35:38 +02:00
19518da2e2 fix filter setup 2020-05-02 01:25:23 +02:00
b956a0dcd6 resolve todos 2020-05-02 01:16:36 +02:00
20023e3989 update bookmarks 2020-05-02 01:10:41 +02:00
d9a818525d refactor demodulator classes, part 2 2020-05-02 01:07:44 +02:00
b8f7686a6d refactor demodulator classes, part 1 2020-05-02 00:05:20 +02:00
5013af2117 combine methods 2020-04-30 23:31:52 +02:00
02a6326605 fix method names in comments 2020-04-30 23:20:56 +02:00
1441b9610c refactor into the classes, too 2020-04-30 23:16:49 +02:00
56f3f089a1 fix debugging; synchronize startup 2020-04-30 22:54:44 +02:00
1764abe65f update secondary parameters 2020-04-30 22:31:18 +02:00
33762574c3 improve demodulator initalization, part 2: refactor js classes 2020-04-30 22:07:19 +02:00
f1dc9af651 use synchronized setup; start dsp later 2020-04-27 22:49:24 +02:00
25a7bbd86a reset secondary demodulator, too 2020-04-26 23:14:34 +02:00
6a8168025d improve demodulator initialization 2020-04-26 22:46:30 +02:00
26321ab68b keep more parameters on the server side 2020-04-26 18:45:41 +02:00
449b3b3986 features no longer used on this level 2020-04-26 17:19:05 +02:00
39f9d4c273 streamline button generation 2020-04-26 17:18:48 +02:00
bb1b561c47 fully-automatic mode panel generation 2020-04-26 16:58:31 +02:00
907787cfdc implement first stages of active mode communication 2020-04-26 15:17:03 +02:00
e61d3a22a3 add if_mode mapping for sdrplay, refs #105 2020-04-26 13:49:03 +02:00
fb90a4e54b display sdr devices 2020-04-26 02:15:19 +02:00
5282b5f8df implement redirect on login 2020-04-26 01:54:48 +02:00
9942b3baf2 separate page for sdr settings 2020-04-25 21:55:52 +02:00
b874583931 setup for multiple settings sections 2020-04-25 21:42:00 +02:00
2f011ea249 add remark about web admin 2020-04-25 20:57:12 +02:00
a4ebf87263 check for key 2020-04-25 20:55:33 +02:00
dd492fa63c hide "settings" link if features is disabled 2020-04-25 20:52:41 +02:00
4dc10fb6a3 lose the logo 2020-04-25 20:36:08 +02:00
4a2b81c793 use autogain on airspyhf 2020-04-25 20:32:11 +02:00
e064352621 finally, remove debugging 2020-04-25 19:18:30 +02:00
b58357741a separate modes in here, too 2020-04-25 19:05:24 +02:00
2198c00d00 add js8 settings to web configuration 2020-04-25 17:33:30 +02:00
978eea400d clean up wsjt remainders in absctract code 2020-04-25 16:22:40 +02:00
a828f61c72 use right message delay for mode 2020-04-24 23:47:05 +02:00
4e67be8a3c dynamic profiles 2020-04-23 23:30:56 +02:00
623f21f769 fast and turbo modes 2020-04-23 22:27:03 +02:00
34838abfa9 profiles 2020-04-23 22:21:26 +02:00
280e39d9c4 js8 slow mode 2020-04-23 22:19:07 +02:00
2df56ad8b9 js8 slow mode (attempt?) 2020-04-23 00:34:49 +02:00
5ab2f02f63 multi-profile decoding 2020-04-23 00:21:59 +02:00
0120b33a25 refactor chopper out of wsjt 2020-04-22 23:53:19 +02:00
9622cd6a2a Merge branch 'develop' into js8call 2020-04-22 18:34:10 +02:00
78ccaa7d65 access regex groups in python 3.5 compatible way, closes #109 2020-04-22 18:28:45 +02:00
4f07c62cc9 use the latest available thread 2020-04-21 21:00:16 +02:00
520ddbb034 Merge branch 'develop' into js8call 2020-04-21 19:27:13 +02:00
0a16500133 get avatar path from pkg_resources, refs #108 2020-04-21 19:24:57 +02:00
681a583711 always begin a new message if the flag says so 2020-04-21 18:11:07 +02:00
aa4362fe9f add js8 to the changelog 2020-04-20 22:12:33 +02:00
0c12d07a26 finalize visual message representation 2020-04-20 22:07:21 +02:00
f474ab94d2 close threads when ending message has been received 2020-04-20 18:31:45 +02:00
5ba77012a7 update js8py library 2020-04-19 23:42:36 +02:00
a573fa0b93 Merge branch 'develop' into js8call 2020-04-19 23:38:51 +02:00
9a86bc23be make hackrf sleep for 1 second on restarts (device is not released
immediately)
2020-04-19 23:36:35 +02:00
c90b415c8b add scroll-to-bottom and cleanup intervals 2020-04-19 23:35:06 +02:00
4287387a5e threading frontend implementation 2020-04-19 22:10:32 +02:00
32bd1bb4aa install js8py 2020-04-18 00:41:16 +02:00
1023087c8a get locator from compound frame, too 2020-04-17 23:50:23 +02:00
5843aec342 fix js8call in the docker build 2020-04-16 22:01:51 +02:00
f52bf560ec add hamlib and js8 to docker (not working yet) 2020-04-15 23:20:17 +02:00
05a4139f94 add js8call dependency 2020-04-15 22:26:45 +02:00
116e20335e Merge branch 'develop' into js8call 2020-04-15 22:25:14 +02:00
5e6b45eaec Merge branch 'develop' of github.com:jketterl/openwebrx into develop 2020-04-15 21:40:38 +02:00
aa38340415 Merge pull request #104 from moepman/fix-hackrf-ppm
hackrf: properly use ppm setting as parameter
2020-04-15 21:40:29 +02:00
4d157d275a hackrf: properly use ppm setting as parameter 2020-04-15 21:22:06 +02:00
70818836de switch to recommended dependencies 2020-04-15 20:40:03 +02:00
1f70b93310 seems like we're in upper case 2020-04-14 23:16:45 +02:00
4c604bf400 Merge branch 'develop' into js8call 2020-04-14 22:37:22 +02:00
7fe694ba0a add urls to debian packaging 2020-04-14 22:36:59 +02:00
eb9059a711 switch to homepage url 2020-04-14 22:36:59 +02:00
da4917998d js8 metrics 2020-04-14 22:31:30 +02:00
99b4a25de7 js8 service 2020-04-14 21:27:50 +02:00
899445d586 display messages on the web 2020-04-14 21:12:25 +02:00
2de0cbc6c0 send messages to frontend, spots to pskreporter 2020-04-14 21:10:35 +02:00
7948d1f27a move dmr_filter property to the right list 2020-04-14 13:43:26 +02:00
bcb8a2315c use new library for js8 decoding 2020-04-13 16:35:31 +02:00
ddfd85c586 add js8 decoding if available 2020-04-12 13:10:23 +02:00
0e8715b5a1 the space has been introduced at some point, make it optional 2020-04-12 00:53:58 +02:00
1b2e237816 increment connector dependency 2020-04-12 00:32:01 +02:00
6d43126fa5 remove unused import 2020-04-10 20:05:06 +02:00
3c0146b1c4 add patches for armv7l and aarch64 2020-04-10 18:37:09 +02:00
893a56aa83 update install script patch 2020-04-10 18:03:19 +02:00
f7c9fbcc22 factory name has been changed in v3 2020-04-10 17:25:32 +02:00
aa29836039 remove debugging output 2020-04-10 17:21:53 +02:00
c30740c4e3 add uhd and redpitaya device modules; switch driver detection to
factories
2020-04-10 16:33:04 +02:00
d07cbb2b10 more abc 2020-04-05 21:48:05 +02:00
8fdf263e4b explicitly cast frequency 2020-04-05 21:47:40 +02:00
4d67b684e4 refactor 2020-04-05 19:08:58 +02:00
d06e9151b9 pass the frequency along with the job, refs #22 #61 2020-04-05 16:35:46 +02:00
366def0235 use abc 2020-04-05 15:22:23 +02:00
2301141b44 add missing keys() method 2020-04-02 18:21:45 +02:00
112eda2021 use the command mapper to generate event keys 2020-04-02 00:10:28 +02:00
d9e15357f3 update connector for docker 2020-04-01 23:50:00 +02:00
70ba0cd618 add direct_sampling mapping for rtl_sdr 2020-04-01 23:37:40 +02:00
78704885d7 drop another todo 2020-04-01 22:39:32 +02:00
513b477fac add user.json provisioning 2020-04-01 22:31:14 +02:00
6c3bb0b520 add first user storage implementation 2020-04-01 22:29:42 +02:00
c2e85ce9a6 web admin is disabled by default for now 2020-04-01 21:40:33 +02:00
3f742c7b1a webadmin feature flag 2020-04-01 21:39:53 +02:00
b7831b824a update dependencies 2020-03-29 22:48:10 +02:00
f0ef5bb371 add location picker so set receiver location 2020-03-29 21:40:29 +02:00
29566430a6 add location input fields 2020-03-29 20:49:37 +02:00
a3126b060d add forms to setup 2020-03-29 20:15:13 +02:00
2ef80eee1d refactor and format 2020-03-29 20:14:34 +02:00
65a0320cea refactor 2020-03-29 19:52:56 +02:00
199dfe106a add a new multi-checkbox to select background detection services 2020-03-29 19:50:37 +02:00
056a8a3289 migrate waterfall settings away from tuples 2020-03-29 18:49:13 +02:00
1d5f450f74 config file brush-up 2020-03-29 18:35:48 +02:00
7914202df3 move over to fork 2020-03-29 18:33:14 +02:00
a6b5984dce migrate to version 2 2020-03-29 18:28:18 +02:00
fd9e913a49 config migration for receiver_gps 2020-03-29 18:08:26 +02:00
2b7d6738f1 switch to json to avoid external dependency 2020-03-29 17:14:37 +02:00
f81e53e455 fix typo 2020-03-28 00:40:36 +01:00
3011e62fad add first steps towards a storage implementation 2020-03-27 23:44:03 +01:00
54dc412c4a add number types 2020-03-27 22:00:10 +01:00
0e9bb45d89 add more fields 2020-03-27 21:11:33 +01:00
6493fb86c1 add sdr.hu settings 2020-03-27 01:14:38 +01:00
df21a1eed6 send initial settings 2020-03-27 00:35:05 +01:00
c5a5d25320 update config settings directly in the frontend 2020-03-26 23:34:25 +01:00
7efe254a66 apply new values to config 2020-03-26 23:04:02 +01:00
d71dc35239 fill the form with data 2020-03-26 22:08:24 +01:00
ab9df41a21 render inputs in code, not in html 2020-03-26 21:52:34 +01:00
16639c0b5b add autofocus 2020-03-26 20:19:05 +01:00
2d86483907 no more debugging for the map 2020-03-26 20:13:36 +01:00
24a4d03eff note about agc in changelog 2020-03-26 17:28:37 +01:00
0d93186066 drop the passed modulation if the frequency is invalid 2020-03-26 15:36:49 +01:00
69b43b40b5 update changelog 2020-03-26 13:38:14 +01:00
16d5db00af send bias tee configs for rtl_sdr 2020-03-26 13:33:32 +01:00
b87f7017d1 remove unused detector 2020-03-26 13:14:25 +01:00
8a053f47d4 update changelog 2020-03-26 12:53:58 +01:00
895d8019e3 switch to raw mode to avoid arecord file size limit 2020-03-26 09:45:41 +01:00
25755d09dd improve waterfall auto-adjust for SDRs with oversampling 2020-03-25 21:50:22 +01:00
a7345bb16f propagate measurement reset to reporters, closes #88 2020-03-25 20:49:34 +01:00
0bffc2b3dd this doesn't do anything useful any more 2020-03-25 20:35:42 +01:00
14382e012f don't send event when value doesn't change 2020-03-25 17:59:00 +01:00
0e19a40968 fix import 2020-03-25 15:48:27 +01:00
4aac5c9584 use the interface 2020-03-25 15:47:15 +01:00
8a2356580a rename 2020-03-24 22:52:17 +01:00
4e4266f1c4 fix wording 2020-03-24 22:50:43 +01:00
cfea251d60 clean up 2020-03-24 22:50:18 +01:00
d1ef1810bf update changelog 2020-03-24 22:35:44 +01:00
25b287344f rename collect -> filter 2020-03-24 22:16:11 +01:00
f30cf3fecd fix up properties in the application 2020-03-24 22:13:42 +01:00
236f3d2058 more layer replacement 2020-03-24 22:11:54 +01:00
14634af83c add layer add / remove events + tests 2020-03-24 20:36:26 +01:00
4b7ac0e299 remove unused specials 2020-03-24 00:29:59 +01:00
cc5c130f49 fix secondary demod; add same-value handling 2020-03-24 00:18:10 +01:00
d5c2f8414e add stack event handling 2020-03-24 00:08:48 +01:00
c83d8580ba rewrite property engine
Property class is gone; logic is now done with Layers, Stack and Filter
2020-03-23 23:56:05 +01:00
7562dc8ecb use dictionary api 2020-03-23 22:09:41 +01:00
37e74f9027 use dictionary api 2020-03-23 22:09:26 +01:00
7cae383127 include defaults 2020-03-23 22:09:05 +01:00
b25e61ae9a rename 2020-03-22 21:59:22 +01:00
885d02ceca start implementing property layering 2020-03-22 21:51:49 +01:00
b3a5a36d9c more tests 2020-03-22 19:42:59 +01:00
5076f79aaa add owrx.property to the setup 2020-03-22 15:39:59 +01:00
9768fa7c50 add docker build for perseus 2020-03-22 15:32:39 +01:00
92cd65b66f remove installation leftovers 2020-03-22 11:56:43 +01:00
541c38151f split config and property code, first test 2020-03-21 22:40:39 +01:00
7948b7bfa1 move openwebrx installation to the end to profit from docker build cache 2020-03-21 21:44:09 +01:00
05485ba8e3 add perseus basic build (not enabled for now) 2020-03-21 18:58:03 +01:00
2505e95d1c reduce layers 2020-03-21 15:54:17 +01:00
135e9ae7b9 compile the connector into soapy for faster builds 2020-03-21 15:49:22 +01:00
8ed6dbe5d1 update changelog 2020-03-21 15:45:50 +01:00
752cd42ad7 Merge pull request #86 from amontefusco/iw0hdv
Perseus HF Receiver integration
2020-03-21 15:40:54 +01:00
fbf74a1286 add bitpack flag for airspy 2020-03-21 15:18:45 +01:00
55e1a97d43 update changelogs 2020-03-21 15:13:11 +01:00
8a03951713 re-align main buttons 2020-03-16 22:48:42 +01:00
1a1ad670ee fix background 2020-03-16 22:32:07 +01:00
5273131b25 apply new image background 2020-03-16 22:16:56 +01:00
d74b79f585 references to Perseus HF receiver removed from main config file 2020-03-16 18:05:49 +01:00
e1af089658 Merge branch 'develop' into iw0hdv 2020-03-16 17:39:53 +01:00
34ee5d8e3b More info on Perseus integration. 2020-03-16 00:21:49 +01:00
68e8a77b1d more refinements as per Jakob Ketterl suggestions 2020-03-16 00:13:51 +01:00
edded220b5 add the mailing list links 2020-03-15 23:39:38 +01:00
1581c659af add version to startup messages 2020-03-15 23:34:44 +01:00
ca5889f925 introduce config checking infrastructure 2020-03-15 23:32:19 +01:00
6e6861479d fix bugs with negative lat / long; update formatting
ref: #81
2020-03-15 18:46:37 +01:00
8e87aa0342 Merge branch 'develop' into iw0hdv 2020-03-15 17:25:56 +01:00
97cb51d990 Perseus SDR HF receiver first support 2020-03-15 17:24:36 +01:00
d2ce27eeab convert boolean values into something that soapy understands 2020-03-14 23:07:23 +01:00
00a7b7877c update parameter to match latest dev code 2020-03-14 23:06:52 +01:00
c387fe0fe9 add fictional bias_tee mapping for rtl_sdr_soapy
needs to be implemented in SoapyRTLSDR first
2020-03-14 01:56:17 +01:00
fea2cd1cc5 add new settings mappings for rf_notch and dab_notch 2020-03-14 01:21:43 +01:00
7742d7a048 don't include None values 2020-03-14 01:21:30 +01:00
e37e2f4540 add biastee setting for sdrplay, too 2020-03-14 01:15:25 +01:00
4deb4c781e use new mechanism for airspy bias-tee, too 2020-03-14 01:13:23 +01:00
5da2047935 introduce a generic mapping from sdr properties to soapy settings 2020-03-14 01:04:52 +01:00
fb82daf936 add to changelog 2020-03-13 23:55:45 +01:00
ede40e4a68 always add OSM source, switch when no google maps key is present. 2020-03-13 23:53:14 +01:00
3852f28fd4 Merge pull request #72 from jquagga/develop
Add fallback to use OpenStreetMap/Wikipedia Maps
2020-03-13 23:52:47 +01:00
c385fd635b Add fallback to use OpenStreetMap/Wikipedia Maps 2020-03-11 10:27:46 -04:00
b9ac887eed add the first form elements 2020-03-08 23:23:36 +01:00
a2dc2b3085 align the icons 2020-03-08 22:33:41 +01:00
6ab77f958c add settings button, start with the admin template 2020-03-08 21:28:15 +01:00
4928f80929 let's try to close that bracket 2020-03-07 21:23:08 +01:00
687e504af4 replace links 2020-03-07 20:53:17 +01:00
14b293e0cb add note about groups.io 2020-03-05 22:03:19 +01:00
beb59da6a6 remove incomplete setup instructions and link to the wiki 2020-03-05 20:53:47 +01:00
c2702e02a9 use the recommended way to generate google maps urls 2020-03-05 17:52:40 +01:00
6b4509fca5 update owrx_connector 2020-03-02 21:55:21 +01:00
8abfe059b7 now the sequence doesn't matter any more 2020-02-28 16:55:50 +01:00
10523dbbd7 use threading to uncouble the queues 2020-02-28 16:13:53 +01:00
b8c71109b8 initialize dmr filter, too 2020-02-28 00:30:41 +01:00
9cc850e578 introduce new pipe classes to improve sequencing 2020-02-28 00:20:37 +01:00
0e47f2d92a update changelog 2020-02-27 23:23:22 +01:00
fbcfb550a2 pass direct sampling mode changes to owrx_connector 2020-02-27 23:21:00 +01:00
a388acdf03 update changelog 2020-02-27 22:54:42 +01:00
d36be799d0 improve lock handling 2020-02-27 19:48:22 +01:00
c325368be8 improve variable handling 2020-02-27 18:50:53 +01:00
388218f9df implement a reconnection loop for direwolf, ref #60 2020-02-27 18:43:58 +01:00
6b2656efae fix constructor 2020-02-27 18:43:44 +01:00
278fab268f use dicts for the pipes 2020-02-25 20:55:42 +01:00
bd8b8ca410 session cookie handling 2020-02-23 21:52:13 +01:00
fb7422e5a8 generate session cookie 2020-02-23 21:39:12 +01:00
a70c51193b parse login data 2020-02-23 20:52:32 +01:00
fa75cac7f5 post login data 2020-02-23 20:25:36 +01:00
de3694248a restore audioworklets 2020-02-23 20:14:07 +01:00
9f06149ae3 add shadow 2020-02-23 20:13:36 +01:00
437e28c3a9 add templating 2020-02-23 20:13:11 +01:00
cad6175db0 login form 2020-02-23 20:04:19 +01:00
af053b9ac4 no more abstract methods 2020-02-23 19:29:17 +01:00
0a20cb5e41 prepare route protection 2020-02-23 19:23:18 +01:00
aa9737498a add controller options to allow multiple routes per controller 2020-02-23 18:32:37 +01:00
42191f4e77 rewrite routing logic 2020-02-23 17:53:02 +01:00
451eb99f8a split the controllers into separate files 2020-02-23 17:22:13 +01:00
b110705f45 fix this in develop, at least 2020-02-21 22:37:10 +01:00
36e94d4e3c fix typo 2020-02-20 22:30:56 +01:00
4e98bbc1c9 continue development as version 0.19 2020-02-20 22:01:21 +01:00
c3b13b224c add back under construction panel 2020-02-20 21:58:08 +01:00
5f388fd38d add dependency to soapysdr-tool to make SoapySDRUtil available 2020-02-19 20:06:27 +01:00
9bc161c140 split the manifest step into a separate skript 2020-02-18 22:47:51 +01:00
dbb7c0cde3 remove the "under construction" banner 2020-02-18 22:26:44 +01:00
52e517dfc3 make tags overridable from the outside 2020-02-18 21:52:52 +01:00
37ffb2a02c break lines at 80 chars 2020-02-18 21:19:00 +01:00
91b3713dad fix date 2020-02-18 21:09:22 +01:00
c53ac1aa4f pin the dependency release commits 2020-02-18 20:58:01 +01:00
c4166997be release version 0.18 2020-02-18 20:55:24 +01:00
f0f9455c6e add the changelog to the debian package 2020-02-18 20:53:53 +01:00
7bc78425cd add user to plugdev group, fix some lintian issues 2020-02-17 17:05:31 +01:00
d1dc14d9e5 don't put debian files in docker builds 2020-02-17 15:03:39 +01:00
521755b9f2 create and use custom user on debian install 2020-02-17 15:03:20 +01:00
ad565c5a2b re-wire the audio output to "null" - thanks to @dl9rdz 2020-02-17 12:06:13 +01:00
ebba6e1ada use more cpu cores 2020-02-16 12:19:49 +01:00
0b7b5d985f update copyright date 2020-02-16 11:49:20 +01:00
b948e06a4f use urllib to update sdr.hu, no wget dependency
ref: #52
2020-02-15 00:16:04 +01:00
eaa98b0d64 new status controller as json 2020-02-09 21:46:03 +01:00
16b3c11678 add soapy remote to docker build, too 2020-02-09 15:23:17 +01:00
c92929a32d add soapyremote source 2020-02-09 13:59:37 +01:00
46c3e5077d fix typo 2020-02-08 21:43:47 +01:00
dc12c54ae6 fix libiio installation 2020-02-08 21:05:12 +01:00
bdc43455a5 add dependencies 2020-02-08 19:53:23 +01:00
42eeb00a0f add limesdr build 2020-02-08 19:47:16 +01:00
5951d2a874 add docker build for pluto 2020-02-08 19:01:50 +01:00
9a5aba7313 disable config interface unless explicitly enables in the config 2020-02-08 18:29:48 +01:00
147 changed files with 6367 additions and 2640 deletions

View File

@ -4,3 +4,4 @@
**/*.pyc **/*.pyc
**/*.swp **/*.swp
black-env black-env
debian

View File

@ -1,3 +1,38 @@
**0.19.1**
- Added ability to authenticate receivers with listing sites using "receiver id" tokens
**0.19.0**
- Fix direwolf connection setup by implementing a retry loop
- Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector
- OSM maps instead of Google when google_maps_api_key is not set (thanks @jquagga)
- Improved logic to pass parameters to soapy devices.
- `rtl_sdr_soapy`: added support for `bias_tee`
- `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch`
- `airspy`: added support for `bitpack`
- Added support for Perseus-SDR devices, (thanks @amontefusco)
- Property System has been rewritten so that defaults on sdr behave as expected
- Waterfall range auto-adjustment now only takes the center 80% of the spectrum into account, which should work better
with SDRs that oversample or have rather flat filter curves towards the spectrum edges
- Bugfix for negative network usage
- FiFi SDR: prevent arecord from shutting down after 2GB of data has been sent
- Added support for bias tee control on rtl_sdr devices
- All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
- `rtl_sdr` type now also supports the `direct_sampling` option
- Added decoding implementation for for digimode "JS8Call"
(requires an installation of [js8call](http://js8call.com/) and
[the js8py library](https://github.com/jketterl/js8py))
- Reorganization of the frontend demodulator code
- Improve receiver load time by concatenating javascript assets
- Docker images migrated to Debian slim images; This was necessary to allow the use of function multiversioning in
csdr and owrx_connector to allow the images to run on a wider range of CPUs
- Docker containers have been updated to include the SDRplay driver version 3
- HackRF support is now based on SoapyHackRF
- Removed sdr.hu server listing support since the site has been shut down
- Added support for Radioberry 2 Rasbperry Pi SDR Cape
**0.18.0**
- Support for SoapyRemote
**2020-02-08** **2020-02-08**
- Compression, resampling and filtering in the frontend have been rewritten in javascript, sdr.js has been removed - Compression, resampling and filtering in the frontend have been rewritten in javascript, sdr.js has been removed
- Decoding of Pocsag modulation is now possible - Decoding of Pocsag modulation is now possible

View File

@ -3,7 +3,7 @@ OpenWebRX
OpenWebRX is a multi-user SDR receiver software with a web interface. OpenWebRX is a multi-user SDR receiver software with a web interface.
![OpenWebRX](http://blog.sdr.hu/images/openwebrx/screenshot.png) ![OpenWebRX](https://www.openwebrx.de/gfx/openwebrx-screenshot.png)
It has the following features: It has the following features:
@ -19,58 +19,21 @@ It has the following features:
## Setup ## Setup
### Raspberry Pi SD Card Images The following methods of setting up a receiver are currently available:
Probably the quickest way to get started is to download the latest Raspberry Pi SD Card Image. It contains all the - Raspberry Pi SD card images
depencencies out of the box, and should work on all Raspberry Pis. It is based off the Raspbian Lite distribution, - Debian repository
so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply. - Docker images
- Manual installation
You can find the latest images [here](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/index.html). You can Please checkout the [setup guide on the wiki](https://github.com/jketterl/openwebrx/wiki/Setup-Guide) for more details
also checkout the `nightly` folder, which has the most recent builds, albeit untested. on the respective methods.
Once you have booted a Raspberry with the SD Card, it will appear in your network with the hostname "openwebrx", which ## Community
should make it available as https://openwebrx/ on most networks. This may vary depending on your specific setup.
For Digital voice, the minimum requirement right now seems to be a Rasbperry Pi 3B+. I would like to work on optimizing If you have trouble setting up or configuring your receiver, you have some great idea you want to see implemented, or
this for lower specs, but at this point I am not sure how much can be done. you just generally want to have some OpenWebRX-related chat, come visit us over on
[our groups.io group](https://groups.io/g/openwebrx).
### Docker Images
For those familiar with docker, I am providing
[recent builds and Releases for both x86 and arm processors on the Docker hub](https://hub.docker.com/r/jketterl/openwebrx).
You can find a short introduction there.
### Manual Installation
OpenWebRX currently requires Linux and python >= 3.6 to run.
First you will need to install the dependencies:
- [csdr](https://github.com/jketterl/csdr)
- [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr)
- [owrx_connector](https://github.com/jketterl/owrx_connector)
Optional dependencies if you want to be able to listen do digital voice:
- [digiham](https://github.com/jketterl/digiham)
- [dsd](https://github.com/f4exb/dsdcc)
Optional dependency if you want to decode WSJT-X modes:
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html)
[Detailed installation instructions in the Wiki](https://github.com/jketterl/openwebrx/wiki/Manual-Package-installation-(including-digital-voice))
After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server:
./openwebrx.py
You can now open the GUI at <a href="http://localhost:8073">http://localhost:8073</a>.
Now the next step is to customize the parameters of your server in `config_webrx.py`.
Actually, if you do something cool with OpenWebRX, please drop me a mail:
*Jakob Ketterl, DD5JFK &lt;dd5jfk@darc.de&gt;*
## Usage tips ## Usage tips
@ -83,7 +46,7 @@ However, if you hold down the shift key, you can drag the center line (BFO) or t
## Licensing ## Licensing
OpenWebRX is available under Affero GPL v3 license OpenWebRX is available under Affero GPL v3 license
([summary](https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)). ([summary](https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0))).
OpenWebRX is also available under a commercial license on request. Please contact me at the address OpenWebRX is also available under a commercial license on request. Please contact me at the address
*&lt;randras@sdr.hu&gt;* for licensing options. *&lt;randras@sdr.hu&gt;* for licensing options.

View File

@ -8,7 +8,8 @@
"ft8": 1840000, "ft8": 1840000,
"wspr": 1836600, "wspr": 1836600,
"jt65": 1838000, "jt65": 1838000,
"jt9": 1839000 "jt9": 1839000,
"js8": 1842000
} }
}, },
{ {
@ -21,7 +22,8 @@
"wspr": 3592600, "wspr": 3592600,
"jt65": 3570000, "jt65": 3570000,
"jt9": 3572000, "jt9": 3572000,
"ft4": [3568000, 3575000] "ft4": [3568000, 3575000],
"js8": 3578000
} }
}, },
{ {
@ -43,7 +45,8 @@
"wspr": 7038600, "wspr": 7038600,
"jt65": 7076000, "jt65": 7076000,
"jt9": 7078000, "jt9": 7078000,
"ft4": 7047500 "ft4": 7047500,
"js8": 7078000
} }
}, },
{ {
@ -56,7 +59,8 @@
"wspr": 10138700, "wspr": 10138700,
"jt65": 10138000, "jt65": 10138000,
"jt9": 10140000, "jt9": 10140000,
"ft4": 10140000 "ft4": 10140000,
"js8": 10130000
} }
}, },
{ {
@ -69,7 +73,8 @@
"wspr": 14095600, "wspr": 14095600,
"jt65": 14076000, "jt65": 14076000,
"jt9": 14078000, "jt9": 14078000,
"ft4": 14080000 "ft4": 14080000,
"js8": 14078000
} }
}, },
{ {
@ -82,7 +87,8 @@
"wspr": 18104600, "wspr": 18104600,
"jt65": 18102000, "jt65": 18102000,
"jt9": 18104000, "jt9": 18104000,
"ft4": 18104000 "ft4": 18104000,
"js8": 18104000
} }
}, },
{ {
@ -95,7 +101,8 @@
"wspr": 21094600, "wspr": 21094600,
"jt65": 21076000, "jt65": 21076000,
"jt9": 21078000, "jt9": 21078000,
"ft4": 21140000 "ft4": 21140000,
"js8": 21078000
} }
}, },
{ {
@ -108,7 +115,8 @@
"wspr": 24924600, "wspr": 24924600,
"jt65": 24917000, "jt65": 24917000,
"jt9": 24919000, "jt9": 24919000,
"ft4": 24919000 "ft4": 24919000,
"js8": 24922000
} }
}, },
{ {
@ -121,7 +129,8 @@
"wspr": 28124600, "wspr": 28124600,
"jt65": 28076000, "jt65": 28076000,
"jt9": 28078000, "jt9": 28078000,
"ft4": 28180000 "ft4": 28180000,
"js8": 28078000
} }
}, },
{ {
@ -134,7 +143,8 @@
"wspr": 50293000, "wspr": 50293000,
"jt65": 50310000, "jt65": 50310000,
"jt9": 50312000, "jt9": 50312000,
"ft4": 50318000 "ft4": 50318000,
"js8": 50318000
} }
}, },
{ {
@ -189,5 +199,75 @@
"name": "3cm", "name": "3cm",
"lower_bound": 10000000000, "lower_bound": 10000000000,
"upper_bound": 10500000000 "upper_bound": 10500000000
},
{
"name": "120m Broadcast",
"lower_bound": 2300000,
"upper_bound": 2495000
},
{
"name": "90m Broadcast",
"lower_bound": 3200000,
"upper_bound": 3400000
},
{
"name": "75m Broadcast",
"lower_bound": 3900000,
"upper_bound": 4000000
},
{
"name": "60m Broadcast",
"lower_bound": 4750000,
"upper_bound": 4995000
},
{
"name": "49m Broadcast",
"lower_bound": 5900000,
"upper_bound": 6200000
},
{
"name": "41m Broadcast",
"lower_bound": 7200000,
"upper_bound": 7450000
},
{
"name": "31m Broadcast",
"lower_bound": 9400000,
"upper_bound": 9900000
},
{
"name": "25m Broadcast",
"lower_bound": 11600000,
"upper_bound": 12100000
},
{
"name": "22m Broadcast",
"lower_bound": 13570000,
"upper_bound": 13870000
},
{
"name": "19m Broadcast",
"lower_bound": 15100000,
"upper_bound": 15830000
},
{
"name": "16m Broadcast",
"lower_bound": 17480000,
"upper_bound": 17900000
},
{
"name": "15m Broadcast",
"lower_bound": 18900000,
"upper_bound": 19020000
},
{
"name": "13m Broadcast",
"lower_bound": 21450000,
"upper_bound": 21850000
},
{
"name": "11m Broadcast",
"lower_bound": 25670000,
"upper_bound": 26100000
} }
] ]

View File

@ -1,10 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
. docker/env
ARCH=$(uname -m)
TAG="latest"
ARCHTAG="$TAG-$ARCH"
docker build --pull -t openwebrx-base:$ARCHTAG -f docker/Dockerfiles/Dockerfile-base . 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 jketterl/openwebrx-rtlsdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr .
@ -13,4 +9,8 @@ docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-sdrplay:$ARCHTAG
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-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-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-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 . docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-full:$ARCHTAG -t jketterl/openwebrx:$ARCHTAG -f docker/Dockerfiles/Dockerfile-full .

View File

@ -32,8 +32,11 @@ config_webrx: configuration options for OpenWebRX
and use them for running your web service with OpenWebRX.) and use them for running your web service with OpenWebRX.)
""" """
# configuration version. please only modify if you're able to perform the associated migration steps.
version = 2
# NOTE: you can find additional information about configuring OpenWebRX in the Wiki: # NOTE: you can find additional information about configuring OpenWebRX in the Wiki:
# https://github.com/simonyiszk/openwebrx/wiki # https://github.com/jketterl/openwebrx/wiki/Configuration-guide
# ==== Server settings ==== # ==== Server settings ====
web_port = 8073 web_port = 8073
@ -44,24 +47,30 @@ receiver_name = "[Callsign]"
receiver_location = "Budapest, Hungary" receiver_location = "Budapest, Hungary"
receiver_asl = 200 receiver_asl = 200
receiver_admin = "example@example.com" receiver_admin = "example@example.com"
receiver_gps = (47.000000, 19.000000) receiver_gps = {"lat": 47.000000, "lon": 19.000000}
photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory" photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
# photo_desc allows you to put pretty much any HTML you like into the receiver description.
# The lines below should give you some examples of what's possible.
photo_desc = """ photo_desc = """
You can add your own background photo and receiver information.<br /> You can add your own background photo and receiver information.<br />
Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/> Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
Device: %[RX_DEVICE]<br /> Device: Receiver Device<br />
Antenna: %[RX_ANT]<br /> Antenna: Receiver Antenna<br />
Website: <a href="http://localhost" target="_blank">http://localhost</a> Website: <a href="http://localhost" target="_blank">http://localhost</a>
""" """
# ==== sdr.hu listing ==== # ==== Public receiver listings ====
# If you want your ham receiver to be listed publicly on sdr.hu, then take the following steps: # You can publish your receiver on online receiver directories, like https://www.receiverbook.de
# 1. Register at: http://sdr.hu/register # You will receive a receiver key from the directory that will authenticate you as the operator of this receiver.
# 2. You will get an unique key by email. Copy it and paste here: # Please note that you not share your receiver keys publicly since anyone that obtains your receiver key can take over
sdrhu_key = "" # your public listing.
# 3. Set this setting to True to enable listing: # Your receiver keys should be placed into this array:
sdrhu_public_listing = False receiver_keys = []
server_hostname = "localhost" # If you list your receiver on multiple sites, you can place all your keys into the array above, or you can append
# keys to the arraylike this:
# receiver_keys += ["my-receiver-key"]
# If you're not sure, simply copy & paste the code you received from your listing site below this line:
# ==== DSP/RX settings ==== # ==== DSP/RX settings ====
fft_fps = 9 fft_fps = 9
@ -93,13 +102,14 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
# ==== I/Q sources ==== # ==== I/Q sources ====
# (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.) # (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.)
################################################################################################# ###############################################################################
# Is my SDR hardware supported? # # Is my SDR hardware supported? #
# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support # # Check here: https://github.com/jketterl/openwebrx/wiki/Supported-Hardware #
################################################################################################# ###############################################################################
# Currently supported types of sdr receivers: # Currently supported types of sdr receivers:
# "rtl_sdr", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr" # "rtl_sdr", "rtl_sdr_soapy", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr",
# "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote"
# #
# In order to use rtl_sdr, you will need to install librtlsdr-dev and the connector. # In order to use rtl_sdr, you will need to install librtlsdr-dev and the connector.
# In order to use sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the # In order to use sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the
@ -107,8 +117,13 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
# #
# https://github.com/jketterl/owrx_connector # https://github.com/jketterl/owrx_connector
# #
# NOTE: The connector sources have replaced the old piped nmux style of reading input. If you still have any sdrs # In order to use Perseus HF you need to install the libperseus-sdr
# configured that have type endin in "_connector", simply remove that suffix. #
# https://github.com/Microtelecom/libperseus-sdr
#
# and do the proper changes to the sdrs object below
# (see also Wiki in https://github.com/jketterl/openwebrx/wiki/Sample-configuration-for-Perseus-HF-receiver).
#
sdrs = { sdrs = {
"rtlsdr": { "rtlsdr": {
@ -141,19 +156,18 @@ sdrs = {
"name": "Airspy HF+", "name": "Airspy HF+",
"type": "airspyhf", "type": "airspyhf",
"ppm": 0, "ppm": 0,
"rf_gain": "auto",
"profiles": { "profiles": {
"20m": { "20m": {
"name": "20m", "name": "20m",
"center_freq": 14150000, "center_freq": 14150000,
"rf_gain": 10, "samp_rate": 384000,
"samp_rate": 768000,
"start_freq": 14070000, "start_freq": 14070000,
"start_mod": "usb", "start_mod": "usb",
}, },
"30m": { "30m": {
"name": "30m", "name": "30m",
"center_freq": 10125000, "center_freq": 10125000,
"rf_gain": 10,
"samp_rate": 192000, "samp_rate": 192000,
"start_freq": 10142000, "start_freq": 10142000,
"start_mod": "usb", "start_mod": "usb",
@ -161,24 +175,21 @@ sdrs = {
"40m": { "40m": {
"name": "40m", "name": "40m",
"center_freq": 7100000, "center_freq": 7100000,
"rf_gain": 10,
"samp_rate": 256000, "samp_rate": 256000,
"start_freq": 7070000, "start_freq": 7070000,
"start_mod": "usb", "start_mod": "lsb",
}, },
"80m": { "80m": {
"name": "80m", "name": "80m",
"center_freq": 3650000, "center_freq": 3650000,
"rf_gain": 10, "samp_rate": 384000,
"samp_rate": 768000,
"start_freq": 3570000, "start_freq": 3570000,
"start_mod": "usb", "start_mod": "lsb",
}, },
"49m": { "49m": {
"name": "49m Broadcast", "name": "49m Broadcast",
"center_freq": 6000000, "center_freq": 6050000,
"rf_gain": 10, "samp_rate": 384000,
"samp_rate": 768000,
"start_freq": 6070000, "start_freq": 6070000,
"start_mod": "am", "start_mod": "am",
}, },
@ -188,6 +199,7 @@ sdrs = {
"name": "SDRPlay RSP2", "name": "SDRPlay RSP2",
"type": "sdrplay", "type": "sdrplay",
"ppm": 0, "ppm": 0,
"antenna": "Antenna A",
"profiles": { "profiles": {
"20m": { "20m": {
"name": "20m", "name": "20m",
@ -196,7 +208,6 @@ sdrs = {
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 14070000, "start_freq": 14070000,
"start_mod": "usb", "start_mod": "usb",
"antenna": "Antenna A",
}, },
"30m": { "30m": {
"name": "30m", "name": "30m",
@ -212,8 +223,7 @@ sdrs = {
"rf_gain": 0, "rf_gain": 0,
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 7070000, "start_freq": 7070000,
"start_mod": "usb", "start_mod": "lsb",
"antenna": "Antenna A",
}, },
"80m": { "80m": {
"name": "80m", "name": "80m",
@ -221,8 +231,7 @@ sdrs = {
"rf_gain": 0, "rf_gain": 0,
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 3570000, "start_freq": 3570000,
"start_mod": "usb", "start_mod": "lsb",
"antenna": "Antenna A",
}, },
"49m": { "49m": {
"name": "49m Broadcast", "name": "49m Broadcast",
@ -231,7 +240,6 @@ sdrs = {
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 6070000, "start_freq": 6070000,
"start_mod": "am", "start_mod": "am",
"antenna": "Antenna A",
}, },
}, },
}, },
@ -245,21 +253,21 @@ sdrs = {
waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF] waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF]
waterfall_min_level = -88 # in dB waterfall_min_level = -88 # in dB
waterfall_max_level = -20 waterfall_max_level = -20
waterfall_auto_level_margin = (5, 40) waterfall_auto_level_margin = {"min": 5, "max": 40}
### old theme by HA7ILM: ### old theme by HA7ILM:
# waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" # waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
# waterfall_min_level = -115 #in dB # waterfall_min_level = -115 #in dB
# waterfall_max_level = 0 # waterfall_max_level = 0
# waterfall_auto_level_margin = (20, 30) # waterfall_auto_level_margin = {"min": 20, "max": 30}
##For the old colors, you might also want to set [fft_voverlap_factor] to 0. ##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
# Note: When the auto waterfall level button is clicked, the following happens: # Note: When the auto waterfall level button is clicked, the following happens:
# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]] # [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin["min"]]
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]] # [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin["max"]]
# #
# ___|____________________________________|____________________________________|____________________________________|___> signal power # ___|________________________________________|____________________________________|________________________________________|___> signal power
# \_waterfall_auto_level_margin[0]_/ |__ current_min_power_level | \_waterfall_auto_level_margin[1]_/ # \_waterfall_auto_level_margin["min"]_/ |__ current_min_power_level | \_waterfall_auto_level_margin["max"]_/
# current_max_power_level __| # current_max_power_level __|
# === Experimental settings === # === Experimental settings ===
# Warning! The settings below are very experimental. # Warning! The settings below are very experimental.
@ -276,22 +284,28 @@ google_maps_api_key = ""
# in seconds; default: 2 hours # in seconds; default: 2 hours
map_position_retention_time = 2 * 60 * 60 map_position_retention_time = 2 * 60 * 60
# wsjt decoder queue configuration # decoder queue configuration
# due to the nature of the wsjt operating modes (ft8, ft8, jt9, jt65 and wspr), the data is recorded for a given amount # due to the nature of some operating modes (ft8, ft8, jt9, jt65, wspr and js8), the data is recorded for a given amount
# of time (6.5 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads. # of time (6 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
# to mitigate this, the recordings will be queued and processed in sequence. # to mitigate this, the recordings will be queued and processed in sequence.
# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread) # the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
wsjt_queue_workers = 2 decoding_queue_workers = 2
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up # the maximum queue length will cause decodes to be dumped if the workers cannot keep up
# if you are running background services, make sure this number is high enough to accept the task influx during peaks # if you are running background services, make sure this number is high enough to accept the task influx during peaks
# i.e. this should be higher than the number of wsjt services running at the same time # i.e. this should be higher than the number of decoding services running at the same time
wsjt_queue_length = 10 decoding_queue_length = 10
# wsjt decoding depth will allow more results, but will also consume more cpu # wsjt decoding depth will allow more results, but will also consume more cpu
wsjt_decoding_depth = 3 wsjt_decoding_depth = 3
# can also be set for each mode separately # can also be set for each mode separately
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent # jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
wsjt_decoding_depths = {"jt65": 1} wsjt_decoding_depths = {"jt65": 1}
# JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled.
js8_enabled_profiles = ["normal", "slow"]
# JS8 decoding depth; higher value will get more results, but will also consume more cpu
js8_decoding_depth = 3
temporary_directory = "/tmp" temporary_directory = "/tmp"
services_enabled = False services_enabled = False
@ -314,3 +328,8 @@ aprs_symbols_path = "/opt/aprs-symbols/png"
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator # this also uses the receiver_gps setting from above, so make sure it contains a correct locator
pskreporter_enabled = False pskreporter_enabled = False
pskreporter_callsign = "N0CALL" pskreporter_callsign = "N0CALL"
# === Web admin settings ===
# this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote
# changes to the receiver settings. enable for testing in controlled environment only.
# webadmin_enabled = False

View File

@ -29,7 +29,9 @@ import math
from functools import partial from functools import partial
from owrx.kiss import KissClient, DirewolfConfig from owrx.kiss import KissClient, DirewolfConfig
from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile
from owrx.js8 import Js8Profiles
from owrx.audio import AudioChopper
import logging import logging
@ -64,12 +66,105 @@ class output(object):
return True 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): class dsp(object):
def __init__(self, output): def __init__(self, output):
self.samp_rate = 250000 self.samp_rate = 250000
self.output_rate = 11025 self.output_rate = 11025
self.fft_size = 1024 self.fft_size = 1024
self.fft_fps = 5 self.fft_fps = 5
self.center_freq = 0
self.offset_freq = 0 self.offset_freq = 0
self.low_cut = -4000 self.low_cut = -4000
self.high_cut = 4000 self.high_cut = 4000
@ -82,6 +177,8 @@ class dsp(object):
self.demodulator = "nfm" self.demodulator = "nfm"
self.name = "csdr" self.name = "csdr"
self.base_bufsize = 512 self.base_bufsize = 512
self.decimation = None
self.last_decimation = None
self.nc_port = None self.nc_port = None
self.csdr_dynamic_bufsize = False self.csdr_dynamic_bufsize = False
self.csdr_print_bufsizes = False self.csdr_print_bufsizes = False
@ -94,31 +191,38 @@ class dsp(object):
self.secondary_fft_size = 1024 self.secondary_fft_size = 1024
self.secondary_process_fft = None self.secondary_process_fft = None
self.secondary_process_demod = None self.secondary_process_demod = None
self.pipe_names = [ self.pipe_names = {
"bpf_pipe", "bpf_pipe": Pipe.WRITE,
"shift_pipe", "shift_pipe": Pipe.WRITE,
"squelch_pipe", "squelch_pipe": Pipe.WRITE,
"smeter_pipe", "smeter_pipe": Pipe.READ,
"meta_pipe", "meta_pipe": Pipe.READ,
"iqtee_pipe", "iqtee_pipe": Pipe.NONE,
"iqtee2_pipe", "iqtee2_pipe": Pipe.NONE,
"dmr_control_pipe", "dmr_control_pipe": Pipe.WRITE,
] }
self.secondary_pipe_names = ["secondary_shift_pipe"] self.pipes = {}
self.secondary_pipe_names = {"secondary_shift_pipe": Pipe.WRITE}
self.secondary_offset_freq = 1000 self.secondary_offset_freq = 1000
self.unvoiced_quality = 1 self.unvoiced_quality = 1
self.modification_lock = threading.Lock() self.modification_lock = threading.Lock()
self.output = output self.output = output
self.temporary_directory = "/tmp"
self.temporary_directory = None
self.pipe_base_path = None
self.set_temporary_directory("/tmp")
self.is_service = False self.is_service = False
self.direwolf_config = None self.direwolf_config = None
self.direwolf_port = None self.direwolf_port = None
self.process = None
def set_service(self, flag=True): def set_service(self, flag=True):
self.is_service = flag self.is_service = flag
def set_temporary_directory(self, what): def set_temporary_directory(self, what):
self.temporary_directory = what self.temporary_directory = what
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self))
def chain(self, which): def chain(self, which):
chain = ["nc -v 127.0.0.1 {nc_port}"] chain = ["nc -v 127.0.0.1 {nc_port}"]
@ -137,7 +241,7 @@ class dsp(object):
if self.fft_compression == "adpcm": if self.fft_compression == "adpcm":
chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"] chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"]
return chain return chain
chain += ["csdr shift_addition_cc --fifo {shift_pipe}"] chain += ["csdr shift_addfast_cc --fifo {shift_pipe}"]
if self.decimation > 1: if self.decimation > 1:
chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"] chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"]
chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"] chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"]
@ -227,14 +331,14 @@ class dsp(object):
return chain return chain
elif which == "bpsk31" or which == "bpsk63": elif which == "bpsk31" or which == "bpsk63":
return chain + [ return chain + [
"csdr shift_addition_cc --fifo {secondary_shift_pipe}", "csdr shift_addfast_cc --fifo {secondary_shift_pipe}",
"csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}", "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}",
"csdr simple_agc_cc 0.001 0.5", "csdr simple_agc_cc 0.001 0.5",
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q", "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q",
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8", "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8",
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8", "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8",
] ]
elif self.isWsjtMode(which): elif self.isWsjtMode(which) or self.isJs8(which):
chain += ["csdr realpart_cf"] chain += ["csdr realpart_cf"]
if self.last_decimation != 1.0: if self.last_decimation != 1.0:
chain += ["csdr fractional_decimator_ff {last_decimation}"] chain += ["csdr fractional_decimator_ff {last_decimation}"]
@ -243,7 +347,7 @@ class dsp(object):
chain += ["csdr fmdemod_quadri_cf"] chain += ["csdr fmdemod_quadri_cf"]
if self.last_decimation != 1.0: if self.last_decimation != 1.0:
chain += ["csdr fractional_decimator_ff {last_decimation}"] chain += ["csdr fractional_decimator_ff {last_decimation}"]
return chain + ["csdr convert_f_s16", "direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h - 1>&2"] return chain + ["csdr convert_f_s16", "direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h 1>&2"]
elif which == "pocsag": elif which == "pocsag":
chain += ["csdr fmdemod_quadri_cf"] chain += ["csdr fmdemod_quadri_cf"]
if self.last_decimation != 1.0: if self.last_decimation != 1.0:
@ -301,8 +405,8 @@ class dsp(object):
self.try_create_configs(secondary_command_demod) self.try_create_configs(secondary_command_demod)
secondary_command_demod = secondary_command_demod.format( secondary_command_demod = secondary_command_demod.format(
input_pipe=self.iqtee2_pipe, input_pipe=self.pipes["iqtee2_pipe"],
secondary_shift_pipe=self.secondary_shift_pipe, secondary_shift_pipe=self.pipes["secondary_shift_pipe"],
secondary_decimation=self.secondary_decimation(), secondary_decimation=self.secondary_decimation(),
secondary_samples_per_bits=self.secondary_samples_per_bits(), secondary_samples_per_bits=self.secondary_samples_per_bits(),
secondary_bpf_cutoff=self.secondary_bpf_cutoff(), secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
@ -321,7 +425,7 @@ class dsp(object):
if self.output.supports_type("secondary_fft"): if self.output.supports_type("secondary_fft"):
secondary_command_fft = " | ".join(self.secondary_chain("fft")) secondary_command_fft = " | ".join(self.secondary_chain("fft"))
secondary_command_fft = secondary_command_fft.format( secondary_command_fft = secondary_command_fft.format(
input_pipe=self.iqtee_pipe, input_pipe=self.pipes["iqtee_pipe"],
secondary_fft_input_size=self.secondary_fft_size, secondary_fft_input_size=self.secondary_fft_size,
secondary_fft_size=self.secondary_fft_size, secondary_fft_size=self.secondary_fft_size,
secondary_fft_block_size=self.secondary_fft_block_size(), secondary_fft_block_size=self.secondary_fft_block_size(),
@ -347,18 +451,25 @@ class dsp(object):
if self.isWsjtMode(): if self.isWsjtMode():
smd = self.get_secondary_demodulator() smd = self.get_secondary_demodulator()
chopper_profile = None
if smd == "ft8": if smd == "ft8":
chopper = Ft8Chopper(self.secondary_process_demod.stdout) chopper_profile = Ft8Profile()
elif smd == "wspr": elif smd == "wspr":
chopper = WsprChopper(self.secondary_process_demod.stdout) chopper_profile = WsprProfile()
elif smd == "jt65": elif smd == "jt65":
chopper = Jt65Chopper(self.secondary_process_demod.stdout) chopper_profile = Jt65Profile()
elif smd == "jt9": elif smd == "jt9":
chopper = Jt9Chopper(self.secondary_process_demod.stdout) chopper_profile = Jt9Profile()
elif smd == "ft4": elif smd == "ft4":
chopper = Ft4Chopper(self.secondary_process_demod.stdout) chopper_profile = Ft4Profile()
if chopper_profile is not None:
chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile)
chopper.start()
self.output.send_output("wsjt_demod", chopper.read)
elif self.isJs8():
chopper = AudioChopper(self, self.secondary_process_demod.stdout, *Js8Profiles.getEnabledProfiles())
chopper.start() chopper.start()
self.output.send_output("wsjt_demod", chopper.read) self.output.send_output("js8_demod", chopper.read)
elif self.isPacket(): elif self.isPacket():
# we best get the ax25 packets from the kiss socket # we best get the ax25 packets from the kiss socket
kiss = KissClient(self.direwolf_port) kiss = KissClient(self.direwolf_port)
@ -369,18 +480,16 @@ class dsp(object):
self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
# open control pipes for csdr and send initialization data # open control pipes for csdr and send initialization data
if self.secondary_shift_pipe != None: # TODO digimodes if self.has_pipe("secondary_shift_pipe"): # TODO digimodes
self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w") # TODO digimodes
self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes
def set_secondary_offset_freq(self, value): def set_secondary_offset_freq(self, value):
self.secondary_offset_freq = value self.secondary_offset_freq = value
if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"): if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"):
self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())) self.pipes["secondary_shift_pipe"].write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
self.secondary_shift_pipe_file.flush()
def stop_secondary_demodulator(self): def stop_secondary_demodulator(self):
if self.secondary_processes_running == False: if not self.secondary_processes_running:
return return
self.try_delete_pipes(self.secondary_pipe_names) self.try_delete_pipes(self.secondary_pipe_names)
self.try_delete_configs() self.try_delete_configs()
@ -447,7 +556,7 @@ class dsp(object):
decimation += 1 decimation += 1
fraction = float(input_rate / decimation) / output_rate fraction = float(input_rate / decimation) / output_rate
intermediate_rate = input_rate / decimation intermediate_rate = input_rate / decimation
return (decimation, fraction, intermediate_rate) return decimation, fraction, intermediate_rate
def if_samp_rate(self): def if_samp_rate(self):
return self.samp_rate / self.decimation return self.samp_rate / self.decimation
@ -461,7 +570,7 @@ class dsp(object):
def get_audio_rate(self): def get_audio_rate(self):
if self.isDigitalVoice() or self.isPacket() or self.isPocsag(): if self.isDigitalVoice() or self.isPacket() or self.isPocsag():
return 48000 return 48000
elif self.isWsjtMode(): elif self.isWsjtMode() or self.isJs8():
return 12000 return 12000
return self.get_output_rate() return self.get_output_rate()
@ -475,6 +584,11 @@ class dsp(object):
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
def isJs8(self, demodulator = None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator == "js8"
def isPacket(self, demodulator=None): def isPacket(self, demodulator=None):
if demodulator is None: if demodulator is None:
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
@ -493,6 +607,8 @@ class dsp(object):
self.restart() self.restart()
def set_demodulator(self, demodulator): def set_demodulator(self, demodulator):
if demodulator in ["usb", "lsb", "cw"]:
demodulator = "ssb"
if self.demodulator == demodulator: if self.demodulator == demodulator:
return return
self.demodulator = demodulator self.demodulator = demodulator
@ -523,21 +639,22 @@ class dsp(object):
def set_offset_freq(self, offset_freq): def set_offset_freq(self, offset_freq):
self.offset_freq = offset_freq self.offset_freq = offset_freq
if self.running: if self.running:
self.modification_lock.acquire() self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
self.shift_pipe_file.write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
self.shift_pipe_file.flush() def set_center_freq(self, center_freq):
self.modification_lock.release() # dsp only needs to know this to be able to pass it to decoders in the form of get_operating_freq()
self.center_freq = center_freq
def get_operating_freq(self):
return self.center_freq + self.offset_freq
def set_bpf(self, low_cut, high_cut): def set_bpf(self, low_cut, high_cut):
self.low_cut = low_cut self.low_cut = low_cut
self.high_cut = high_cut self.high_cut = high_cut
if self.running: if self.running:
self.modification_lock.acquire() self.pipes["bpf_pipe"].write(
self.bpf_pipe_file.write(
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate()) "%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
) )
self.bpf_pipe_file.flush()
self.modification_lock.release()
def get_bpf(self): def get_bpf(self):
return [self.low_cut, self.high_cut] return [self.low_cut, self.high_cut]
@ -550,10 +667,7 @@ class dsp(object):
# no squelch required on digital voice modes # 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() else self.squelch_level
if self.running: if self.running:
self.modification_lock.acquire() self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
self.squelch_pipe_file.write("%g\n" % (self.convertToLinear(actual_squelch)))
self.squelch_pipe_file.flush()
self.modification_lock.release()
def set_unvoiced_quality(self, q): def set_unvoiced_quality(self, q):
self.unvoiced_quality = q self.unvoiced_quality = q
@ -563,39 +677,33 @@ class dsp(object):
return self.unvoiced_quality return self.unvoiced_quality
def set_dmr_filter(self, filter): def set_dmr_filter(self, filter):
if self.dmr_control_pipe_file: if self.has_pipe("dmr_control_pipe"):
self.dmr_control_pipe_file.write("{0}\n".format(filter)) self.pipes["dmr_control_pipe"].write("{0}\n".format(filter))
self.dmr_control_pipe_file.flush()
def mkfifo(self, path):
try:
os.unlink(path)
except:
pass
os.mkfifo(path)
def ddc_transition_bw(self): def ddc_transition_bw(self):
return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate)) return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate))
def try_create_pipes(self, pipe_names, command_base): def try_create_pipes(self, pipe_names, command_base):
for pipe_name in pipe_names: for pipe_name, pipe_type in pipe_names.items():
if "{" + pipe_name + "}" in command_base: if "{" + pipe_name + "}" in command_base:
setattr(self, pipe_name, self.pipe_base_path + pipe_name) p = self.pipe_base_path + pipe_name
self.mkfifo(getattr(self, pipe_name)) encoding = None
# TODO make digiham output unicode and then change this here
# the whole pipe enoding feature onlye exists because of this
if pipe_name == "meta_pipe":
encoding = "cp437"
self.pipes[pipe_name] = Pipe.create(p, pipe_type, encoding=encoding)
else: else:
setattr(self, pipe_name, None) self.pipes[pipe_name] = None
def has_pipe(self, name):
return name in self.pipes and self.pipes[name] is not None
def try_delete_pipes(self, pipe_names): def try_delete_pipes(self, pipe_names):
for pipe_name in pipe_names: for pipe_name in pipe_names:
pipe_path = getattr(self, pipe_name, None) if self.has_pipe(pipe_name):
if pipe_path: self.pipes[pipe_name].close()
try: self.pipes[pipe_name] = None
os.unlink(pipe_path)
except FileNotFoundError:
# it seems like we keep calling this twice. no idea why, but we don't need the resulting error.
pass
except Exception:
logger.exception("try_delete_pipes()")
def try_create_configs(self, command): def try_create_configs(self, command):
if "{direwolf_config}" in command: if "{direwolf_config}" in command:
@ -622,108 +730,94 @@ class dsp(object):
self.direwolf_config = None self.direwolf_config = None
def start(self): def start(self):
self.modification_lock.acquire() with self.modification_lock:
if self.running: if self.running:
self.modification_lock.release() return
return self.running = True
self.running = True
command_base = " | ".join(self.chain(self.demodulator)) command_base = " | ".join(self.chain(self.demodulator))
# create control pipes for csdr # create control pipes for csdr
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self)) self.try_create_pipes(self.pipe_names, command_base)
self.try_create_pipes(self.pipe_names, command_base) # send initial config through the pipes
if self.has_pipe("bpf_pipe"):
self.set_bpf(self.low_cut, self.high_cut)
if self.has_pipe("shift_pipe"):
self.set_offset_freq(self.offset_freq)
if self.has_pipe("squelch_pipe"):
self.set_squelch_level(self.squelch_level)
if self.has_pipe("dmr_control_pipe"):
self.set_dmr_filter(3)
# run the command # run the command
command = command_base.format( command = command_base.format(
bpf_pipe=self.bpf_pipe, bpf_pipe=self.pipes["bpf_pipe"],
shift_pipe=self.shift_pipe, shift_pipe=self.pipes["shift_pipe"],
decimation=self.decimation, squelch_pipe=self.pipes["squelch_pipe"],
last_decimation=self.last_decimation, smeter_pipe=self.pipes["smeter_pipe"],
fft_size=self.fft_size, meta_pipe=self.pipes["meta_pipe"],
fft_block_size=self.fft_block_size(), iqtee_pipe=self.pipes["iqtee_pipe"],
fft_averages=self.fft_averages, iqtee2_pipe=self.pipes["iqtee2_pipe"],
bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(), dmr_control_pipe=self.pipes["dmr_control_pipe"],
ddc_transition_bw=self.ddc_transition_bw(), decimation=self.decimation,
flowcontrol=int(self.samp_rate * 2), last_decimation=self.last_decimation,
start_bufsize=self.base_bufsize * self.decimation, fft_size=self.fft_size,
nc_port=self.nc_port, fft_block_size=self.fft_block_size(),
squelch_pipe=self.squelch_pipe, fft_averages=self.fft_averages,
smeter_pipe=self.smeter_pipe, bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(),
meta_pipe=self.meta_pipe, ddc_transition_bw=self.ddc_transition_bw(),
iqtee_pipe=self.iqtee_pipe, flowcontrol=int(self.samp_rate * 2),
iqtee2_pipe=self.iqtee2_pipe, start_bufsize=self.base_bufsize * self.decimation,
output_rate=self.get_output_rate(), nc_port=self.nc_port,
smeter_report_every=int(self.if_samp_rate() / 6000), output_rate=self.get_output_rate(),
unvoiced_quality=self.get_unvoiced_quality(), smeter_report_every=int(self.if_samp_rate() / 6000),
dmr_control_pipe=self.dmr_control_pipe, unvoiced_quality=self.get_unvoiced_quality(),
audio_rate=self.get_audio_rate(), audio_rate=self.get_audio_rate(),
)
logger.debug("Command = %s", command)
my_env = os.environ.copy()
if self.csdr_dynamic_bufsize:
my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
if self.csdr_print_bufsizes:
my_env["CSDR_PRINT_BUFSIZES"] = "1"
out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
self.process = subprocess.Popen(command, stdout=out, shell=True, start_new_session=True, env=my_env)
def watch_thread():
rc = self.process.wait()
logger.debug("dsp thread ended with rc=%d", rc)
if rc == 0 and self.running and not self.modification_lock.locked():
logger.debug("restarting since rc = 0, self.running = true, and no modification")
self.restart()
threading.Thread(target=watch_thread).start()
if self.output.supports_type("audio"):
self.output.send_output(
"audio",
partial(
self.process.stdout.read,
self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(),
),
) )
# open control pipes for csdr logger.debug("Command = %s", command)
if self.bpf_pipe: my_env = os.environ.copy()
self.bpf_pipe_file = open(self.bpf_pipe, "w") if self.csdr_dynamic_bufsize:
if self.shift_pipe: my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
self.shift_pipe_file = open(self.shift_pipe, "w") if self.csdr_print_bufsizes:
if self.squelch_pipe: my_env["CSDR_PRINT_BUFSIZES"] = "1"
self.squelch_pipe_file = open(self.squelch_pipe, "w")
self.start_secondary_demodulator()
self.modification_lock.release() out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
self.process = subprocess.Popen(command, stdout=out, shell=True, start_new_session=True, env=my_env)
# send initial config through the pipes def watch_thread():
if self.squelch_pipe: rc = self.process.wait()
self.set_squelch_level(self.squelch_level) logger.debug("dsp thread ended with rc=%d", rc)
if self.shift_pipe: if rc == 0 and self.running and not self.modification_lock.locked():
self.set_offset_freq(self.offset_freq) logger.debug("restarting since rc = 0, self.running = true, and no modification")
if self.bpf_pipe: self.restart()
self.set_bpf(self.low_cut, self.high_cut)
if self.smeter_pipe:
self.smeter_pipe_file = open(self.smeter_pipe, "r")
threading.Thread(target=watch_thread).start()
if self.output.supports_type("audio"):
self.output.send_output(
"audio",
partial(
self.process.stdout.read,
self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(),
),
)
self.start_secondary_demodulator()
if self.has_pipe("smeter_pipe"):
def read_smeter(): def read_smeter():
raw = self.smeter_pipe_file.readline() raw = self.pipes["smeter_pipe"].readline()
if len(raw) == 0: if len(raw) == 0:
return None return None
else: else:
return float(raw.rstrip("\n")) return float(raw.rstrip("\n"))
self.output.send_output("smeter", read_smeter) self.output.send_output("smeter", read_smeter)
if self.meta_pipe != None: if self.has_pipe("meta_pipe"):
# TODO make digiham output unicode and then change this here
self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437")
def read_meta(): def read_meta():
raw = self.meta_pipe_file.readline() raw = self.pipes["meta_pipe"].readline()
if len(raw) == 0: if len(raw) == 0:
return None return None
else: else:
@ -731,23 +825,23 @@ class dsp(object):
self.output.send_output("meta", read_meta) self.output.send_output("meta", read_meta)
if self.dmr_control_pipe: if self.csdr_dynamic_bufsize:
self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w") self.process.stdout.read(8) # dummy read to skip bufsize & preamble
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
def stop(self): def stop(self):
self.modification_lock.acquire() with self.modification_lock:
self.running = False self.running = False
if hasattr(self, "process"): if self.process is not None:
try: try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
except ProcessLookupError: self.process = None
# been killed by something else, ignore except ProcessLookupError:
pass # been killed by something else, ignore
self.stop_secondary_demodulator() pass
self.stop_secondary_demodulator()
self.try_delete_pipes(self.pipe_names) self.try_delete_pipes(self.pipe_names)
self.modification_lock.release()
def restart(self): def restart(self):
if not self.running: if not self.running:
@ -757,4 +851,3 @@ class dsp(object):
def __del__(self): def __del__(self):
self.stop() self.stop()
del self.process

136
debian/changelog vendored
View File

@ -1,5 +1,135 @@
openwebrx (0.18) UNRELEASED; urgency=low openwebrx (0.19.1) buster focal; urgency=low
* Initial release. * Added ability to authenticate receivers with listing sites using
"receiver id" tokens
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 08 Dec 2019 12:35:48 +0000 -- Jakob Ketterl <jakob.ketterl@gmx.de> Sat, 13 Jun 2020 16:46:00 +0000
openwebrx (0.19.0) buster focal; urgency=low
* Fix direwolf connection setup by implementing a retry loop
* Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector
* OSM maps instead of Google when google_maps_api_key is not set (thanks
@jquagga)
* Improved logic to pass parameters to soapy devices.
- `rtl_sdr_soapy`: added support for `bias_tee`
- `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch`
- `airspy`: added support for `bitpack`
* Added support for Perseus-SDR devices, (thanks @amontefusco)
* Property System has been rewritten so that defaults on sdr behave as
expected
* Waterfall range auto-adjustment now only takes the center 80% of the
spectrum into account, which should work better with SDRs that oversample
or have rather flat filter curves towards the spectrum edges
* Bugfix for negative network usage
* FiFi SDR: prevent arecord from shutting down after 2GB of data has been
sent
* Added support for bias tee control on rtl_sdr devices
* All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
* `rtl_sdr` type now also supports the `direct_sampling` option
* Added decoding implementation for for digimode "JS8Call" (requires an
installation of js8call and the js8py library)
* Reorganization of the frontend demodulator code
* Improve receiver load time by concatenating javascript assets
* HackRF support is now based on SoapyHackRF
* Removed sdr.hu server listing support since the site has been shut down
* Added support for Radioberry 2 Rasbperry Pi SDR Cape
-- Jakob Ketterl <jakob.ketterl@gmx.de> Mon, 01 Jun 2020 17:02:00 +0000
openwebrx (0.18.0) buster; urgency=low
* Compression, resampling and filtering in the frontend have been rewritten
in javascript, sdr.js has been removed
* Decoding of Pocsag modulation is now possible
* Removed the 3D waterfall since it had no real application and required ~1MB
of javascript code to be downloaded
* Improved the frontend handling of the "too many users" scenario
* PSK63 digimode is now available (same decoding pipeline as PSK31, but with
adopted parameters)
* The frequency can now be manipulated with the mousewheel, which should
allow the user to tune more precise. The tuning step size is determined by
the digit the mouse cursor is hovering over.
* Clicking on the frequency now opens an input for direct frequency selection
* URL hashes have been fixed and improved: They are now updated
automatically, so a shared URL will include frequency and demodulator,
which allows for improved sharing and linking.
* New daylight scheduler for background decoding, allows profiles to be
selected by local sunrise / sunset times
* The owrx_connector is now the default way of communicating with sdr
devices. The old sdr types have been replaced, all `_connector` suffixes on
the type must be removed!
* The sources have been refactored, making it a lot easier to add support for
other devices
* SDR device failure handling has been improved, including user feedback
* New devices supported:
* wsjt-x updated to 2.1.2
* The rtl_tcp compatibility mode of the owrx_connector is now configurable
using the `rtltcp_compat` flag
* explicit device filter for soapy devices for multi-device setups
* compatibility fixes for safari browsers (ios and mac)
* Offset tuning using the `lfo_offset` has been reworked in a way that
`center_freq` has to be set to the frequency you actually want to listen
to. If you're using an `lfo_offset` already, you will probably need to
change its sign.
* `initial_squelch_level` can now be set on each profile.
* Part of the frontend code has been reworked
- Audio buffer minimums have been completely stripped. As a result, you
should get better latency. Unfortunately, this also means there will be
some skipping when audio starts.
- Now also supports AudioWorklets (for those browser that have it).
- Mousewheel controls for the receiver sliders
* Error handling for failed SDR devices
* One of the most-requested features is finally coming to OpenWebRX:
Bookmarks (sometimes also referred to as labels).
There's two kinds of bookmarks available:
- Serverside bookmarks that are set up by the receiver administrator.
Check the file `bookmarks.json` for examples!
- Clientside bookmarks which every user can store for themselves. They are
stored in the browser's localStorage.
* Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is
now possible. Please have a look at the configuration on how to set it up.
* Websocket communication has been overhauled in large parts. It should now
be more reliable, and failing connections should now have no impact on
other users.
* Profile scheduling allows to set up band-hopping if you are running
background services.
* APRS now has the ability to show symbols on the map, if a corresponding
symbol set has been installed. Check the config!
* Debug logging has been disabled in a handful of modules, expect vastly
reduced output on the shell.
* New set of APRS-related features
- Decode Packet transmissions using direwolf (1k2 only for now)
- APRS packets are mostly decoded and shown both in a new panel and on the
map
- APRS is also available as a background service
- direwolfs I-gate functionality can be enabled, which allows your receiver
to work as a receive-only I-gate for the APRS network in the background
* Demodulation for background services has been optimized to use less total
bandwidth, saving CPU
* More metrics have been added; they can be used together with collectd and
its curl_json plugin for now, with some limitations.
* New bandplan feature, the first thing visible is the "dial" indicator that
brings you right to the dial frequency for digital modes
* fixed some bugs in the websocket communication which broke the map
* WSJT-X integration (FT8, FT4, WSPR, JT65, JT9 using wsjt-x demodulators)
* New Map Feature that shows both decoded grid squares from FT8 and Locations
decoded from YSF digital voice
* New Feature report that will show what functionality is available
* major rework on the openwebrx core
* Support of multiple SDR devices simultaneously
* Support for multiple profiles per SDR that allow the user to listen to
different frequencies
* Support for digital voice decoding
* Feature detection that will disable functionality when dependencies are not
available (if you're missing the digital
buttons, this is probably why)
* Support added for the following SDR sources:
- LimeSDR (`"type": "lime_sdr"`)
- PlutoSDR (`"type": "pluto_sdr"`)
- RTL_SDR via Soapy (`"type": "rtl_sdr_soapy"`) on special request to allow
use of the direct sampling mode
- SoapyRemote (`"type": "soapy_remote"`)
- FiFiSDR (`"type": "fifi_sdr"`)
- airspyhf devices (Airspy HF+ / Discovery) (`"type": "airspyhf"`)
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 18 Feb 2020 20:09:00 +0000

9
debian/control vendored
View File

@ -3,11 +3,14 @@ Maintainer: Jakob Ketterl <jakob.ketterl@gmx.de>
Section: hamradio Section: hamradio
Priority: optional Priority: optional
Standards-Version: 4.2.0 Standards-Version: 4.2.0
Build-Depends: debhelper (>= 10), dh-python, python3 (>= 3.5), dh-systemd (>= 1.5) Build-Depends: debhelper (>= 11), dh-python, python3-all (>= 3.5), python3-setuptools
Homepage: https://www.openwebrx.de/
Vcs-Browser: https://github.com/jketterl/openwebrx
Vcs-Git: https://github.com/jketterl/openwebrx.git
Package: openwebrx Package: openwebrx
Architecture: all Architecture: all
Depends: python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.1), ${python3:Depends} Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.2), python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends}
Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools
Description: multi-user web sdr Description: multi-user web sdr
Open source, multi-user SDR receiver with a web interface Open source, multi-user SDR receiver with a web interface

View File

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

7
debian/postinst vendored Executable file
View File

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

View File

@ -2,9 +2,7 @@ ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-airspy.sh / ADD docker/scripts/install-dependencies-airspy.sh /
RUN /install-dependencies-airspy.sh RUN /install-dependencies-airspy.sh &&\
RUN rm /install-dependencies-airspy.sh rm /install-dependencies-airspy.sh
ADD docker/scripts/install-connectors.sh / ADD . /opt/openwebrx
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

@ -1,19 +1,18 @@
FROM alpine:3.10 FROM debian:buster-slim
RUN apk add --no-cache bash ADD docker/files/js8call/js8call-hamlib.patch /
ADD docker/files/wsjtx/*.patch /
RUN ln -s /usr/local/lib /usr/local/lib64
ADD docker/scripts/direwolf-1.5.patch /
ADD docker/scripts/install-dependencies.sh / ADD docker/scripts/install-dependencies.sh /
RUN /install-dependencies.sh RUN /install-dependencies.sh && \
RUN rm /install-dependencies.sh rm /install-dependencies.sh && \
rm /*.patch
ADD . /opt/openwebrx ENTRYPOINT ["/init"]
WORKDIR /opt/openwebrx WORKDIR /opt/openwebrx
VOLUME /etc/openwebrx VOLUME /etc/openwebrx
ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ] CMD [ "/opt/openwebrx/docker/scripts/run.sh" ]
EXPOSE 8073 EXPOSE 8073

View File

@ -2,16 +2,25 @@ ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG FROM openwebrx-base:$ARCHTAG
ADD docker/scripts/install-dependencies-*.sh / ADD docker/scripts/install-dependencies-*.sh /
ADD docker/scripts/install-lib.*.patch / ADD docker/files/sdrplay/install-lib.*.patch /
RUN /install-dependencies-rtlsdr.sh RUN /install-dependencies-rtlsdr.sh &&\
RUN /install-dependencies-hackrf.sh /install-dependencies-soapysdr.sh &&\
RUN /install-dependencies-soapysdr.sh /install-dependencies-hackrf.sh &&\
RUN /install-dependencies-sdrplay.sh /install-dependencies-sdrplay.sh &&\
RUN /install-dependencies-airspy.sh /install-dependencies-airspy.sh &&\
RUN /install-dependencies-rtlsdr-soapy.sh /install-dependencies-rtlsdr-soapy.sh &&\
RUN rm /install-dependencies-*.sh /install-dependencies-plutosdr.sh &&\
/install-dependencies-limesdr.sh &&\
/install-dependencies-soapyremote.sh &&\
/install-dependencies-perseus.sh &&\
rm /install-dependencies-*.sh &&\
rm /install-lib.*.patch
ADD docker/scripts/install-connectors.sh / ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh RUN /install-connectors.sh &&\
RUN rm /install-connectors.sh rm /install-connectors.sh
ADD docker/files/services/sdrplay /etc/services.d/sdrplay
ADD . /opt/openwebrx

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,11 @@ ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG FROM openwebrx-base:$ARCHTAG
ADD docker/scripts/install-dependencies-rtlsdr.sh / ADD docker/scripts/install-dependencies-rtlsdr.sh /
RUN /install-dependencies-rtlsdr.sh
RUN rm /install-dependencies-rtlsdr.sh
ADD docker/scripts/install-connectors.sh / ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh
RUN rm /install-connectors.sh RUN /install-dependencies-rtlsdr.sh &&\
rm /install-dependencies-rtlsdr.sh &&\
/install-connectors.sh &&\
rm /install-connectors.sh
ADD . /opt/openwebrx

View File

@ -2,9 +2,7 @@ ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-rtlsdr-soapy.sh / ADD docker/scripts/install-dependencies-rtlsdr-soapy.sh /
RUN /install-dependencies-rtlsdr-soapy.sh RUN /install-dependencies-rtlsdr-soapy.sh &&\
RUN rm /install-dependencies-rtlsdr-soapy.sh rm /install-dependencies-rtlsdr-soapy.sh
ADD docker/scripts/install-connectors.sh / ADD . /opt/openwebrx
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

@ -2,10 +2,11 @@ ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-sdrplay.sh / ADD docker/scripts/install-dependencies-sdrplay.sh /
ADD docker/scripts/install-lib.*.patch / ADD docker/files/sdrplay/install-lib.*.patch /
RUN /install-dependencies-sdrplay.sh RUN /install-dependencies-sdrplay.sh &&\
RUN rm /install-dependencies-sdrplay.sh rm /install-dependencies-sdrplay.sh &&\
rm /install-lib.*.patch
ADD docker/scripts/install-connectors.sh / ADD docker/files/services/sdrplay /etc/services.d/sdrplay
RUN /install-connectors.sh
RUN rm /install-connectors.sh ADD . /opt/openwebrx

View File

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

View File

@ -2,6 +2,8 @@ ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG FROM openwebrx-base:$ARCHTAG
ADD docker/scripts/install-dependencies-soapysdr.sh / ADD docker/scripts/install-dependencies-soapysdr.sh /
RUN /install-dependencies-soapysdr.sh ADD docker/scripts/install-connectors.sh /
RUN rm /install-dependencies-soapysdr.sh RUN /install-dependencies-soapysdr.sh &&\
rm /install-dependencies-soapysdr.sh &&\
/install-connectors.sh &&\
rm /install-connectors.sh

5
docker/env Normal file
View File

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

View File

@ -0,0 +1,150 @@
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
@@ -78,4 +78,4 @@
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
# TRUE if all listed variables are TRUE
include (FindPackageHandleStandardArgs)
-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
+find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
diff -ur 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 @@
#
# libhamlib setup
#
-set (hamlib_STATIC 1)
+set (hamlib_STATIC 0)
find_package (hamlib 3 REQUIRED)
find_program (RIGCTL_EXE rigctl)
find_program (RIGCTLD_EXE rigctld)
@@ -1033,55 +1033,6 @@
target_link_libraries (js8 wsjt_fort wsjt_cxx Qt5::Core)
endif (${OPENMP_FOUND} OR APPLE)
-# build the main application
-add_executable (js8call MACOSX_BUNDLE
- ${sqlite3_CSRCS}
- ${wsjtx_CXXSRCS}
- ${wsjtx_GENUISRCS}
- wsjtx.rc
- ${WSJTX_ICON_FILE}
- ${wsjtx_RESOURCES_RCC}
- )
-
-if (WSJT_CREATE_WINMAIN)
- set_target_properties (js8call PROPERTIES WIN32_EXECUTABLE ON)
-endif (WSJT_CREATE_WINMAIN)
-
-set_target_properties (js8call PROPERTIES
- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in"
- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}"
- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}"
- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION}
- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}"
- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}"
- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
- MACOSX_BUNDLE_GUI_IDENTIFIER "org.kn4crd.js8call"
- )
-
-target_include_directories (js8call PRIVATE ${FFTW3_INCLUDE_DIRS})
-if (APPLE)
- target_link_libraries (js8call wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
-else ()
- target_link_libraries (js8call wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
- if (OpenMP_C_FLAGS)
- set_target_properties (js8call PROPERTIES
- COMPILE_FLAGS "${OpenMP_C_FLAGS}"
- LINK_FLAGS "${OpenMP_C_FLAGS}"
- )
- endif ()
- set_target_properties (js8call PROPERTIES
- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp
- )
- if (WIN32)
- set_target_properties (js8call PROPERTIES
- LINK_FLAGS -Wl,--stack,16777216
- )
- endif ()
-endif ()
-qt5_use_modules (js8call SerialPort) # not sure why the interface link library syntax above doesn't work
-
# if (UNIX)
# if (NOT WSJT_SKIP_MANPAGES)
# add_subdirectory (manpages)
@@ -1097,38 +1048,10 @@
#
# installation
#
-install (TARGETS js8call
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
- BUNDLE DESTINATION . COMPONENT runtime
- )
-
install (TARGETS js8 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
)
-install (PROGRAMS
- ${RIGCTL_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctl-local${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (PROGRAMS
- ${RIGCTLD_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctld-local${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (FILES
- README
- COPYING
- INSTALL
- INSTALL-WSJTX
- DESTINATION ${CMAKE_INSTALL_DOCDIR}
- #COMPONENT runtime
- )
-
install (FILES
contrib/Ephemeris/JPLEPH
DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
@@ -1182,32 +1105,6 @@
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
)
-
-if (NOT WIN32 AND NOT APPLE)
- # install a desktop file so js8call appears in the application start
- # menu with an icon
- install (
- FILES js8call.desktop
- DESTINATION /usr/share/applications
- #COMPONENT runtime
- )
- install (
- FILES icons/Unix/js8call_icon.png
- DESTINATION /usr/share/pixmaps
- #COMPONENT runtime
- )
-
- IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/opt/js8call")
- execute_process(COMMAND ln -s /opt/js8call/bin/js8call ljs8call)
-
- install(FILES
- ${CMAKE_BINARY_DIR}/ljs8call DESTINATION /usr/bin/ RENAME js8call
- #COMPONENT runtime
- )
- endif()
-endif (NOT WIN32 AND NOT APPLE)
-
-
#
# bundle fixup only done in Release or MinSizeRel configurations
#
Only in js8call/: .idea

View File

@ -0,0 +1,23 @@
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
--- sdrplay-orig/install_lib.sh 2020-05-24 14:30:06.022483867 +0000
+++ sdrplay/install_lib.sh 2020-05-24 14:30:49.093435726 +0000
@@ -4,19 +4,6 @@
export MAJVERS="3"
echo "Installing SDRplay RSP API library ${VERS}..."
-read -p "Press RETURN to view the license agreement" ret
-
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
export ARCH=`uname -m`

View File

@ -0,0 +1,40 @@
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
--- sdrplay-orig/install_lib.sh 2020-05-24 14:13:04.561271707 +0000
+++ sdrplay/install_lib.sh 2020-05-24 14:16:20.068329040 +0000
@@ -4,19 +4,6 @@
MAJVERS="3"
echo "Installing SDRplay RSP API library ${VERS}..."
-read -p "Press RETURN to view the license agreement" ret
-
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
ARCH=`uname -m`
@@ -141,16 +128,6 @@
echo "SDRplay API ${VERS} Installation Finished"
echo " "
-while true; do
- echo "Would you like to add SDRplay USB IDs to the local database for easier
-"
- read -p "identification in applications such as lsusb? [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/.
sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh
sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/.

View File

@ -0,0 +1,39 @@
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
--- sdrplay-orig/install_lib.sh 2020-05-24 13:56:56.622000041 +0000
+++ sdrplay/install_lib.sh 2020-05-24 13:58:51.837801559 +0000
@@ -4,19 +4,6 @@
MAJVERS="3"
echo "Installing SDRplay RSP API library ${VERS}..."
-read -p "Press RETURN to view the license agreement" ret
-
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
ARCH=`uname -m`
OSDIST="Unknown"
@@ -157,15 +144,6 @@
echo " "
echo "SDRplay API ${VERS} Installation Finished"
echo " "
-while true; do
- echo "Would you like to add SDRplay USB IDs to the local database for easier"
- read -p "identification in applications such as lsusb? [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/.
sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh
sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/.

View File

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

View File

@ -0,0 +1,43 @@
--- CMakeLists.txt 2020-05-25 19:26:41.423517236 +0200
+++ CMakeLists.txt 2020-05-25 19:11:36.116236231 +0200
@@ -79,24 +79,6 @@
include (ExternalProject)
-
-#
-# build and install hamlib locally so it can be referenced by the
-# WSJT-X build
-#
-ExternalProject_Add (hamlib
- GIT_REPOSITORY ${hamlib_repo}
- GIT_TAG ${hamlib_TAG}
- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/hamlib.tgz
- URL_HASH MD5=${hamlib_md5sum}
- UPDATE_COMMAND ./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
- INSTALL_COMMAND $(MAKE) install-strip V=1 DESTDIR=""
- STEP_TARGETS update install
- )
-
#
# custom target to make a hamlib source tarball
#
@@ -128,7 +110,6 @@
# build and optionally install WSJT-X using the hamlib package built
# above
#
-ExternalProject_Get_Property (hamlib INSTALL_DIR)
ExternalProject_Add (wsjtx
GIT_REPOSITORY ${wsjtx_repo}
GIT_TAG ${WSJTX_TAG}
@@ -152,7 +133,6 @@
DEPENDEES build
)
-set_target_properties (hamlib PROPERTIES EXCLUDE_FROM_ALL 1)
set_target_properties (wsjtx PROPERTIES EXCLUDE_FROM_ALL 1)
add_dependencies (wsjtx-configure hamlib-install)

View File

@ -0,0 +1,156 @@
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
@@ -85,4 +85,4 @@
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
# TRUE if all listed variables are TRUE
include (FindPackageHandleStandardArgs)
-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
+find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
--- 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 @@
#
# libhamlib setup
#
-set (hamlib_STATIC 1)
+set (hamlib_STATIC 0)
find_package (hamlib 3 REQUIRED)
find_program (RIGCTL_EXE rigctl)
find_program (RIGCTLD_EXE rigctld)
@@ -1326,54 +1326,10 @@
endif(WSJT_BUILD_UTILS)
-# build the main application
-add_executable (wsjtx MACOSX_BUNDLE
- ${wsjtx_CXXSRCS}
- ${wsjtx_GENUISRCS}
- wsjtx.rc
- ${WSJTX_ICON_FILE}
- ${wsjtx_RESOURCES_RCC}
- )
-
if (WSJT_CREATE_WINMAIN)
set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON)
endif (WSJT_CREATE_WINMAIN)
-set_target_properties (wsjtx PROPERTIES
- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in"
- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}"
- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}"
- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION}
- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}"
- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}"
- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
- MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx"
- )
-
-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})
-else ()
- target_link_libraries (wsjtx 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}"
- LINK_FLAGS "${OpenMP_C_FLAGS}"
- )
- endif ()
- set_target_properties (wsjtx PROPERTIES
- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp
- )
- if (WIN32)
- set_target_properties (wsjtx PROPERTIES
- LINK_FLAGS -Wl,--stack,16777216
- )
- 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 @@
set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
endif (WSJT_CREATE_WINMAIN)
-if (UNIX)
- if (NOT WSJT_SKIP_MANPAGES)
- add_subdirectory (manpages)
- add_dependencies (wsjtx manpages)
- endif (NOT WSJT_SKIP_MANPAGES)
- if (NOT APPLE)
- add_subdirectory (debian)
- add_dependencies (wsjtx debian)
- endif (NOT APPLE)
-endif (UNIX)
-
#
# installation
#
-install (TARGETS wsjtx
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
- BUNDLE DESTINATION . COMPONENT runtime
- )
# install (TARGETS wsjtx_udp EXPORT udp
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -1453,12 +1394,7 @@
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
# )
-install (TARGETS udp_daemon message_aggregator
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
- BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
- )
-
-install (TARGETS jt9 wsprd fmtave fcal fmeasure
+install (TARGETS jt9 wsprd
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
)
@@ -1471,39 +1407,6 @@
)
endif(WSJT_BUILD_UTILS)
-install (PROGRAMS
- ${RIGCTL_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctl-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (PROGRAMS
- ${RIGCTLD_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctld-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (PROGRAMS
- ${RIGCTLCOM_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctlcom-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (FILES
- README
- COPYING
- AUTHORS
- THANKS
- NEWS
- INSTALL
- BUGS
- DESTINATION ${CMAKE_INSTALL_DOCDIR}
- #COMPONENT runtime
- )
-
install (FILES
contrib/Ephemeris/JPLEPH
DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
Only in wsjtx: .idea

View File

@ -1,241 +0,0 @@
diff --git a/Makefile.linux b/Makefile.linux
index 5010833..3f61de9 100644
--- a/Makefile.linux
+++ b/Makefile.linux
@@ -585,102 +585,102 @@ install : $(APPS) direwolf.conf tocalls.txt symbols-new.txt symbolsX.txt dw-icon
# Applications, not installed with package manager, normally go in /usr/local/bin.
# /usr/bin is used instead when installing from .DEB or .RPM package.
#
- $(INSTALL) -D --mode=755 direwolf $(DESTDIR)/bin/direwolf
- $(INSTALL) -D --mode=755 decode_aprs $(DESTDIR)/bin/decode_aprs
- $(INSTALL) -D --mode=755 text2tt $(DESTDIR)/bin/text2tt
- $(INSTALL) -D --mode=755 tt2text $(DESTDIR)/bin/tt2text
- $(INSTALL) -D --mode=755 ll2utm $(DESTDIR)/bin/ll2utm
- $(INSTALL) -D --mode=755 utm2ll $(DESTDIR)/bin/utm2ll
- $(INSTALL) -D --mode=755 aclients $(DESTDIR)/bin/aclients
- $(INSTALL) -D --mode=755 log2gpx $(DESTDIR)/bin/log2gpx
- $(INSTALL) -D --mode=755 gen_packets $(DESTDIR)/bin/gen_packets
- $(INSTALL) -D --mode=755 atest $(DESTDIR)/bin/atest
- $(INSTALL) -D --mode=755 ttcalc $(DESTDIR)/bin/ttcalc
- $(INSTALL) -D --mode=755 kissutil $(DESTDIR)/bin/kissutil
- $(INSTALL) -D --mode=755 cm108 $(DESTDIR)/bin/cm108
- $(INSTALL) -D --mode=755 dwespeak.sh $(DESTDIR)/bin/dwspeak.sh
+ $(INSTALL) -D -m=755 direwolf $(DESTDIR)/bin/direwolf
+ $(INSTALL) -D -m=755 decode_aprs $(DESTDIR)/bin/decode_aprs
+ $(INSTALL) -D -m=755 text2tt $(DESTDIR)/bin/text2tt
+ $(INSTALL) -D -m=755 tt2text $(DESTDIR)/bin/tt2text
+ $(INSTALL) -D -m=755 ll2utm $(DESTDIR)/bin/ll2utm
+ $(INSTALL) -D -m=755 utm2ll $(DESTDIR)/bin/utm2ll
+ $(INSTALL) -D -m=755 aclients $(DESTDIR)/bin/aclients
+ $(INSTALL) -D -m=755 log2gpx $(DESTDIR)/bin/log2gpx
+ $(INSTALL) -D -m=755 gen_packets $(DESTDIR)/bin/gen_packets
+ $(INSTALL) -D -m=755 atest $(DESTDIR)/bin/atest
+ $(INSTALL) -D -m=755 ttcalc $(DESTDIR)/bin/ttcalc
+ $(INSTALL) -D -m=755 kissutil $(DESTDIR)/bin/kissutil
+ $(INSTALL) -D -m=755 cm108 $(DESTDIR)/bin/cm108
+ $(INSTALL) -D -m=755 dwespeak.sh $(DESTDIR)/bin/dwspeak.sh
#
# Telemetry Toolkit executables. Other .conf and .txt files will go into doc directory.
#
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-balloon.pl $(DESTDIR)/bin/telem-balloon.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-bits.pl $(DESTDIR)/bin/telem-bits.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-data.pl $(DESTDIR)/bin/telem-data.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-data91.pl $(DESTDIR)/bin/telem-data91.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-eqns.pl $(DESTDIR)/bin/telem-eqns.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-parm.pl $(DESTDIR)/bin/telem-parm.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-seq.sh $(DESTDIR)/bin/telem-seq.sh
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-unit.pl $(DESTDIR)/bin/telem-unit.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-volts.py $(DESTDIR)/bin/telem-volts.py
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-balloon.pl $(DESTDIR)/bin/telem-balloon.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-bits.pl $(DESTDIR)/bin/telem-bits.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-data.pl $(DESTDIR)/bin/telem-data.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-data91.pl $(DESTDIR)/bin/telem-data91.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-eqns.pl $(DESTDIR)/bin/telem-eqns.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-parm.pl $(DESTDIR)/bin/telem-parm.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-seq.sh $(DESTDIR)/bin/telem-seq.sh
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-unit.pl $(DESTDIR)/bin/telem-unit.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-volts.py $(DESTDIR)/bin/telem-volts.py
#
# Misc. data such as "tocall" to system mapping.
#
- $(INSTALL) -D --mode=644 tocalls.txt $(DESTDIR)/share/direwolf/tocalls.txt
- $(INSTALL) -D --mode=644 symbols-new.txt $(DESTDIR)/share/direwolf/symbols-new.txt
- $(INSTALL) -D --mode=644 symbolsX.txt $(DESTDIR)/share/direwolf/symbolsX.txt
+ $(INSTALL) -D -m=644 tocalls.txt $(DESTDIR)/share/direwolf/tocalls.txt
+ $(INSTALL) -D -m=644 symbols-new.txt $(DESTDIR)/share/direwolf/symbols-new.txt
+ $(INSTALL) -D -m=644 symbolsX.txt $(DESTDIR)/share/direwolf/symbolsX.txt
#
# For desktop icon.
#
- $(INSTALL) -D --mode=644 dw-icon.png $(DESTDIR)/share/direwolf/pixmaps/dw-icon.png
- $(INSTALL) -D --mode=644 direwolf.desktop $(DESTDIR)/share/applications/direwolf.desktop
+ $(INSTALL) -D -m=644 dw-icon.png $(DESTDIR)/share/direwolf/pixmaps/dw-icon.png
+ $(INSTALL) -D -m=644 direwolf.desktop $(DESTDIR)/share/applications/direwolf.desktop
#
# Documentation. Various plain text files and PDF.
#
- $(INSTALL) -D --mode=644 CHANGES.md $(DESTDIR)/share/doc/direwolf/CHANGES.md
- $(INSTALL) -D --mode=644 LICENSE-dire-wolf.txt $(DESTDIR)/share/doc/direwolf/LICENSE-dire-wolf.txt
- $(INSTALL) -D --mode=644 LICENSE-other.txt $(DESTDIR)/share/doc/direwolf/LICENSE-other.txt
+ $(INSTALL) -D -m=644 CHANGES.md $(DESTDIR)/share/doc/direwolf/CHANGES.md
+ $(INSTALL) -D -m=644 LICENSE-dire-wolf.txt $(DESTDIR)/share/doc/direwolf/LICENSE-dire-wolf.txt
+ $(INSTALL) -D -m=644 LICENSE-other.txt $(DESTDIR)/share/doc/direwolf/LICENSE-other.txt
#
# ./README.md is an overview for the project main page.
# Maybe we could stick it in some other place.
# doc/README.md contains an overview of the PDF file contents and is more useful here.
#
- $(INSTALL) -D --mode=644 doc/README.md $(DESTDIR)/share/doc/direwolf/README.md
- $(INSTALL) -D --mode=644 doc/2400-4800-PSK-for-APRS-Packet-Radio.pdf $(DESTDIR)/share/doc/direwolf/2400-4800-PSK-for-APRS-Packet-Radio.pdf
- $(INSTALL) -D --mode=644 doc/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf
- $(INSTALL) -D --mode=644 doc/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf
- $(INSTALL) -D --mode=644 doc/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf $(DESTDIR)/share/doc/direwolf/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf
- $(INSTALL) -D --mode=644 doc/APRS-Telemetry-Toolkit.pdf $(DESTDIR)/share/doc/direwolf/APRS-Telemetry-Toolkit.pdf
- $(INSTALL) -D --mode=644 doc/APRStt-Implementation-Notes.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Implementation-Notes.pdf
- $(INSTALL) -D --mode=644 doc/APRStt-interface-for-SARTrack.pdf $(DESTDIR)/share/doc/direwolf/APRStt-interface-for-SARTrack.pdf
- $(INSTALL) -D --mode=644 doc/APRStt-Listening-Example.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Listening-Example.pdf
- $(INSTALL) -D --mode=644 doc/Bluetooth-KISS-TNC.pdf $(DESTDIR)/share/doc/direwolf/Bluetooth-KISS-TNC.pdf
- $(INSTALL) -D --mode=644 doc/Going-beyond-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/Going-beyond-9600-baud.pdf
- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-APRS.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS.pdf
- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-APRS-Tracker.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS-Tracker.pdf
- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-SDR-IGate.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-SDR-IGate.pdf
- $(INSTALL) -D --mode=644 doc/Successful-APRS-IGate-Operation.pdf $(DESTDIR)/share/doc/direwolf/Successful-APRS-IGate-Operation.pdf
- $(INSTALL) -D --mode=644 doc/User-Guide.pdf $(DESTDIR)/share/doc/direwolf/User-Guide.pdf
- $(INSTALL) -D --mode=644 doc/WA8LMF-TNC-Test-CD-Results.pdf $(DESTDIR)/share/doc/direwolf/WA8LMF-TNC-Test-CD-Results.pdf
+ $(INSTALL) -D -m=644 doc/README.md $(DESTDIR)/share/doc/direwolf/README.md
+ $(INSTALL) -D -m=644 doc/2400-4800-PSK-for-APRS-Packet-Radio.pdf $(DESTDIR)/share/doc/direwolf/2400-4800-PSK-for-APRS-Packet-Radio.pdf
+ $(INSTALL) -D -m=644 doc/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf
+ $(INSTALL) -D -m=644 doc/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf
+ $(INSTALL) -D -m=644 doc/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf $(DESTDIR)/share/doc/direwolf/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf
+ $(INSTALL) -D -m=644 doc/APRS-Telemetry-Toolkit.pdf $(DESTDIR)/share/doc/direwolf/APRS-Telemetry-Toolkit.pdf
+ $(INSTALL) -D -m=644 doc/APRStt-Implementation-Notes.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Implementation-Notes.pdf
+ $(INSTALL) -D -m=644 doc/APRStt-interface-for-SARTrack.pdf $(DESTDIR)/share/doc/direwolf/APRStt-interface-for-SARTrack.pdf
+ $(INSTALL) -D -m=644 doc/APRStt-Listening-Example.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Listening-Example.pdf
+ $(INSTALL) -D -m=644 doc/Bluetooth-KISS-TNC.pdf $(DESTDIR)/share/doc/direwolf/Bluetooth-KISS-TNC.pdf
+ $(INSTALL) -D -m=644 doc/Going-beyond-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/Going-beyond-9600-baud.pdf
+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-APRS.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS.pdf
+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-APRS-Tracker.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS-Tracker.pdf
+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-SDR-IGate.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-SDR-IGate.pdf
+ $(INSTALL) -D -m=644 doc/Successful-APRS-IGate-Operation.pdf $(DESTDIR)/share/doc/direwolf/Successful-APRS-IGate-Operation.pdf
+ $(INSTALL) -D -m=644 doc/User-Guide.pdf $(DESTDIR)/share/doc/direwolf/User-Guide.pdf
+ $(INSTALL) -D -m=644 doc/WA8LMF-TNC-Test-CD-Results.pdf $(DESTDIR)/share/doc/direwolf/WA8LMF-TNC-Test-CD-Results.pdf
#
# Various sample config and other files go into examples under the doc directory.
# When building from source, these can be put in home directory with "make install-conf".
# When installed from .DEB or .RPM package, the user will need to copy these to
# the home directory or other desired location.
#
- $(INSTALL) -D --mode=644 direwolf.conf $(DESTDIR)/share/doc/direwolf/examples/direwolf.conf
- $(INSTALL) -D --mode=755 dw-start.sh $(DESTDIR)/share/doc/direwolf/examples/dw-start.sh
- $(INSTALL) -D --mode=644 sdr.conf $(DESTDIR)/share/doc/direwolf/examples/sdr.conf
- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-m0xer-3.txt $(DESTDIR)/share/doc/direwolf/examples/telem-m0xer-3.txt
- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-balloon.conf $(DESTDIR)/share/doc/direwolf/examples/telem-balloon.conf
- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-volts.conf $(DESTDIR)/share/doc/direwolf/examples/telem-volts.conf
+ $(INSTALL) -D -m=644 direwolf.conf $(DESTDIR)/share/doc/direwolf/examples/direwolf.conf
+ $(INSTALL) -D -m=755 dw-start.sh $(DESTDIR)/share/doc/direwolf/examples/dw-start.sh
+ $(INSTALL) -D -m=644 sdr.conf $(DESTDIR)/share/doc/direwolf/examples/sdr.conf
+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-m0xer-3.txt $(DESTDIR)/share/doc/direwolf/examples/telem-m0xer-3.txt
+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-balloon.conf $(DESTDIR)/share/doc/direwolf/examples/telem-balloon.conf
+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-volts.conf $(DESTDIR)/share/doc/direwolf/examples/telem-volts.conf
#
# "man" pages
#
- $(INSTALL) -D --mode=644 man1/aclients.1 $(DESTDIR)/share/man/man1/aclients.1
- $(INSTALL) -D --mode=644 man1/atest.1 $(DESTDIR)/share/man/man1/atest.1
- $(INSTALL) -D --mode=644 man1/decode_aprs.1 $(DESTDIR)/share/man/man1/decode_aprs.1
- $(INSTALL) -D --mode=644 man1/direwolf.1 $(DESTDIR)/share/man/man1/direwolf.1
- $(INSTALL) -D --mode=644 man1/gen_packets.1 $(DESTDIR)/share/man/man1/gen_packets.1
- $(INSTALL) -D --mode=644 man1/kissutil.1 $(DESTDIR)/share/man/man1/kissutil.1
- $(INSTALL) -D --mode=644 man1/ll2utm.1 $(DESTDIR)/share/man/man1/ll2utm.1
- $(INSTALL) -D --mode=644 man1/log2gpx.1 $(DESTDIR)/share/man/man1/log2gpx.1
- $(INSTALL) -D --mode=644 man1/text2tt.1 $(DESTDIR)/share/man/man1/text2tt.1
- $(INSTALL) -D --mode=644 man1/tt2text.1 $(DESTDIR)/share/man/man1/tt2text.1
- $(INSTALL) -D --mode=644 man1/utm2ll.1 $(DESTDIR)/share/man/man1/utm2ll.1
+ $(INSTALL) -D -m=644 man1/aclients.1 $(DESTDIR)/share/man/man1/aclients.1
+ $(INSTALL) -D -m=644 man1/atest.1 $(DESTDIR)/share/man/man1/atest.1
+ $(INSTALL) -D -m=644 man1/decode_aprs.1 $(DESTDIR)/share/man/man1/decode_aprs.1
+ $(INSTALL) -D -m=644 man1/direwolf.1 $(DESTDIR)/share/man/man1/direwolf.1
+ $(INSTALL) -D -m=644 man1/gen_packets.1 $(DESTDIR)/share/man/man1/gen_packets.1
+ $(INSTALL) -D -m=644 man1/kissutil.1 $(DESTDIR)/share/man/man1/kissutil.1
+ $(INSTALL) -D -m=644 man1/ll2utm.1 $(DESTDIR)/share/man/man1/ll2utm.1
+ $(INSTALL) -D -m=644 man1/log2gpx.1 $(DESTDIR)/share/man/man1/log2gpx.1
+ $(INSTALL) -D -m=644 man1/text2tt.1 $(DESTDIR)/share/man/man1/text2tt.1
+ $(INSTALL) -D -m=644 man1/tt2text.1 $(DESTDIR)/share/man/man1/tt2text.1
+ $(INSTALL) -D -m=644 man1/utm2ll.1 $(DESTDIR)/share/man/man1/utm2ll.1
#
# Set group and mode of HID devices corresponding to C-Media USB Audio adapters.
# This will allow us to use the CM108/CM119 GPIO pins for PTT.
#
- $(INSTALL) -D --mode=644 99-direwolf-cmedia.rules /etc/udev/rules.d/99-direwolf-cmedia.rules
+ $(INSTALL) -D -m=644 99-direwolf-cmedia.rules /etc/udev/rules.d/99-direwolf-cmedia.rules
#
@echo " "
@echo "If this is your first install, not an upgrade, type this to put a copy"
diff --git a/cdigipeater.c b/cdigipeater.c
index 9c40d95..94112e9 100644
--- a/cdigipeater.c
+++ b/cdigipeater.c
@@ -49,7 +49,7 @@
#include <stdio.h>
#include <ctype.h> /* for isdigit, isupper */
#include "regex.h"
-#include <sys/unistd.h>
+#include <unistd.h>
#include "ax25_pad.h"
#include "cdigipeater.h"
diff --git a/decode_aprs.c b/decode_aprs.c
index 35c186b..a620cb3 100644
--- a/decode_aprs.c
+++ b/decode_aprs.c
@@ -3872,11 +3872,7 @@ static void decode_tocall (decode_aprs_t *A, char *dest)
* models before getting to the more generic APY.
*/
-#if defined(__WIN32__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__)
qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), tocall_cmp);
-#else
- qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), (__compar_fn_t)tocall_cmp);
-#endif
}
else {
if ( ! A->g_quiet) {
diff --git a/digipeater.c b/digipeater.c
index 36970d7..5195582 100644
--- a/digipeater.c
+++ b/digipeater.c
@@ -62,7 +62,7 @@
#include <stdio.h>
#include <ctype.h> /* for isdigit, isupper */
#include "regex.h"
-#include <sys/unistd.h>
+#include <unistd.h>
#include "ax25_pad.h"
#include "digipeater.h"
diff --git a/direwolf.h b/direwolf.h
index 514bcc5..52f5ae9 100644
--- a/direwolf.h
+++ b/direwolf.h
@@ -274,7 +274,7 @@ char *strtok_r(char *str, const char *delim, char **saveptr);
char *strcasestr(const char *S, const char *FIND);
-#if defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__)
+#if 1
// strlcpy and strlcat should be in string.h and the C library.
diff --git a/multi_modem.c b/multi_modem.c
index 5d96c79..24261b9 100644
--- a/multi_modem.c
+++ b/multi_modem.c
@@ -80,7 +80,7 @@
#include <string.h>
#include <assert.h>
#include <stdio.h>
-#include <sys/unistd.h>
+#include <unistd.h>
#include "ax25_pad.h"
#include "textcolor.h"

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,12 +18,14 @@ function cmakebuild() {
cd /tmp cd /tmp
BUILD_PACKAGES="git cmake make gcc g++ musl-dev" BUILD_PACKAGES="git cmake make gcc g++"
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
apt-get update
apt-get -y install --no-install-recommends $BUILD_PACKAGES
git clone https://github.com/jketterl/owrx_connector.git git clone https://github.com/jketterl/owrx_connector.git
cmakebuild owrx_connector df35e33e42c2e4527853ca18bf04981848860317 cmakebuild owrx_connector 45ec227b38bb763b0a923a1856740f4ddf74216c
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,22 +18,24 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb" STATIC_PACKAGES="libusb-1.0-0"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/airspy/airspyone_host.git git clone https://github.com/airspy/airspyone_host.git
cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3 cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3
git clone https://github.com/pothosware/SoapyAirspy.git git clone https://github.com/pothosware/SoapyAirspy.git
cmakebuild SoapyAirspy 99756be5c3413a2d447baf70cb5a880662452655 cmakebuild SoapyAirspy 10d697b209e7f1acc8b2c8d24851d46170ef77e3
git clone https://github.com/airspy/airspyhf.git git clone https://github.com/airspy/airspyhf.git
cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475 cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475
git clone https://github.com/pothosware/SoapyAirspyHF.git git clone https://github.com/pothosware/SoapyAirspyHF.git
cmakebuild SoapyAirspyHF 54f5487dd96207540b2dd562ff9e718e0588770b cmakebuild SoapyAirspyHF 81ca737bb044dd930a9de738bced1e4915491f1b
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,17 +18,22 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb fftw udev" STATIC_PACKAGES="libusb-1.0-0 libfftw3-3 udev"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/mossmann/hackrf.git git clone https://github.com/mossmann/hackrf.git
cd hackrf cd hackrf
git checkout 06eb9192cd348083f5f7de9c0da9ead276020011 git checkout 43e6f99fe8543094d18ff3a6550ed2066c398862
cmakebuild host cmakebuild host
cd .. cd ..
rm -rf hackrf rm -rf hackrf
apk del .build-deps git clone https://github.com/pothosware/SoapyHackRF.git
cmakebuild SoapyHackRF 3c514cecd3dd2fdf4794aebc96c482940c11d7ff
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,31 @@
#!/usr/bin/env bash
set -euo pipefail
export MAKEFLAGS="-j4"
cd /tmp
STATIC_PACKAGES="libusb-1.0-0 libatomic1"
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
SIMD_FLAGS=""
if [[ 'x86_64' == `uname -m` ]] ; then
SIMD_FLAGS="-DDEFAULT_SIMD_FLAGS=SSE3"
fi
git clone https://github.com/myriadrf/LimeSuite.git
cd LimeSuite
git checkout 0854a51ec06b30b01f19a562149c39461e92f24d
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}
make
make install
cd ../..
rm -rf LimeSuite
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euxo pipefail
export MAKEFLAGS="-j4"
cd /tmp
STATIC_PACKAGES="libusb-1.0-0 libudev1"
BUILD_PACKAGES="git make gcc autoconf automake libtool libusb-1.0-0-dev xxd"
apt-get update
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
./bootstrap.sh
./configure
make
make install
ldconfig /etc/ld.so.conf.d
cd ..
rm -rf libperseus-sdr
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake .. ${3:-}
make
make install
cd ../..
rm -rf $1
}
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"
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
git clone https://github.com/analogdevicesinc/libad9361-iio.git
cmakebuild libad9361-iio 8ac95f3325c18c2e34cd9cfd49c7b63d69a0a9d2
git clone https://github.com/pothosware/SoapyPlutoSDR.git
cmakebuild SoapyPlutoSDR c88b7f5bac1e5785f212f9a7c6ce8fef64eb719e
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,16 +18,18 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb" STATIC_PACKAGES="libusb-1.0-0"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/osmocom/rtl-sdr.git git clone https://github.com/osmocom/rtl-sdr.git
cmakebuild rtl-sdr b5af355b1d833b3c898a61cf1e072b59b0ea3440 cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
git clone https://github.com/pothosware/SoapyRTLSDR.git git clone https://github.com/pothosware/SoapyRTLSDR.git
cmakebuild SoapyRTLSDR 5c5d9503337c6d1c34b496dec6f908aab9478b0f cmakebuild SoapyRTLSDR 8ba18f17d64005e43ff2a4e46611f8c710b05007
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,13 +18,15 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb" STATIC_PACKAGES="libusb-1.0.0"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" BUILD_PACKAGES="git libusb-1.0.0-dev cmake make gcc g++ pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/osmocom/rtl-sdr.git git clone https://github.com/osmocom/rtl-sdr.git
cmakebuild rtl-sdr b5af355b1d833b3c898a61cf1e072b59b0ea3440 cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,23 +18,23 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb udev" STATIC_PACKAGES="libusb-1.0.0 udev"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
ARCH=$(uname -m) ARCH=$(uname -m)
case $ARCH in case $ARCH in
x86_64) x86_64)
BINARY=SDRplay_RSP_API-Linux-2.13.1.run BINARY=SDRplay_RSP_API-Linux-3.07.1.run
;; ;;
armv*) armv*)
BINARY=SDRplay_RSP_API-RPi-2.13.1.run BINARY=SDRplay_RSP_API-ARM32-3.07.2.run
;; ;;
aarch64) aarch64)
BINARY=SDRplay_RSP_API-ARM64-2.13.1.run BINARY=SDRplay_RSP_API-ARM64-3.07.1.run
;; ;;
esac esac
@ -47,7 +48,9 @@ cd ..
rm -rf sdrplay rm -rf sdrplay
rm $BINARY rm $BINARY
git clone https://github.com/pothosware/SoapySDRPlay.git git clone https://github.com/SDRplay/SoapySDRPlay.git
cmakebuild SoapySDRPlay 14ec39e4ff0dab7ae7fdf1afbbd2d28b49b0ffae cmakebuild SoapySDRPlay 1c2728a04db5edf8154d02f5cca87e655152d7c1
apk del .build-deps 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 @@
#!/usr/bin/env 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="avahi-daemon libavahi-client3"
BUILD_PACKAGES="git cmake make gcc g++ libavahi-client-dev"
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
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,13 +18,15 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="udev" STATIC_PACKAGES="libudev1"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/pothosware/SoapySDR git clone https://github.com/pothosware/SoapySDR
cmakebuild SoapySDR f722f9ce5b629c3c44401a9bf628b3f8e67a9695 cmakebuild SoapySDR f722f9ce5b629c3c44401a9bf628b3f8e67a9695
apk del .build-deps SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -8,7 +9,7 @@ function cmakebuild() {
fi fi
mkdir build mkdir build
cd build cd build
cmake .. cmake ${CMAKE_ARGS:-} ..
make make
make install make install
cd ../.. cd ../..
@ -17,18 +18,46 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools alsa-lib" 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="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers" 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"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install auto-apt-proxy
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
case `uname -m` in
arm*)
PLATFORM=armhf
;;
aarch64*)
PLATFORM=aarch64
;;
x86_64*)
PLATFORM=amd64
;;
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 git clone https://git.code.sf.net/p/itpp/git itpp
cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d
git clone https://github.com/jketterl/csdr.git git clone https://github.com/jketterl/csdr.git
cd csdr cd csdr
git checkout 43c36df5dcd92d3bdb322f9d53f99ca0c7c816a4 git checkout c4d8a8a5590898e8c9e94b88b96a2fdc7cd0493a
autoreconf -i
./configure
make make
make install make install
cd .. cd ..
@ -38,20 +67,33 @@ git clone https://github.com/szechyjs/mbelib.git
cmakebuild mbelib 9a04ed5c78176a9965f3d43f7aa1b1f5330e771f cmakebuild mbelib 9a04ed5c78176a9965f3d43f7aa1b1f5330e771f
git clone https://github.com/jketterl/digiham.git git clone https://github.com/jketterl/digiham.git
cmakebuild digiham e5e11ce9611e3d8f5f9dce7dee97f86a31af107c cmakebuild digiham 95206501be89b38d0267bf6c29a6898e7c65656f
git clone https://github.com/f4exb/dsd.git git clone https://github.com/f4exb/dsd.git
cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7 cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7
JS8CALL_VERSION=2.1.1
JS8CALL_DIR=js8call-${JS8CALL_VERSION}
JS8CALL_TGZ=${JS8CALL_DIR}.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}
rm ${JS8CALL_TGZ}
WSJT_DIR=wsjtx-2.1.2 WSJT_DIR=wsjtx-2.1.2
WSJT_TGZ=${WSJT_DIR}.tgz WSJT_TGZ=${WSJT_DIR}.tgz
wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
tar xvfz $WSJT_TGZ tar xfz ${WSJT_TGZ}
cmakebuild $WSJT_DIR patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch
mv /wsjtx.patch ${WSJT_DIR}
cmakebuild ${WSJT_DIR}
rm ${WSJT_TGZ}
git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git
cd direwolf cd direwolf
patch -Np1 < /direwolf-1.5.patch
make make
make install make install
cd .. cd ..
@ -62,4 +104,6 @@ pushd /opt/aprs-symbols
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802 git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
popd popd
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,40 +0,0 @@
--- sdrplay/install_lib.sh 2018-06-21 18:47:08.000000000 +0000
+++ sdrplay/install_lib_patched.sh 2019-12-15 01:49:49.477386963 +0000
@@ -3,19 +3,7 @@
echo "Installing SDRplay RSP API library 2.13..."
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
-
-export ARCH=`arch`
+export ARCH=`uname -m`
export VERS="2.13"
echo "Architecture: ${ARCH}"
@@ -63,16 +51,6 @@
echo " "
exit 1
fi
-
-if /sbin/ldconfig -p | /bin/fgrep -q libusb-1.0; then
- echo "Libusb found, continuing..."
-else
- echo " "
- echo "ERROR: Libusb cannot be found. Please install libusb and then run"
- echo "the installer again. Libusb can be installed from http://libusb.info"
- echo " "
- exit 1
-fi
sudo ldconfig

View File

@ -1,40 +0,0 @@
--- sdrplay/install_lib.sh
+++ sdrplay/install_lib_patched.sh
@@ -3,19 +3,7 @@
echo "Installing SDRplay RSP API library 2.13..."
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
-
-export ARCH=`arch`
+export ARCH=`uname -m`
export VERS="2.13"
echo "Architecture: ${ARCH}"
@@ -60,16 +48,6 @@
echo "ERROR: udev rules directory not found, add udev support and run the"
echo "installer again. udev support can be added by running..."
echo "sudo apt-get install libudev-dev"
- echo " "
- exit 1
-fi
-
-if /sbin/ldconfig -p | /bin/fgrep -q libusb-1.0; then
- echo "Libusb found, continuing..."
-else
- echo " "
- echo "ERROR: Libusb cannot be found. Please install libusb and then run"
- echo "the installer again. Libusb can be installed from http://libusb.info"
echo " "
exit 1
fi

View File

@ -1,40 +0,0 @@
--- sdrplay/install_lib.sh 2018-06-21 01:57:02.000000000 +0200
+++ sdrplay/install_lib_patched.sh 2019-01-22 17:21:06.445804136 +0100
@@ -2,19 +2,7 @@
echo "Installing SDRplay RSP API library 2.13..."
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
-
-export ARCH=`arch`
+export ARCH=`uname -m`
export VERS="2.13"
echo "Architecture: ${ARCH}"
@@ -60,16 +48,6 @@
echo " "
exit 1
fi
-
-if /sbin/ldconfig -p | /bin/fgrep -q libusb-1.0; then
- echo "Libusb found, continuing..."
-else
- echo " "
- echo "ERROR: Libusb cannot be found. Please install libusb and then run"
- echo "the installer again. Libusb can be installed from http://libusb.info"
- echo " "
- exit 1
-fi
#echo "Installing SoapySDRPlay..."

View File

@ -12,6 +12,9 @@ fi
if [[ ! -f /etc/openwebrx/bookmarks.json ]] ; then if [[ ! -f /etc/openwebrx/bookmarks.json ]] ; then
cp bookmarks.json /etc/openwebrx/ cp bookmarks.json /etc/openwebrx/
fi fi
if [[ ! -f /etc/openwebrx/users.json ]] ; then
cp users.json /etc/openwebrx/
fi
_term() { _term() {

14
htdocs/css/admin.css Normal file
View File

@ -0,0 +1,14 @@
@import url("openwebrx-header.css");
@import url("openwebrx-globals.css");
.buttons {
text-align: right;
}
.row .map-input {
margin: 15px 15px 0;
}
.device {
margin-top: 20px;
}

12
htdocs/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,7 @@
@import url("openwebrx-header.css"); @import url("openwebrx-header.css");
@import url("openwebrx-globals.css"); @import url("openwebrx-globals.css");
/* expandable photo not implemented on features page */
#webrx-top-photo-clip {
max-height: 67px;
}
h1 { h1 {
text-align: center; text-align: center;
margin: 50px 0; margin: 50px 0;
} }

24
htdocs/css/login.css Normal file
View File

@ -0,0 +1,24 @@
@import url("openwebrx-header.css");
@import url("openwebrx-globals.css");
.login {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 500px;
padding: 20px;
border-radius: 10px;
border: 1px solid #575757;
box-shadow: 0 0 20px #000;
}
.login .btn {
width: 100%;
}
.btn-login {
height: 50px;
}

View File

@ -1,11 +1,6 @@
@import url("openwebrx-header.css"); @import url("openwebrx-header.css");
@import url("openwebrx-globals.css"); @import url("openwebrx-globals.css");
/* expandable photo not implemented on map page */
#webrx-top-photo-clip {
max-height: 67px;
}
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -2,6 +2,7 @@
{ {
position: relative; position: relative;
z-index:1000; z-index:1000;
background-color: #575757;
} }
#webrx-top-photo #webrx-top-photo
@ -13,7 +14,8 @@
#webrx-top-photo-clip #webrx-top-photo-clip
{ {
min-height: 67px; min-height: 67px;
max-height: 350px; max-height: 67px;
height: 350px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
@ -41,18 +43,21 @@
right: 0; right: 0;
} }
#webrx-tob-container, #webrx-top-container * {
line-height: initial;
box-sizing: initial;
}
#webrx-top-container img {
vertical-align: initial;
}
#webrx-top-logo #webrx-top-logo
{ {
padding: 12px; padding: 12px;
float: left; float: left;
} }
#webrx-ha5kfu-top-logo
{
float: right;
padding: 15px;
}
#webrx-rx-avatar #webrx-rx-avatar
{ {
background-color: rgba(154, 154, 154, .5); background-color: rgba(154, 154, 154, .5);
@ -107,46 +112,38 @@
cursor:pointer; cursor:pointer;
position: absolute; position: absolute;
left: 470px; left: 470px;
top: 51px; top: 55px;
} }
#openwebrx-rx-details-arrow a #openwebrx-rx-details-arrow a
{ {
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 0;
display: block;
} }
#openwebrx-rx-details-arrow-down #openwebrx-main-buttons .button {
{ display: block;
display:none; width: 55px;
}
#openwebrx-main-buttons ul
{
display: table;
margin:0;
}
#openwebrx-main-buttons ul li
{
display: table-cell;
padding-left: 5px;
padding-right: 5px;
cursor:pointer; cursor:pointer;
} }
#openwebrx-main-buttons .button img {
height: 38px;
}
#openwebrx-main-buttons a { #openwebrx-main-buttons a {
color: inherit; color: inherit;
text-decoration: inherit; text-decoration: inherit;
} }
#openwebrx-main-buttons li:hover #openwebrx-main-buttons .button:hover
{ {
background-color: rgba(255, 255, 255, 0.3); background-color: rgba(255, 255, 255, 0.3);
} }
#openwebrx-main-buttons li:active #openwebrx-main-buttons .button:active
{ {
background-color: rgba(255, 255, 255, 0.55); background-color: rgba(255, 255, 255, 0.55);
} }
@ -154,6 +151,9 @@
#openwebrx-main-buttons #openwebrx-main-buttons
{ {
padding: 5px 15px;
display: flex;
list-style: none;
float: right; float: right;
margin:0; margin:0;
color: white; color: white;

View File

@ -150,6 +150,10 @@ input[type=range]:focus::-ms-fill-upper
background: #B6B6B6; background: #B6B6B6;
} }
input[type=range]:disabled {
opacity: 0.5;
}
#webrx-page-container #webrx-page-container
{ {
height: 100%; height: 100%;
@ -311,7 +315,7 @@ input[type=range]:focus::-ms-fill-upper
font-style: normal; font-style: normal;
} }
#webrx-actual-freq { .webrx-actual-freq {
width: 100%; width: 100%;
text-align: left; text-align: left;
padding: 0; padding: 0;
@ -320,11 +324,11 @@ input[type=range]:focus::-ms-fill-upper
flex-direction: row; flex-direction: row;
} }
#webrx-actual-freq > * { .webrx-actual-freq > * {
flex: 1; flex: 1;
} }
#webrx-actual-freq input { .webrx-actual-freq input {
font-family: 'roboto-mono'; font-family: 'roboto-mono';
width: 0; width: 0;
box-sizing: border-box; box-sizing: border-box;
@ -334,14 +338,13 @@ input[type=range]:focus::-ms-fill-upper
color: inherit; color: inherit;
} }
#webrx-actual-freq, #webrx-actual-freq input { .webrx-actual-freq, .webrx-actual-freq input {
font-size: 16pt; font-size: 16pt;
font-family: 'roboto-mono'; font-family: 'roboto-mono';
line-height: 22px; line-height: 22px;
} }
#webrx-mouse-freq .webrx-mouse-freq {
{
width: 100%; width: 100%;
text-align: left; text-align: left;
font-size: 10pt; font-size: 10pt;
@ -381,6 +384,7 @@ input[type=range]:focus::-ms-fill-upper
border-radius: 15px; border-radius: 15px;
-moz-border-radius: 15px; -moz-border-radius: 15px;
margin: 5.9px; margin: 5.9px;
box-sizing: content-box;
} }
.openwebrx-panel a .openwebrx-panel a
@ -435,6 +439,10 @@ input[type=range]:focus::-ms-fill-upper
margin-right: 0; margin-right: 0;
} }
.openwebrx-button.disabled {
opacity: 0.5;
}
.openwebrx-demodulator-button .openwebrx-demodulator-button
{ {
width: 38px; width: 38px;
@ -445,6 +453,10 @@ input[type=range]:focus::-ms-fill-upper
margin-right: 5px; margin-right: 5px;
} }
.openwebrx-demodulator-button.same-mod {
color: #FFC;
}
.openwebrx-square-button img .openwebrx-square-button img
{ {
height: 27px; height: 27px;
@ -723,8 +735,7 @@ img.openwebrx-mirror-img
color: White; color: White;
} }
#openwebrx-secondary-demod-listbox .openwebrx-secondary-demod-listbox {
{
width: 173px; width: 173px;
height: 27px; height: 27px;
padding-left:3px; padding-left:3px;
@ -923,37 +934,23 @@ img.openwebrx-mirror-img
display: inline-block; display: inline-block;
} }
#openwebrx-panel-wsjt-message, .openwebrx-message-panel {
#openwebrx-panel-packet-message,
#openwebrx-panel-pocsag-message
{
height: 180px; height: 180px;
} }
#openwebrx-panel-wsjt-message tbody, .openwebrx-message-panel tbody {
#openwebrx-panel-packet-message tbody,
#openwebrx-panel-pocsag-message tbody
{
display: block; display: block;
overflow: auto; overflow: auto;
height: 150px; height: 150px;
width: 100%; width: 100%;
} }
#openwebrx-panel-wsjt-message thead tr, .openwebrx-message-panel thead tr {
#openwebrx-panel-packet-message thead tr,
#openwebrx-panel-pocsag-message thead tr
{
display: block; display: block;
} }
#openwebrx-panel-wsjt-message th, .openwebrx-message-panel th,
#openwebrx-panel-wsjt-message td, .openwebrx-message-panel td {
#openwebrx-panel-packet-message th,
#openwebrx-panel-packet-message td,
#openwebrx-panel-pocsag-message th,
#openwebrx-panel-pocsag-message td
{
width: 50px; width: 50px;
text-align: left; text-align: left;
padding: 1px 3px; padding: 1px 3px;
@ -972,6 +969,31 @@ img.openwebrx-mirror-img
width: 70px; width: 70px;
} }
#openwebrx-panel-js8-message .message {
width: 465px;
max-width: 465px;
}
#openwebrx-panel-js8-message td.message {
white-space: nowrap;
overflow: hidden;
display: flex;
flex-direction: row-reverse;
}
#openwebrx-panel-js8-message .message div {
flex: 1;
}
#openwebrx-panel-js8-message .decimal {
text-align: right;
width: 35px;
}
#openwebrx-panel-js8-message .decimal.freq {
width: 70px;
}
#openwebrx-panel-packet-message .message { #openwebrx-panel-packet-message .message {
width: 410px; width: 410px;
max-width: 410px; max-width: 410px;
@ -1078,13 +1100,15 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel
{ {
display: none; display: none;
} }
@ -1095,7 +1119,8 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container
{ {
height: 200px; height: 200px;
margin: -10px; margin: -10px;

View File

@ -1,10 +1,12 @@
<HTML><HEAD> <HTML><HEAD>
<TITLE>OpenWebRX Feature report</TITLE> <TITLE>OpenWebRX Feature report</TITLE>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<link rel="stylesheet" href="static/css/features.css"> <link rel="stylesheet" href="static/css/features.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script>
<script src="static/lib/jquery-3.2.1.min.js"></script> <script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
<script src="static/features.js"></script> <script src="static/features.js"></script>
</HEAD><BODY> </HEAD><BODY>
${header} ${header}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,25 +1,23 @@
<div id="webrx-top-container"> <div id="webrx-top-container">
<div id="webrx-top-photo-clip"> <div id="webrx-top-photo-clip">
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/> <img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo" alt="Receiver panorama"/>
<div id="webrx-top-bar" class="webrx-top-bar-parts"> <div id="webrx-top-bar" class="webrx-top-bar-parts">
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a> <a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a>
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a> <img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png"/>
<div id="webrx-rx-texts"> <div id="webrx-rx-texts">
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div> <div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div> <div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
</div> </div>
<div id="openwebrx-rx-details-arrow"> <div id="openwebrx-rx-details-arrow">
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a> <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-down" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a>
</div> </div>
<section id="openwebrx-main-buttons"> <section id="openwebrx-main-buttons">
<ul> <div class="button" data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" alt="Status"/><br/>Status</div>
<li data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" /><br/>Status</li> <div class="button" data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" alt="Log"/><br/>Log</div>
<li data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" /><br/>Log</li> <div class="button" data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" alt="Receiver"/><br/>Receiver</div>
<li data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li> <a class="button" href="map" target="openwebrx-map"><img src="static/gfx/openwebrx-panel-map.png" alt="Map"/><br/>Map</a>
<li><a href="map" target="_blank"><img src="static/gfx/openwebrx-panel-map.png" /><br/>Map</a></li> ${settingslink}
</ul>
</section> </section>
</div> </div>
<div id="webrx-rx-photo-title"></div> <div id="webrx-rx-photo-title"></div>

View File

@ -24,14 +24,7 @@
<head> <head>
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title> <title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<script src="static/openwebrx.js"></script> <script src="compiled/receiver.js"></script>
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/jquery.nanoscroller.js"></script>
<script src="static/lib/BookmarkBar.js"></script>
<script src="static/lib/AudioEngine.js"></script>
<script src="static/lib/ProgressBar.js"></script>
<script src="static/lib/Measurement.js"></script>
<script src="static/lib/FrequencyDisplay.js"></script>
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" /> <link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" /> <link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
<meta charset="utf-8"> <meta charset="utf-8">
@ -52,10 +45,6 @@
</div> </div>
<div id="openwebrx-panels-container"> <div id="openwebrx-panels-container">
<div id="openwebrx-panels-container-left"> <div id="openwebrx-panels-container-left">
<div class="openwebrx-panel" data-panel-name="client-under-devel" style="width: 245px; background-color: Red;">
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
<br />We're working on the code right now, so the application might fail.
</div>
<div class="openwebrx-panel" id="openwebrx-panel-digimodes" style="display: none; width: 619px;" data-panel-name="digimodes"> <div class="openwebrx-panel" id="openwebrx-panel-digimodes" style="display: none; width: 619px;" data-panel-name="digimodes">
<div id="openwebrx-digimode-canvas-container"> <div id="openwebrx-digimode-canvas-container">
<div id="openwebrx-digimode-select-channel"></div> <div id="openwebrx-digimode-select-channel"></div>
@ -67,7 +56,7 @@
</div> </div>
</div> </div>
</div> </div>
<table class="openwebrx-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message"> <table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message">
<thead><tr> <thead><tr>
<th>UTC</th> <th>UTC</th>
<th class="decimal">dB</th> <th class="decimal">dB</th>
@ -77,7 +66,15 @@
</tr></thead> </tr></thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
<table class="openwebrx-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"> <table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message">
<thead><tr>
<th>UTC</th>
<th class="decimal freq">Freq</th>
<th class="message">Message</th>
</tr></thead>
<tbody></tbody>
</table>
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message">
<thead><tr> <thead><tr>
<th>UTC</th> <th>UTC</th>
<th class="callsign">Callsign</th> <th class="callsign">Callsign</th>
@ -86,7 +83,7 @@
</tr></thead> </tr></thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
<table class="openwebrx-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"> <table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message">
<thead><tr> <thead><tr>
<th class="address">Address</th> <th class="address">Address</th>
<th class="message">Message</th> <th class="message">Message</th>
@ -126,26 +123,30 @@
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll"> <div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
<div class="nano-content"> <div class="nano-content">
<div id="openwebrx-client-log-title">OpenWebRX client log</div> <div id="openwebrx-client-log-title">OpenWebRX client log</div>
<div>Author contact: <a href="http://www.justjakob.de/" target="_blank">Jakob Ketterl, DD5JFK</a></div> <div>
Author contact: <a href="http://www.justjakob.de/" target="_blank">Jakob Ketterl, DD5JFK</a> |
<a href="https://www.openwebrx.de" target="_blank">OpenWebRX homepage</a>
</div>
<div>Support and information: <a href="https://groups.io/g/openwebrx" target="_blank">Groups.io Mailinglist</a></div>
<div id="openwebrx-debugdiv"></div> <div id="openwebrx-debugdiv"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" style="width: 615px;" data-panel-transparent="true"> <div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" style="width: 615px;" data-panel-transparent="true">
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer"> <span class="openwebrx-progressbar-text">Audio buffer [0 ms]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer" data-type="audiobuffer"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output"> <span class="openwebrx-progressbar-text">Audio output [0 sps]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-audio-output" data-type="audiooutput"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed"> <span class="openwebrx-progressbar-text">Audio stream [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed" data-type="audiospeed"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed"> <span class="openwebrx-progressbar-text">Network usage [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-network-speed" data-type="networkspeed"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu" data-type="cpu"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-clients" data-type="clients"></div>
</div> </div>
</div> </div>
<div id="openwebrx-panels-container-right"> <div id="openwebrx-panels-container-right">
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" style="width: 259px;"> <div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" style="width: 259px;">
<div class="openwebrx-panel-line frequencies-container"> <div class="openwebrx-panel-line frequencies-container">
<div class="frequencies"> <div class="frequencies">
<div id="webrx-actual-freq"></div> <div class="webrx-actual-freq"></div>
<div id="webrx-mouse-freq"></div> <div class="webrx-mouse-freq"></div>
</div> </div>
<div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark..."> <div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark...">
<img src="static/gfx/openwebrx-bookmark.png"> <img src="static/gfx/openwebrx-bookmark.png">
@ -155,47 +156,7 @@
<select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();"> <select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();">
</select> </select>
</div> </div>
<div class="openwebrx-panel-line openwebrx-panel-flex-line"> <div class="openwebrx-modes openwebrx-panel-line"></div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
onclick="demodulator_analog_replace('nfm');">FM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-am"
onclick="demodulator_analog_replace('am');">AM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-lsb"
onclick="demodulator_analog_replace('lsb');">LSB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-usb"
onclick="demodulator_analog_replace('usb');">USB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
onclick="demodulator_analog_replace('cw');">CW</div>
</div>
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dmr"
style="display:none;" data-feature="digital_voice_digiham"
onclick="demodulator_analog_replace('dmr');">DMR</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dstar"
style="display:none;" data-feature="digital_voice_dsd"
onclick="demodulator_analog_replace('dstar');">DStar</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nxdn"
style="display:none;" data-feature="digital_voice_dsd"
onclick="demodulator_analog_replace('nxdn');">NXDN</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-ysf"
style="display:none;" data-feature="digital_voice_digiham"
onclick="demodulator_analog_replace('ysf');">YSF</div>
</div>
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dig" onclick="demodulator_digital_replace_last();">DIG</div>
<select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();">
<option value="none"></option>
<option value="bpsk31">BPSK31</option>
<option value="bpsk63">BPSK63</option>
<option value="ft8" data-feature="wsjt-x">FT8</option>
<option value="wspr" data-feature="wsjt-x">WSPR</option>
<option value="jt65" data-feature="wsjt-x">JT65</option>
<option value="jt9" data-feature="wsjt-x">JT9</option>
<option value="ft4" data-feature="wsjt-x">FT4</option>
<option value="packet" data-feature="packet">Packet</option>
<option value="pocsag" data-feature="pocsag">Pocsag</option>
</select>
</div>
<div class="openwebrx-panel-line"> <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();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></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()"> <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()">
@ -203,8 +164,8 @@
<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()"> <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>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><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"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()"> <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()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></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()"> <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>
@ -249,17 +210,7 @@
</div> </div>
<div class="form-field"> <div class="form-field">
<label for="modulation">Modulation:</label> <label for="modulation">Modulation:</label>
<select name="modulation" id="modulation"> <select name="modulation" id="modulation"></select>
<option value="nfm">FM</option>
<option value="am">AM</option>
<option value="usb">USB</option>
<option value="lsb">LSB</option>
<option value="cw">CW</option>
<option value="dmr">DMR</option>
<option value="dstar">D-Star</option>
<option value="nxdn">NXDN</option>
<option value="ysf">YSF</option>
</select>
</div> </div>
<div class="buttons"> <div class="buttons">
<div class="openwebrx-button" data-action="cancel">Cancel</div> <div class="openwebrx-button" data-action="cancel">Cancel</div>

View File

@ -8,12 +8,10 @@ function BookmarkBar() {
var $bookmark = $(e.target).closest('.bookmark'); var $bookmark = $(e.target).closest('.bookmark');
me.$container.find('.bookmark').removeClass('selected'); me.$container.find('.bookmark').removeClass('selected');
var b = $bookmark.data(); var b = $bookmark.data();
if (!b || !b.frequency || (!b.modulation && !b.digital_modulation)) return; if (!b || !b.frequency || !b.modulation) return;
demodulators[0].set_offset_frequency(b.frequency - center_freq); me.getDemodulator().set_offset_frequency(b.frequency - center_freq);
if (b.modulation) { if (b.modulation) {
demodulator_analog_replace(b.modulation); me.getDemodulatorPanel().setMode(b.modulation);
} else if (b.digital_modulation) {
demodulator_digital_replace(b.digital_modulation);
} }
$bookmark.addClass('selected'); $bookmark.addClass('selected');
}); });
@ -104,40 +102,26 @@ BookmarkBar.prototype.render = function(){
}; };
BookmarkBar.prototype.showEditDialog = function(bookmark) { BookmarkBar.prototype.showEditDialog = function(bookmark) {
var $form = this.$dialog.find("form");
if (!bookmark) { if (!bookmark) {
bookmark = { bookmark = {
name: "", name: "",
frequency: center_freq + demodulators[0].offset_frequency, frequency: center_freq + this.getDemodulator().get_offset_frequency(),
modulation: demodulators[0].subtype modulation: this.getDemodulator().get_secondary_demod() || this.getDemodulator().get_modulation()
} }
} }
['name', 'frequency', 'modulation'].forEach(function(key){ this.$dialog.bookmarkDialog().setValues(bookmark);
$form.find('#' + key).val(bookmark[key]);
});
this.$dialog.data('id', bookmark.id);
this.$dialog.show(); this.$dialog.show();
this.$dialog.find('#name').focus(); this.$dialog.find('#name').focus();
}; };
BookmarkBar.prototype.storeBookmark = function() { BookmarkBar.prototype.storeBookmark = function() {
var me = this; var me = this;
var bookmark = {}; var bookmark = this.$dialog.bookmarkDialog().getValues();
var valid = true; if (!bookmark) return;
['name', 'frequency', 'modulation'].forEach(function(key){
var $input = me.$dialog.find('#' + key);
valid = valid && $input[0].checkValidity();
bookmark[key] = $input.val();
});
if (!valid) {
me.$dialog.find("form :submit").click();
return;
}
bookmark.frequency = Number(bookmark.frequency); bookmark.frequency = Number(bookmark.frequency);
var bookmarks = me.localBookmarks.getBookmarks(); var bookmarks = me.localBookmarks.getBookmarks();
bookmark.id = me.$dialog.data('id');
if (!bookmark.id) { if (!bookmark.id) {
if (bookmarks.length) { if (bookmarks.length) {
bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; })); bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; }));
@ -154,6 +138,14 @@ BookmarkBar.prototype.storeBookmark = function() {
me.$dialog.hide(); me.$dialog.hide();
}; };
BookmarkBar.prototype.getDemodulatorPanel = function() {
return $('#openwebrx-panel-receiver').demodulatorPanel();
};
BookmarkBar.prototype.getDemodulator = function() {
return this.getDemodulatorPanel().getDemodulator();
};
BookmarkLocalStorage = function(){ BookmarkLocalStorage = function(){
}; };
@ -171,7 +163,3 @@ BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
bookmarks = bookmarks.filter(function(b) { return b.id !== data; }); bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
this.setBookmarks(bookmarks); this.setBookmarks(bookmarks);
}; };

View File

@ -0,0 +1,36 @@
$.fn.bookmarkDialog = function() {
var $el = this;
return {
setModes: function(modes) {
$el.find('#modulation').html(modes.filter(function(m){
return m.isAvailable();
}).map(function(m) {
return '<option value="' + m.modulation + '">' + m.name + '</option>';
}).join(''));
return this;
},
setValues: function(bookmark) {
var $form = $el.find('form');
['name', 'frequency', 'modulation'].forEach(function(key){
$form.find('#' + key).val(bookmark[key]);
});
$el.data('id', bookmark.id || false);
return this;
},
getValues: function() {
var bookmark = {};
var valid = true;
['name', 'frequency', 'modulation'].forEach(function(key){
var $input = $el.find('#' + key);
valid = valid && $input[0].checkValidity();
bookmark[key] = $input.val();
});
if (!valid) {
$el.find("form :submit").click();
return;
}
bookmark.id = $el.data('id');
return bookmark;
}
}
}

356
htdocs/lib/Demodulator.js Normal file
View File

@ -0,0 +1,356 @@
function Filter(demodulator) {
this.demodulator = demodulator;
this.min_passband = 100;
}
Filter.prototype.getLimits = function() {
var max_bw;
if (this.demodulator.get_secondary_demod() === 'pocsag') {
max_bw = 12500;
} else {
max_bw = (audioEngine.getOutputRate() / 2) - 1;
}
return {
high: max_bw,
low: -max_bw
};
};
function Envelope(demodulator) {
this.demodulator = demodulator;
this.dragged_range = Demodulator.draggable_ranges.none;
}
Envelope.prototype.draw = function(visible_range){
this.visible_range = visible_range;
var line = center_freq + this.demodulator.offset_frequency;
// ____
// Draws a standard filter envelope like this: _/ \_
// Parameters are given in offset frequency (Hz).
// Envelope is drawn on the scale canvas.
// A "drag range" object is returned, containing information about the draggable areas of the envelope
// (beginning, ending and the line showing the offset frequency).
var env_bounding_line_w = 5; //
var env_att_w = 5; // _______ ___env_h2 in px ___|_____
var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_
var env_h2 = 5; // |||env_att_line_w |_env_lineplus
var env_lineplus = 1; // ||env_bounding_line_w
var env_line_click_area = 6;
//range=get_visible_freq_range();
var from = center_freq + this.demodulator.offset_frequency + this.demodulator.low_cut;
var from_px = scale_px_from_freq(from, range);
var to = center_freq + this.demodulator.offset_frequency + this.demodulator.high_cut;
var to_px = scale_px_from_freq(to, range);
if (to_px < from_px) /* swap'em */ {
var temp_px = to_px;
to_px = from_px;
from_px = temp_px;
}
from_px -= (env_att_w + env_bounding_line_w);
to_px += (env_att_w + env_bounding_line_w);
// do drawing:
scale_ctx.lineWidth = 3;
var color = this.color || '#ffff00'; // yellow
scale_ctx.strokeStyle = color;
scale_ctx.fillStyle = color;
var drag_ranges = {envelope_on_screen: false, line_on_screen: false};
if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen?
{
drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w};
drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px};
drag_ranges.whole_envelope = {x1: from_px, x2: to_px};
drag_ranges.envelope_on_screen = true;
scale_ctx.beginPath();
scale_ctx.moveTo(from_px, env_h1);
scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1);
scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2);
scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2);
scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1);
scale_ctx.lineTo(to_px, env_h1);
scale_ctx.globalAlpha = 0.3;
scale_ctx.fill();
scale_ctx.globalAlpha = 1;
scale_ctx.stroke();
}
if (typeof line !== "undefined") // out of screen?
{
var line_px = scale_px_from_freq(line, range);
if (!(line_px < 0 || line_px > window.innerWidth)) {
drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2};
drag_ranges.line_on_screen = true;
scale_ctx.moveTo(line_px, env_h1 + env_lineplus);
scale_ctx.lineTo(line_px, env_h2 - env_lineplus);
scale_ctx.stroke();
}
}
this.drag_ranges = drag_ranges;
};
Envelope.prototype.drag_start = function(x, key_modifiers){
this.key_modifiers = key_modifiers;
this.dragged_range = this.where_clicked(x, this.drag_ranges, key_modifiers);
this.drag_origin = {
x: x,
low_cut: this.demodulator.low_cut,
high_cut: this.demodulator.high_cut,
offset_frequency: this.demodulator.offset_frequency
};
return this.dragged_range !== Demodulator.draggable_ranges.none;
};
Envelope.prototype.where_clicked = function(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by envelope_draw().
var in_range = function (x, range) {
return range.x1 <= x && range.x2 >= x;
};
var dr = Demodulator.draggable_ranges;
if (key_modifiers.shiftKey) {
//Check first: shift + center drag emulates BFO knob
if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo;
//Check second: shift + envelope drag emulates PBF knob
if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs;
}
if (drag_ranges.envelope_on_screen) {
// For low and high cut:
if (in_range(x, drag_ranges.beginning)) return dr.beginning;
if (in_range(x, drag_ranges.ending)) return dr.ending;
// Last priority: having clicked anything else on the envelope, without holding the shift key
if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else;
}
return dr.none; //User doesn't drag the envelope for this demodulator
};
Envelope.prototype.drag_move = function(x) {
var dr = Demodulator.draggable_ranges;
var new_value;
if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all
var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x));
//dragging the line in the middle of the filter envelope while holding Shift does emulate
//the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged
//Filter passband moves in the opposite direction than dragged, hence the minus below.
var minus = (this.dragged_range === dr.bfo) ? -1 : 1;
//dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob
//(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset
//frequency.
if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) {
//we don't let low_cut go beyond its limits
if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.demodulator.filter.getLimits().low) return true;
//nor the filter passband be too small
if (this.demodulator.high_cut - new_value < this.demodulator.filter.min_passband) return true;
//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
if (new_value >= this.demodulator.high_cut) return true;
this.demodulator.setLowCut(new_value);
}
if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) {
//we don't let high_cut go beyond its limits
if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.demodulator.filter.getLimits().high) return true;
//nor the filter passband be too small
if (new_value - this.demodulator.low_cut < this.demodulator.filter.min_passband) return true;
//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
if (new_value <= this.demodulator.low_cut) return true;
this.demodulator.setHighCut(new_value);
}
if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) {
//when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it)
new_value = this.drag_origin.offset_frequency + freq_change;
if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-)
this.demodulator.set_offset_frequency(new_value);
}
//now do the actual modifications:
//mkenvelopes(this.visible_range);
//this.demodulator.set();
return true;
};
Envelope.prototype.drag_end = function(){
var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset
this.dragged_range = Demodulator.draggable_ranges.none;
return to_return;
};
//******* class Demodulator_default_analog *******
// This can be used as a base for basic audio demodulators.
// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB
function Demodulator(offset_frequency, modulation) {
this.offset_frequency = offset_frequency;
this.envelope = new Envelope(this);
this.color = Demodulator.get_next_color();
this.modulation = modulation;
this.filter = new Filter(this);
this.squelch_level = -150;
this.dmr_filter = 3;
this.started = false;
this.state = {};
this.secondary_demod = false;
var mode = Modes.findByModulation(modulation);
if (mode) {
this.low_cut = mode.bandpass.low_cut;
this.high_cut = mode.bandpass.high_cut;
}
this.listeners = {
"frequencychange": [],
"squelchchange": []
};
}
//ranges on filter envelope that can be dragged:
Demodulator.draggable_ranges = {
none: 0,
beginning: 1 /*from*/,
ending: 2 /*to*/,
anything_else: 3,
bfo: 4 /*line (while holding shift)*/,
pbs: 5
}; //to which parameter these correspond in envelope_draw()
Demodulator.color_index = 0;
Demodulator.colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"];
Demodulator.get_next_color = function() {
if (this.color_index >= this.colors.length) this.color_index = 0;
return (this.colors[this.color_index++]);
}
Demodulator.prototype.on = function(event, handler) {
this.listeners[event].push(handler);
};
Demodulator.prototype.emit = function(event, params) {
this.listeners[event].forEach(function(fn) {
fn(params);
});
};
Demodulator.prototype.set_offset_frequency = function(to_what) {
if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
to_what = Math.round(to_what);
if (this.offset_frequency === to_what) {
return;
}
this.offset_frequency = to_what;
this.set();
this.emit("frequencychange", to_what);
mkenvelopes(get_visible_freq_range());
};
Demodulator.prototype.get_offset_frequency = function() {
return this.offset_frequency;
};
Demodulator.prototype.get_modulation = function() {
return this.modulation;
};
Demodulator.prototype.start = function() {
this.started = true;
this.set();
ws.send(JSON.stringify({
"type": "dspcontrol",
"action": "start"
}));
};
// TODO check if this is actually used
Demodulator.prototype.stop = function() {
};
Demodulator.prototype.send = function(params) {
ws.send(JSON.stringify({"type": "dspcontrol", "params": params}));
}
Demodulator.prototype.set = function () { //this function sends demodulator parameters to the server
if (!this.started) return;
var params = {
"low_cut": this.low_cut,
"high_cut": this.high_cut,
"offset_freq": this.offset_frequency,
"mod": this.modulation,
"dmr_filter": this.dmr_filter,
"squelch_level": this.squelch_level,
"secondary_mod": this.secondary_demod,
"secondary_offset_freq": this.secondary_offset_freq
};
var to_send = {};
for (var key in params) {
if (!(key in this.state) || params[key] !== this.state[key]) {
to_send[key] = params[key];
}
}
if (Object.keys(to_send).length > 0) {
this.send(to_send);
for (var key in to_send) {
this.state[key] = to_send[key];
}
}
mkenvelopes(get_visible_freq_range());
};
Demodulator.prototype.setSquelch = function(squelch) {
if (this.squelch_level == squelch) {
return;
}
this.squelch_level = squelch;
this.set();
this.emit("squelchchange", squelch);
};
Demodulator.prototype.getSquelch = function() {
return this.squelch_level;
};
Demodulator.prototype.setDmrFilter = function(dmr_filter) {
this.dmr_filter = dmr_filter;
this.set();
};
Demodulator.prototype.setBandpass = function(bandpass) {
this.bandpass = bandpass;
this.low_cut = bandpass.low_cut;
this.high_cut = bandpass.high_cut;
this.set();
};
Demodulator.prototype.setLowCut = function(low_cut) {
this.low_cut = low_cut;
this.set();
};
Demodulator.prototype.setHighCut = function(high_cut) {
this.high_cut = high_cut;
this.set();
};
Demodulator.prototype.getBandpass = function() {
return {
low_cut: this.low_cut,
high_cut: this.high_cut
};
};
Demodulator.prototype.set_secondary_demod = function(secondary_demod) {
if (this.secondary_demod === secondary_demod) {
return;
}
this.secondary_demod = secondary_demod;
this.set();
};
Demodulator.prototype.get_secondary_demod = function() {
return this.secondary_demod;
};
Demodulator.prototype.set_secondary_offset_freq = function(secondary_offset) {
if (this.secondary_offset_freq === secondary_offset) {
return;
}
this.secondary_offset_freq = secondary_offset;
this.set();
};

View File

@ -0,0 +1,333 @@
function DemodulatorPanel(el) {
var self = this;
self.el = el;
self.demodulator = null;
self.mode = null;
var displayEl = el.find('.webrx-actual-freq')
this.tuneableFrequencyDisplay = displayEl.tuneableFrequencyDisplay();
displayEl.on('frequencychange', function(event, freq) {
self.getDemodulator().set_offset_frequency(freq - self.center_freq);
});
Modes.registerModePanel(this);
el.on('click', '.openwebrx-demodulator-button', function() {
var modulation = $(this).data('modulation');
if (modulation) {
self.setMode(modulation);
} else {
self.disableDigiMode();
}
});
el.on('change', '.openwebrx-secondary-demod-listbox', function() {
var value = $(this).val();
if (value === 'none') {
self.disableDigiMode();
} else {
self.setMode(value);
}
});
el.on('click', '.openwebrx-squelch-default', function() {
if (!self.squelchAvailable()) return;
el.find('.openwebrx-squelch-slider').val(getLogSmeterValue(smeter_level) + 10);
self.updateSquelch();
});
el.on('change', '.openwebrx-squelch-slider', function() {
self.updateSquelch();
});
window.addEventListener('hashchange', function() {
self.onHashChange();
});
};
DemodulatorPanel.prototype.render = function() {
var available = Modes.getModes().filter(function(m){ return m.isAvailable(); });
var normalModes = available.filter(function(m){ return m.type === 'analog'; });
var digiModes = available.filter(function(m){ return m.type === 'digimode'; });
var html = []
var buttons = normalModes.map(function(m){
return $(
'<div ' +
'class="openwebrx-button openwebrx-demodulator-button" ' +
'data-modulation="' + m.modulation + '" ' +
'id="openwebrx-button-' + m.modulation + '" r' +
'>' + m.name + '</div>'
);
});
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
}));
html.push($(
'<div class="openwebrx-panel-line openwebrx-panel-flex-line">' +
'<div class="openwebrx-button openwebrx-demodulator-button openwebrx-button-dig">DIG</div>' +
'<select class="openwebrx-secondary-demod-listbox">' +
'<option value="none"></option>' +
digiModes.map(function(m){
return '<option value="' + m.modulation + '">' + m.name + '</option>';
}).join('') +
'</select>' +
'</div>'
));
this.el.find(".openwebrx-modes").html(html);
};
DemodulatorPanel.prototype.setMode = function(requestedModulation) {
var mode = Modes.findByModulation(requestedModulation);
if (!mode) {
return;
}
if (this.mode === mode) {
return;
}
if (!mode.isAvailable()) {
divlog('Modulation "' + mode.name + '" not supported. Please check requirements', true);
return;
}
if (mode.type === 'digimode') {
modulation = mode.underlying[0];
} else {
if (this.mode && this.mode.type === 'digimode' && this.mode.underlying.indexOf(requestedModulation) >= 0) {
// keep the mode, just switch underlying modulation
mode = this.mode;
modulation = requestedModulation;
} else {
modulation = mode.modulation;
}
}
var current = this.collectParams();
if (this.demodulator) {
current.offset_frequency = this.demodulator.get_offset_frequency();
current.squelch_level = this.demodulator.getSquelch();
}
this.stopDemodulator();
this.demodulator = new Demodulator(current.offset_frequency, modulation);
this.demodulator.setSquelch(current.squelch_level);
var self = this;
var updateFrequency = function(freq) {
self.tuneableFrequencyDisplay.setFrequency(self.center_freq + freq);
self.updateHash();
};
this.demodulator.on("frequencychange", updateFrequency);
updateFrequency(this.demodulator.get_offset_frequency());
var updateSquelch = function(squelch) {
self.el.find('.openwebrx-squelch-slider').val(squelch);
self.updateHash();
};
this.demodulator.on('squelchchange', updateSquelch);
updateSquelch(this.demodulator.getSquelch());
if (mode.type === 'digimode') {
this.demodulator.set_secondary_demod(mode.modulation);
if (mode.bandpass) {
this.demodulator.setBandpass(mode.bandpass);
}
} else {
this.demodulator.set_secondary_demod(false);
}
this.demodulator.start();
this.mode = mode;
this.updateButtons();
this.updatePanels();
this.updateHash();
};
DemodulatorPanel.prototype.disableDigiMode = function() {
// just a little trick to get out of the digimode
delete this.mode;
this.setMode(this.getDemodulator().get_modulation());
};
DemodulatorPanel.prototype.updatePanels = function() {
var modulation = this.getDemodulator().get_secondary_demod();
$('#openwebrx-panel-digimodes').attr('data-mode', modulation);
toggle_panel("openwebrx-panel-digimodes", !!modulation);
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(modulation) >= 0);
toggle_panel("openwebrx-panel-js8-message", modulation == "js8");
toggle_panel("openwebrx-panel-packet-message", modulation === "packet");
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");
modulation = this.getDemodulator().get_modulation();
var showing = 'openwebrx-panel-metadata-' + modulation;
$(".openwebrx-meta-panel").each(function (_, p) {
toggle_panel(p.id, p.id === showing);
});
clear_metadata();
};
DemodulatorPanel.prototype.getDemodulator = function() {
return this.demodulator;
};
DemodulatorPanel.prototype.collectParams = function() {
var defaults = {
offset_frequency: 0,
squelch_level: -150,
mod: 'nfm'
}
return $.extend(new Object(), defaults, this.initialParams || {}, this.transformHashParams(this.parseHash()));
};
DemodulatorPanel.prototype.startDemodulator = function() {
if (!Modes.initComplete()) return;
var params = this.collectParams();
this._apply(params);
};
DemodulatorPanel.prototype.stopDemodulator = function() {
if (!this.demodulator) {
return;
}
this.demodulator.stop();
this.demodulator = null;
this.mode = null;
}
DemodulatorPanel.prototype._apply = function(params) {
this.setMode(params.mod);
this.getDemodulator().set_offset_frequency(params.offset_frequency);
this.getDemodulator().setSquelch(params.squelch_level);
this.updateButtons();
};
DemodulatorPanel.prototype.setInitialParams = function(params) {
this.initialParams = params;
};
DemodulatorPanel.prototype.onHashChange = function() {
this._apply(this.transformHashParams(this.parseHash()));
};
DemodulatorPanel.prototype.transformHashParams = function(params) {
var ret = {
mod: params.secondary_mod || params.mod
};
if (typeof(params.offset_frequency) !== 'undefined') ret.offset_frequency = params.offset_frequency;
if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql);
return ret;
};
DemodulatorPanel.prototype.squelchAvailable = function () {
return this.mode && this.mode.squelch;
}
DemodulatorPanel.prototype.updateButtons = function() {
var $buttons = this.el.find(".openwebrx-demodulator-button");
$buttons.removeClass("highlighted").removeClass('same-mod');
var demod = this.getDemodulator()
if (!demod) return;
this.el.find('[data-modulation=' + demod.get_modulation() + ']').addClass("highlighted");
var secondary_demod = demod.get_secondary_demod()
if (secondary_demod) {
this.el.find(".openwebrx-button-dig").addClass("highlighted");
this.el.find('.openwebrx-secondary-demod-listbox').val(secondary_demod);
var mode = Modes.findByModulation(secondary_demod);
if (mode) {
var self = this;
mode.underlying.filter(function(m) {
return m !== demod.get_modulation();
}).forEach(function(m) {
self.el.find('[data-modulation=' + m + ']').addClass('same-mod')
});
}
} else {
this.el.find('.openwebrx-secondary-demod-listbox').val('none');
}
var squelch_disabled = !this.squelchAvailable();
this.el.find('.openwebrx-squelch-slider').prop('disabled', squelch_disabled);
this.el.find('.openwebrx-squelch-default')[squelch_disabled ? 'addClass' : 'removeClass']('disabled');
}
DemodulatorPanel.prototype.setCenterFrequency = function(center_freq) {
if (this.center_freq === center_freq) {
return;
}
this.stopDemodulator();
this.center_freq = center_freq;
this.startDemodulator();
};
DemodulatorPanel.prototype.parseHash = function() {
if (!window.location.hash) {
return {};
}
var params = window.location.hash.substring(1).split(",").map(function(x) {
var harr = x.split('=');
return [harr[0], harr.slice(1).join('=')];
}).reduce(function(params, p){
params[p[0]] = p[1];
return params;
}, {});
return this.validateHash(params);
};
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 true;
}).reduce(function(p, key) {
p[key] = params[key];
return p;
}, {});
if (params['freq']) {
params['offset_frequency'] = params['freq'] - self.center_freq;
delete params['freq'];
}
return params;
};
DemodulatorPanel.prototype.updateHash = function() {
var demod = this.getDemodulator();
if (!demod) return;
var self = this;
window.location.hash = $.map({
freq: demod.get_offset_frequency() + self.center_freq,
mod: demod.get_modulation(),
secondary_mod: demod.get_secondary_demod(),
sql: demod.getSquelch(),
}, function(value, key){
if (typeof(value) === 'undefined' || value === false) return undefined;
return key + '=' + value;
}).filter(function(v) {
return !!v;
}).join(',');
};
DemodulatorPanel.prototype.updateSquelch = function() {
var sliderValue = parseInt(this.el.find(".openwebrx-squelch-slider").val());
var demod = this.getDemodulator();
if (demod) demod.setSquelch(sliderValue);
};
$.fn.demodulatorPanel = function(){
if (!this.data('panel')) {
this.data('panel', new DemodulatorPanel(this));
};
return this.data('panel');
};

View File

@ -50,7 +50,6 @@ TuneableFrequencyDisplay.prototype.setupElements = function() {
TuneableFrequencyDisplay.prototype.setupEvents = function() { TuneableFrequencyDisplay.prototype.setupEvents = function() {
var me = this; var me = this;
me.listeners = [];
me.element.on('wheel', function(e){ me.element.on('wheel', function(e){
e.preventDefault(); e.preventDefault();
@ -63,17 +62,13 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
if (e.originalEvent.deltaY > 0) delta *= -1; if (e.originalEvent.deltaY > 0) delta *= -1;
var newFrequency = me.frequency + delta; var newFrequency = me.frequency + delta;
me.listeners.forEach(function(l) { me.element.trigger('frequencychange', newFrequency);
l(newFrequency);
});
}); });
var submit = function(){ var submit = function(){
var freq = parseInt(me.input.val()); var freq = parseInt(me.input.val());
if (!isNaN(freq)) { if (!isNaN(freq)) {
me.listeners.forEach(function(l) { me.element.trigger('frequencychange', freq);
l(freq);
});
} }
me.input.hide(); me.input.hide();
me.displayContainer.show(); me.displayContainer.show();
@ -96,6 +91,16 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
}); });
}; };
TuneableFrequencyDisplay.prototype.onFrequencyChange = function(listener){ $.fn.frequencyDisplay = function() {
this.listeners.push(listener); if (!this.data('frequencyDisplay')) {
}; this.data('frequencyDisplay', new FrequencyDisplay(this));
}
return this.data('frequencyDisplay');
}
$.fn.tuneableFrequencyDisplay = function() {
if (!this.data('frequencyDisplay')) {
this.data('frequencyDisplay', new TuneableFrequencyDisplay(this));
}
return this.data('frequencyDisplay');
}

77
htdocs/lib/Header.js Normal file
View File

@ -0,0 +1,77 @@
function Header(el) {
this.el = el;
this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () {
toggle_panel($(this).data('toggle-panel'));
});
this.init_rx_photo();
this.download_details();
};
Header.prototype.setDetails = function(details) {
this.el.find('#webrx-rx-title').html(details['receiver_name']);
var query = encodeURIComponent(details['receiver_gps']['lat'] + ',' + details['receiver_gps']['lon']);
this.el.find('#webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m, <a href="https://www.google.com/maps/search/?api=1&query=' + query + '" target="_blank">[maps]</a>');
this.el.find('#webrx-rx-photo-title').html(details['photo_title']);
this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']);
};
Header.prototype.init_rx_photo = function() {
this.rx_photo_state = 0;
$.extend($.easing, {
easeOutCubic:function(x) {
return 1 - Math.pow( 1 - x, 3 );
}
});
$('#webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this));
};
Header.prototype.close_rx_photo = function() {
this.rx_photo_state = 0;
this.el.find("#webrx-rx-photo-desc").animate({opacity: 0});
this.el.find("#webrx-rx-photo-title").animate({opacity: 0});
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'});
this.el.find("#openwebrx-rx-details-arrow-down").show();
this.el.find("#openwebrx-rx-details-arrow-up").hide();
}
Header.prototype.open_rx_photo = function() {
this.rx_photo_state = 1;
this.el.find("#webrx-rx-photo-desc").animate({opacity: 1});
this.el.find("#webrx-rx-photo-title").animate({opacity: 1});
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 350}, {duration: 1000, easing: 'easeOutCubic'});
this.el.find("#openwebrx-rx-details-arrow-down").hide();
this.el.find("#openwebrx-rx-details-arrow-up").show();
}
Header.prototype.toggle_rx_photo = function(ev) {
if (ev && ev.target && ev.target.tagName == 'A') {
return;
}
if (this.rx_photo_state) {
this.close_rx_photo();
} else {
this.open_rx_photo();
}
};
Header.prototype.download_details = function() {
var self = this;
$.ajax('api/receiverdetails').done(function(data){
self.setDetails(data);
});
};
$.fn.header = function() {
if (!this.data('header')) {
this.data('header', new Header(this));
}
return this.data('header');
};
$(function(){
$('#webrx-top-container').header();
});

150
htdocs/lib/Js8Threads.js Normal file
View File

@ -0,0 +1,150 @@
Js8Thread = function(el){
this.messages = [];
this.el = el;
};
Js8Thread.prototype.getAverageFrequency = function(){
var total = this.messages.map(function(message){
return message.freq;
}).reduce(function(t, f){
return t + f;
}, 0);
return total / this.messages.length;
};
Js8Thread.prototype.pushMessage = function(message) {
this.messages.push(message);
this.render();
};
Js8Thread.prototype.render = function() {
this.el.html(
'<td>' + this.renderTimestamp(this.getLatestTimestamp()) + '</td>' +
'<td class="decimal freq">' + Math.round(this.getAverageFrequency()) + '</td>' +
'<td class="message"><div>' + this.renderMessages() + '</div></td>'
);
};
Js8Thread.prototype.getLatestTimestamp = function() {
return this.messages[0].timestamp;
};
Js8Thread.prototype.isOpen = function() {
if (!this.messages.length) return true;
var last_message = this.messages[this.messages.length - 1];
return (last_message.thread_type & 2) === 0;
};
Js8Thread.prototype.renderMessages = function() {
var res = [];
for (var i = 0; i < this.messages.length; i++) {
var msg = this.messages[i];
if (msg.thread_type & 1) {
res.push('[ ');
} else if (i === 0 || msg.timestamp - this.messages[i - 1].timestamp > this.getMessageDuration()) {
res.push(' ... ');
}
res.push(msg.msg);
if (msg.thread_type & 2) {
res.push(' ]');
} else if (i === this.messages.length -1) {
res.push(' ... ');
}
}
return res.join('');
};
Js8Thread.prototype.getMessageDuration = function() {
switch (this.getMode()) {
case 'A':
return 15000;
case 'E':
return 30000;
case 'B':
return 10000;
case 'C':
return 6000;
}
};
Js8Thread.prototype.getMode = function() {
// we filter messages by mode, so the first one is as good as any
if (!this.messages.length) return;
return this.messages[0].mode;
};
Js8Thread.prototype.acceptsMode = function(mode) {
var currentMode = this.getMode();
return typeof(currentMode) === 'undefined' || currentMode === mode;
};
Js8Thread.prototype.renderTimestamp = function(timestamp) {
var t = new Date(timestamp);
var pad = function (i) {
return ('' + i).padStart(2, "0");
};
return pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds());
};
Js8Thread.prototype.purgeOldMessages = function() {
var now = new Date().getTime();
this.messages = this.messages.filter(function(m) {
// keep messages around for 20 minutes
return now - m.timestamp < 20 * 60 * 1000;
});
if (!this.messages.length) {
this.el.remove();
} else {
this.render();
}
return this.messages.length;
};
Js8Threader = function(el){
this.threads = [];
this.tbody = $(el).find('tbody');
var me = this;
this.interval = setInterval(function(){
me.purgeOldMessages();
}, 15000);
};
Js8Threader.prototype.purgeOldMessages = function() {
this.threads = this.threads.filter(function(t) {
return t.purgeOldMessages();
});
};
Js8Threader.prototype.findThread = function(freq, mode) {
var matching = this.threads.filter(function(thread) {
// max frequency deviation: 5 Hz. this may be a little tight.
return thread.isOpen() && thread.acceptsMode(mode) && Math.abs(thread.getAverageFrequency() - freq) <= 5;
});
matching.sort(function(a, b){
return b.getLatestTimestamp() - a.getLatestTimestamp();
});
return matching[0] || false;
};
Js8Threader.prototype.pushMessage = function(message) {
var thread;
// only look for exising threads if the message is not a starting message
if ((message.thread_type & 1) === 0) {
thread = this.findThread(message.freq, message.mode);
}
if (!thread) {
var line = $("<tr></tr>");
this.tbody.append(line);
thread = new Js8Thread(line);
this.threads.push(thread);
}
thread.pushMessage(message);
this.tbody.scrollTop(this.tbody[0].scrollHeight);
};
$.fn.js8 = function() {
if (!this.data('threader')) {
this.data('threader', new Js8Threader(this));
}
return this.data('threader');
};

View File

@ -1,4 +1,5 @@
function Measurement() { function Measurement() {
this.reporters = [];
this.reset(); this.reset();
} }
@ -21,10 +22,13 @@ Measurement.prototype.getRate = function() {
Measurement.prototype.reset = function() { Measurement.prototype.reset = function() {
this.value = 0; this.value = 0;
this.start = new Date(); this.start = new Date();
this.reporters.forEach(function(r){ r.reset(); });
}; };
Measurement.prototype.report = function(range, interval, callback) { Measurement.prototype.report = function(range, interval, callback) {
return new Reporter(this, range, interval, callback); var reporter = new Reporter(this, range, interval, callback);
this.reporters.push(reporter);
return reporter;
}; };
function Reporter(measurement, range, interval, callback) { function Reporter(measurement, range, interval, callback) {
@ -59,4 +63,8 @@ Reporter.prototype.report = function(){
var accumulated = newest.value - oldest.value; var accumulated = newest.value - oldest.value;
// we want rate per second, but our time is in milliseconds... compensate by 1000 // we want rate per second, but our time is in milliseconds... compensate by 1000
this.callback(accumulated * 1000 / elapsed); this.callback(accumulated * 1000 / elapsed);
};
Reporter.prototype.reset = function(){
this.samples = [];
}; };

55
htdocs/lib/Modes.js Normal file
View File

@ -0,0 +1,55 @@
var Modes = {
modes: [],
features: {},
panels: [],
setModes:function(json){
this.modes = json.map(function(m){ return new Mode(m); });
this.updatePanels();
$('#openwebrx-dialog-bookmark').bookmarkDialog().setModes(this.modes);
},
getModes:function(){
return this.modes;
},
setFeatures:function(features){
this.features = features;
this.updatePanels();
},
findByModulation:function(modulation){
matches = this.modes.filter(function(m) { return m.modulation === modulation; });
if (matches.length) return matches[0]
},
registerModePanel: function(el) {
this.panels.push(el);
},
initComplete: function() {
return this.modes.length && Object.keys(this.features).length;
},
updatePanels: function() {
this.panels.forEach(function(p) {
p.render();
p.startDemodulator();
});
}
};
var Mode = function(json){
this.modulation = json.modulation;
this.name = json.name;
this.type = json.type;
this.requirements = json.requirements;
this.squelch = json.squelch;
if (json.bandpass) {
this.bandpass = json.bandpass;
}
if (this.type === 'digimode') {
this.underlying = json.underlying;
}
};
Mode.prototype.isAvailable = function(){
return this.requirements.map(function(r){
return Modes.features[r];
}).reduce(function(a, b){
return a && b;
}, true);
};

View File

@ -1,10 +1,15 @@
ProgressBar = function(el) { ProgressBar = function(el) {
this.$el = $(el); this.$el = $(el);
this.$innerText = this.$el.find('.openwebrx-progressbar-text'); this.$innerText = $('<span class="openwebrx-progressbar-text">' + this.getDefaultText() + '</span>');
this.$innerBar = this.$el.find('.openwebrx-progressbar-bar'); this.$innerBar = $('<div class="openwebrx-progressbar-bar"></div>');
this.$el.empty().append(this.$innerText, this.$innerBar);
this.$innerBar.css('width', '0%'); this.$innerBar.css('width', '0%');
}; };
ProgressBar.prototype.getDefaultText = function() {
return '';
}
ProgressBar.prototype.set = function(val, text, over) { ProgressBar.prototype.set = function(val, text, over) {
this.setValue(val); this.setValue(val);
this.setText(text); this.setText(text);
@ -25,13 +30,20 @@ ProgressBar.prototype.setOver = function(over) {
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6"); this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
}; };
AudioBufferProgressBar = function(el, sampleRate) { AudioBufferProgressBar = function(el) {
ProgressBar.call(this, el); ProgressBar.call(this, el);
this.sampleRate = sampleRate;
}; };
AudioBufferProgressBar.prototype = new ProgressBar(); AudioBufferProgressBar.prototype = new ProgressBar();
AudioBufferProgressBar.prototype.getDefaultText = function() {
return 'Audio buffer [0 ms]';
};
AudioBufferProgressBar.prototype.setSampleRate = function(sampleRate) {
this.sampleRate = sampleRate;
};
AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) { AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) {
var audio_buffer_value = buffersize / this.sampleRate; var audio_buffer_value = buffersize / this.sampleRate;
var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; var overrun = audio_buffer_value > audio_buffer_maximal_length_sec;
@ -53,6 +65,10 @@ NetworkSpeedProgressBar = function(el) {
NetworkSpeedProgressBar.prototype = new ProgressBar(); NetworkSpeedProgressBar.prototype = new ProgressBar();
NetworkSpeedProgressBar.prototype.getDefaultText = function() {
return 'Network usage [0 kbps]';
};
NetworkSpeedProgressBar.prototype.setSpeed = function(speed) { NetworkSpeedProgressBar.prototype.setSpeed = function(speed) {
var speedInKilobits = speed * 8 / 1000; var speedInKilobits = speed * 8 / 1000;
this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false); this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false);
@ -64,18 +80,29 @@ AudioSpeedProgressBar = function(el) {
AudioSpeedProgressBar.prototype = new ProgressBar(); AudioSpeedProgressBar.prototype = new ProgressBar();
AudioSpeedProgressBar.prototype.getDefaultText = function() {
return 'Audio stream [0 kbps]';
};
AudioSpeedProgressBar.prototype.setSpeed = function(speed) { AudioSpeedProgressBar.prototype.setSpeed = function(speed) {
this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false); this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false);
}; };
AudioOutputProgressBar = function(el, sampleRate) { AudioOutputProgressBar = function(el, sampleRate) {
ProgressBar.call(this, el); ProgressBar.call(this, el);
this.maxRate = sampleRate * 1.25;
this.minRate = sampleRate * .25;
}; };
AudioOutputProgressBar.prototype = new ProgressBar(); AudioOutputProgressBar.prototype = new ProgressBar();
AudioOutputProgressBar.prototype.getDefaultText = function() {
return 'Audio output [0 sps]';
};
AudioOutputProgressBar.prototype.setSampleRate = function(sampleRate) {
this.maxRate = sampleRate * 1.25;
this.minRate = sampleRate * .25;
};
AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) { AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) {
this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate); this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate);
}; };
@ -88,6 +115,10 @@ ClientsProgressBar = function(el) {
ClientsProgressBar.prototype = new ProgressBar(); ClientsProgressBar.prototype = new ProgressBar();
ClientsProgressBar.prototype.getDefaultText = function() {
return 'Clients [1]';
};
ClientsProgressBar.prototype.setClients = function(clients) { ClientsProgressBar.prototype.setClients = function(clients) {
this.clients = clients; this.clients = clients;
this.render(); this.render();
@ -108,6 +139,27 @@ CpuProgressBar = function(el) {
CpuProgressBar.prototype = new ProgressBar(); CpuProgressBar.prototype = new ProgressBar();
CpuProgressBar.prototype.getDefaultText = function() {
return 'Server CPU [0%]';
};
CpuProgressBar.prototype.setUsage = function(usage) { CpuProgressBar.prototype.setUsage = function(usage) {
this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85); this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85);
}; };
ProgressBar.types = {
cpu: CpuProgressBar,
audiobuffer: AudioBufferProgressBar,
audiospeed: AudioSpeedProgressBar,
audiooutput: AudioOutputProgressBar,
clients: ClientsProgressBar,
networkspeed: NetworkSpeedProgressBar
}
$.fn.progressbar = function() {
if (!this.data('progressbar')) {
var constructor = ProgressBar.types[this.data('type')] || ProgressBar;
this.data('progressbar', new constructor(this));
}
return this.data('progressbar');
};

View File

@ -0,0 +1,138 @@
function Input(name, value, options) {
this.name = name;
this.value = value;
this.options = options;
this.label = options && options.label || name;
};
Input.prototype.getClasses = function() {
return ['form-control', 'form-control-sm'];
}
Input.prototype.bootstrapify = function(input) {
this.getClasses().forEach(input.addClass.bind(input));
return [
'<div class="form-group row">',
'<label class="col-form-label col-form-label-sm col-3" for="' + this.name + '">' + this.label + '</label>',
'<div class="col-9">',
$.map(input, function(el) {
return el.outerHTML;
}).join(''),
'</div>',
'</div>'
].join('');
};
function TextInput() {
Input.apply(this, arguments);
};
TextInput.prototype = new Input();
TextInput.prototype.render = function() {
return this.bootstrapify($('<input type="text" name="' + this.name + '" value="' + this.value + '">'));
}
function NumberInput() {
Input.apply(this, arguments);
};
NumberInput.prototype = new Input();
NumberInput.prototype.render = function() {
return this.bootstrapify($('<input type="number" name="' + this.name + '" value="' + this.value + '">'));
};
function SoapyGainInput() {
Input.apply(this, arguments);
}
SoapyGainInput.prototype = new Input();
SoapyGainInput.prototype.getClasses = function() {
return [];
};
SoapyGainInput.prototype.render = function(){
var markup = $(
'<div class="row form-group">' +
'<div class="col-4">Gain mode</div>' +
'<div class="col-8">' +
'<select class="form-control form-control-sm">' +
'<option value="auto">automatic gain</option>' +
'<option value="single">single gain value</option>' +
'<option value="separate">separate gain values</option>' +
'</select>' +
'</div>' +
'</div>' +
'<div class="row option form-group gain-mode-single">' +
'<div class="col-4">Gain</div>' +
'<div class="col-8">' +
'<input class="form-control form-control-sm" type="number">' +
'</div>' +
'</div>' +
this.options.gains.map(function(g){
return '<div class="row option form-group gain-mode-separate">' +
'<div class="col-4">' + g + '</div>' +
'<div class="col-8">' +
'<input class="form-control form-control-sm" data-gain="' + g + '" type="number">' +
'</div>' +
'</div>';
}).join('')
);
var el = $(this.bootstrapify(markup))
var setMode = function(mode){
el.find('select').val(mode);
el.find('.option').hide();
el.find('.gain-mode-' + mode).show();
};
el.on('change', 'select', function(){
var mode = $(this).val();
setMode(mode);
});
if (typeof(this.value) === 'number') {
setMode('single');
el.find('.gain-mode-single input').val(this.value);
} else if (typeof(this.value) === 'string') {
if (this.value === 'auto') {
setMode('auto');
} else {
setMode('separate');
values = $.extend.apply($, this.value.split(',').map(function(seg){
var split = seg.split('=');
if (split.length < 2) return;
var res = {};
res[split[0]] = parseInt(split[1]);
return res;
}));
el.find('.gain-mode-separate input').each(function(){
var $input = $(this);
var g = $input.data('gain');
$input.val(g in values ? values[g] : 0);
});
}
} else {
setMode('auto');
}
return el;
};
function ProfileInput() {
Input.apply(this, arguments);
};
ProfileInput.prototype = new Input();
ProfileInput.prototype.render = function() {
return $('<div><h3>Profiles</h3></div>');
};
function SchedulerInput() {
Input.apply(this, arguments);
};
SchedulerInput.prototype = new Input();
SchedulerInput.prototype.render = function() {
return $('<div><h3>Scheduler</h3></div>');
};

View File

@ -0,0 +1,252 @@
function SdrDevice(el, data) {
this.el = el;
this.data = data;
this.inputs = {};
this.render();
var self = this;
el.on('click', '.fieldselector .btn', function() {
var key = el.find('.fieldselector select').val();
self.data[key] = self.getInitialValue(key);
self.render();
});
};
SdrDevice.create = function(el) {
var data = JSON.parse(decodeURIComponent(el.data('config')));
var type = data.type;
var constructor = SdrDevice.types[type] || SdrDevice;
return new constructor(el, data);
};
SdrDevice.prototype.getData = function() {
return $.extend(new Object(), this.getDefaults(), this.data);
};
SdrDevice.prototype.getDefaults = function() {
var defaults = {}
$.each(this.getMappings(), function(k, v) {
if (!v.includeInDefault) return;
defaults[k] = 'initialValue' in v ? v['initialValue'] : false;
});
return defaults;
};
SdrDevice.prototype.getMappings = function() {
return {
"name": {
constructor: TextInput,
inputOptions: {
label: "Name"
},
initialValue: "",
includeInDefault: true
},
"type": {
constructor: TextInput,
inputOptions: {
label: "Type"
},
initialValue: '',
includeInDefault: true
},
"ppm": {
constructor: NumberInput,
inputOptions: {
label: "PPM"
},
initialValue: 0
},
"profiles": {
constructor: ProfileInput,
inputOptions: {
label: "Profiles"
},
initialValue: [],
includeInDefault: true,
position: 100
},
"scheduler": {
constructor: SchedulerInput,
inputOptions: {
label: "Scheduler",
},
initialValue: {},
position: 101
},
"rf_gain": {
constructor: TextInput,
inputOptions: {
label: "Gain",
},
initialValue: 0
}
};
};
SdrDevice.prototype.getMapping = function(key) {
var mappings = this.getMappings();
return mappings[key];
};
SdrDevice.prototype.getInputClass = function(key) {
var mapping = this.getMapping(key);
return mapping && mapping.constructor || TextInput;
};
SdrDevice.prototype.getInitialValue = function(key) {
var mapping = this.getMapping(key);
return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false;
};
SdrDevice.prototype.getPosition = function(key) {
var mapping = this.getMapping(key);
return mapping && mapping.position || 10;
};
SdrDevice.prototype.getInputOptions = function(key) {
var mapping = this.getMapping(key);
return mapping && mapping.inputOptions || {};
};
SdrDevice.prototype.getLabel = function(key) {
var options = this.getInputOptions(key);
return options && options.label || key;
};
SdrDevice.prototype.render = function() {
var self = this;
self.el.empty();
var data = this.getData();
Object.keys(data).sort(function(a, b){
return self.getPosition(a) - self.getPosition(b);
}).forEach(function(key){
var value = data[key];
var inputClass = self.getInputClass(key);
var input = new inputClass(key, value, self.getInputOptions(key));
self.inputs[key] = input;
self.el.append(input.render());
});
self.el.append(this.renderFieldSelector());
};
SdrDevice.prototype.renderFieldSelector = function() {
var self = this;
return '<div class="fieldselector">' +
'<h3>Add new configuration options<h3>' +
'<div class="form-group row">' +
'<div class="col-3"><select class="form-control form-control-sm">' +
Object.keys(self.getMappings()).filter(function(m){
return !(m in self.data);
}).map(function(m) {
return '<option value="' + m + '">' + self.getLabel(m) + '</option>';
}).join('') +
'</select></div>' +
'<div class="col-2">' +
'<div class="btn btn-primary">Add to config</div>' +
'</div>' +
'</div>' +
'</div>';
};
RtlSdrDevice = function() {
SdrDevice.apply(this, arguments);
};
RtlSdrDevice.prototype = Object.create(SdrDevice.prototype);
RtlSdrDevice.prototype.constructor = RtlSdrDevice;
RtlSdrDevice.prototype.getMappings = function() {
var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
return $.extend(new Object(), mappings, {
"device": {
constructor: TextInput,
inputOptions:{
label: "Serial number"
},
initialValue: ""
}
});
};
SoapySdrDevice = function() {
SdrDevice.apply(this, arguments);
};
SoapySdrDevice.prototype = Object.create(SdrDevice.prototype);
SoapySdrDevice.prototype.constructor = SoapySdrDevice;
SoapySdrDevice.prototype.getMappings = function() {
var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
return $.extend(new Object(), mappings, {
"device": {
constructor: TextInput,
inputOptions:{
label: "Soapy device selector"
},
initialValue: ""
},
"rf_gain": {
constructor: SoapyGainInput,
initialValue: 0,
inputOptions: {
label: "Gain",
gains: this.getGains()
}
}
});
};
SoapySdrDevice.prototype.getGains = function() {
return [];
};
SdrplaySdrDevice = function() {
SoapySdrDevice.apply(this, arguments);
};
SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice;
SdrplaySdrDevice.prototype.getGains = function() {
return ['RFGR', 'IFGR'];
};
AirspyHfSdrDevice = function() {
SoapySdrDevice.apply(this, arguments);
};
AirspyHfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
AirspyHfSdrDevice.prototype.constructor = AirspyHfSdrDevice;
AirspyHfSdrDevice.prototype.getGains = function() {
return ['RF', 'VGA'];
};
HackRfSdrDevice = function() {
SoapySdrDevice.apply(this, arguments);
};
HackRfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
HackRfSdrDevice.prototype.constructor = HackRfSdrDevice;
HackRfSdrDevice.prototype.getGains = function() {
return ['LNA', 'VGA', 'AMP'];
};
SdrDevice.types = {
'rtl_sdr': RtlSdrDevice,
'sdrplay': SdrplaySdrDevice,
'airspyhf': AirspyHfSdrDevice,
'hackrf': HackRfSdrDevice
};
$.fn.sdrdevice = function() {
return this.map(function(){
var el = $(this);
if (!el.data('sdrdevice')) {
el.data('sdrdevice', SdrDevice.create(el));
}
return el.data('sdrdevice');
});
};

27
htdocs/login.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Login</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/login.css" />
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="login">
<form method="POST">
<div class="form-group">
<label for="user">Username</label>
<input type="text" class="form-control" id="user" name="user" autofocus="autofocus" placeholder="Username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
</div>
<button type="submit" class="btn btn-secondary btn-login">Login</button>
</form>
</div>
</body>

View File

@ -3,9 +3,7 @@
<head> <head>
<title>OpenWebRX Map</title> <title>OpenWebRX Map</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<script src="static/lib/jquery-3.2.1.min.js"></script> <script src="compiled/map.js"></script>
<script src="static/lib/chroma.min.js"></script>
<script src="static/map.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<link rel="stylesheet" type="text/css" href="static/css/map.css" /> <link rel="stylesheet" type="text/css" href="static/css/map.css" />
<meta charset="utf-8"> <meta charset="utf-8">

View File

@ -135,7 +135,11 @@
if (expectedCallsign && expectedCallsign == update.callsign.trim()) { if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
map.panTo(pos); map.panTo(pos);
showMarkerInfoWindow(update.callsign, pos); showMarkerInfoWindow(update.callsign, pos);
delete(expectedCallsign); expectedCallsign = false;
}
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign.trim()) {
showMarkerInfoWindow(infowindow.callsign, pos);
} }
break; break;
case 'locator': case 'locator':
@ -176,7 +180,11 @@
if (expectedLocator && expectedLocator == update.location.locator) { if (expectedLocator && expectedLocator == update.location.locator) {
map.panTo(center); map.panTo(center);
showLocatorInfoWindow(expectedLocator, center); showLocatorInfoWindow(expectedLocator, center);
delete(expectedLocator); expectedLocator = false;
}
if (infowindow && infowindow.locator && infowindow.locator == update.location.locator) {
showLocatorInfoWindow(infowindow.locator, center);
} }
break; break;
} }
@ -215,13 +223,26 @@
case "config": case "config":
var config = json.value; var config = json.value;
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ 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], { map = new google.maps.Map($('.openwebrx-map')[0], {
center: { center: {
lat: config.receiver_gps[0], lat: config.receiver_gps.lat,
lng: config.receiver_gps[1] lng: config.receiver_gps.lon
}, },
zoom: 5 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(){ $.getScript("static/lib/nite-overlay.js").done(function(){
nite.init(map); nite.init(map);
setInterval(function() { nite.refresh() }, 10000); // every 10s setInterval(function() { nite.refresh() }, 10000); // every 10s
@ -237,6 +258,11 @@
case "update": case "update":
processUpdates(json.value); processUpdates(json.value);
break; break;
case 'receiver_details':
$('#webrx-top-container').header().setDetails(json['value']);
break;
default:
console.warn('received message of unknown type: ' + json['type']);
} }
} catch (e) { } catch (e) {
// don't lose exception // don't lose exception
@ -269,9 +295,21 @@
connect(); connect();
var getInfoWindow = function() {
if (!infowindow) {
infowindow = new google.maps.InfoWindow();
google.maps.event.addListener(infowindow, 'closeclick', function() {
delete infowindow.locator;
delete infowindow.callsign;
});
}
return infowindow;
}
var infowindow; var infowindow;
var showLocatorInfoWindow = function(locator, pos) { var showLocatorInfoWindow = function(locator, pos) {
if (!infowindow) infowindow = new google.maps.InfoWindow(); var infowindow = getInfoWindow();
infowindow.locator = locator;
var inLocator = $.map(rectangles, function(r, callsign) { var inLocator = $.map(rectangles, function(r, callsign) {
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band} return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
}).filter(function(d) { }).filter(function(d) {
@ -297,7 +335,8 @@
}; };
var showMarkerInfoWindow = function(callsign, pos) { var showMarkerInfoWindow = function(callsign, pos) {
if (!infowindow) infowindow = new google.maps.InfoWindow(); var infowindow = getInfoWindow();
infowindow.callsign = callsign;
var marker = markers[callsign]; var marker = markers[callsign];
var timestring = moment(marker.lastseen).fromNow(); var timestring = moment(marker.lastseen).fromNow();
var commentString = ""; var commentString = "";

File diff suppressed because it is too large Load Diff

21
htdocs/sdrsettings.html Normal file
View File

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

27
htdocs/settings.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Settings</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<script src="compiled/settings.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="container">
<div class="col-12">
<h1>Settings</h1>
</div>
<div class="col-12">
<a href="generalsettings">General settings</a>
</div>
<div class="col-12">
<a href="sdrsettings">SDR device settings</a>
</div>
<div class="col-12">
<a href="features">Feature report</a>
</div>
</div>
</body>

25
htdocs/settings.js Normal file
View File

@ -0,0 +1,25 @@
$(function(){
$(".map-input").each(function(el) {
var $el = $(this);
var field_id = $el.attr("for");
var $lat = $('#' + field_id + '-lat');
var $lon = $('#' + field_id + '-lon');
$.getScript("https://maps.googleapis.com/maps/api/js?key=" + $el.data("key")).done(function(){
$el.css("height", "200px");
var lp = new locationPicker($el.get(0), {
lat: parseFloat($lat.val()),
lng: parseFloat($lon.val())
}, {
zoom: 7
});
google.maps.event.addListener(lp.map, 'idle', function(event){
var pos = lp.getMarkerPosition();
$lat.val(pos.lat);
$lon.val(pos.lng);
});
});
});
$(".sdrdevice").sdrdevice();
});

15
manifest.sh Executable file
View File

@ -0,0 +1,15 @@
#!/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

@ -1,17 +1,18 @@
from http.server import HTTPServer
from owrx.http import RequestHandler
from owrx.config import PropertyManager
from owrx.feature import FeatureDetector
from owrx.sdr import SdrService
from socketserver import ThreadingMixIn
from owrx.sdrhu import SdrHuUpdater
from owrx.service import Services
from owrx.websocket import WebSocketConnection
from owrx.pskreporter import PskReporter
import logging import logging
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
from http.server import HTTPServer
from owrx.http import RequestHandler
from owrx.config import Config
from owrx.feature import FeatureDetector
from owrx.sdr import SdrService
from socketserver import ThreadingMixIn
from owrx.service import Services
from owrx.websocket import WebSocketConnection
from owrx.pskreporter import PskReporter
from owrx.version import openwebrx_version
class ThreadedHttpServer(ThreadingMixIn, HTTPServer): class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
@ -26,32 +27,41 @@ OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE fil
_________________________________________________________________________________________________ _________________________________________________________________________________________________
Author contact info: Jakob Ketterl, DD5JFK <dd5jfk@darc.de> Author contact info: Jakob Ketterl, DD5JFK <dd5jfk@darc.de>
Documentation: https://github.com/jketterl/openwebrx/wiki
Support and info: https://groups.io/g/openwebrx
""" """
) )
pm = PropertyManager.getSharedInstance().loadConfig() logger.info("OpenWebRX version {0} starting up...".format(openwebrx_version))
pm = Config.get()
configErrors = Config.validateConfig()
if configErrors:
logger.error(
"your configuration contains errors. please address the following errors:"
)
for e in configErrors:
logger.error(e)
return
featureDetector = FeatureDetector() featureDetector = FeatureDetector()
if not featureDetector.is_available("core"): if not featureDetector.is_available("core"):
print( logger.error(
"you are missing required dependencies to run openwebrx. " "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:"
) )
print(", ".join(featureDetector.get_requirements("core"))) logger.error(", ".join(featureDetector.get_requirements("core")))
return return
# Get error messages about unknown / unavailable features as soon as possible # Get error messages about unknown / unavailable features as soon as possible
SdrService.loadProps() SdrService.loadProps()
if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
updater = SdrHuUpdater()
updater.start()
Services.start() Services.start()
try: try:
server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler) server = ThreadedHttpServer(("0.0.0.0", pm["web_port"]), RequestHandler)
server.serve_forever() server.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
WebSocketConnection.closeAll() WebSocketConnection.closeAll()

244
owrx/audio.py Normal file
View File

@ -0,0 +1,244 @@
from abc import ABC, ABCMeta, abstractmethod
from owrx.config import Config
from owrx.metrics import Metrics, CounterMetric, DirectMetric
import threading
import wave
import subprocess
import os
from multiprocessing.connection import Pipe, wait
from datetime import datetime, timedelta
from queue import Queue, Full
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class QueueJob(object):
def __init__(self, decoder, file, freq):
self.decoder = decoder
self.file = file
self.freq = freq
def run(self):
self.decoder.decode(self)
def unlink(self):
try:
os.unlink(self.file)
except FileNotFoundError:
pass
class QueueWorker(threading.Thread):
def __init__(self, queue):
self.queue = queue
self.doRun = True
super().__init__(daemon=True)
def run(self) -> None:
while self.doRun:
job = self.queue.get()
try:
job.run()
except Exception:
logger.exception("failed to decode job")
self.queue.onError()
finally:
job.unlink()
self.queue.task_done()
class DecoderQueue(Queue):
sharedInstance = None
creationLock = threading.Lock()
@staticmethod
def getSharedInstance():
with DecoderQueue.creationLock:
if DecoderQueue.sharedInstance is None:
pm = Config.get()
DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"])
return DecoderQueue.sharedInstance
def __init__(self, maxsize, workers):
super().__init__(maxsize)
metrics = Metrics.getSharedInstance()
metrics.addMetric("decoding.queue.length", DirectMetric(self.qsize))
self.inCounter = CounterMetric()
metrics.addMetric("decoding.queue.in", self.inCounter)
self.outCounter = CounterMetric()
metrics.addMetric("decoding.queue.out", self.outCounter)
self.overflowCounter = CounterMetric()
metrics.addMetric("decoding.queue.overflow", self.overflowCounter)
self.errorCounter = CounterMetric()
metrics.addMetric("decoding.queue.error", self.errorCounter)
self.workers = [self.newWorker() for _ in range(0, workers)]
def put(self, item, **kwars):
self.inCounter.inc()
try:
super(DecoderQueue, self).put(item, block=False)
except Full:
self.overflowCounter.inc()
raise
def get(self, **kwargs):
# super.get() is blocking, so it would mess up the stats to inc() first
out = super(DecoderQueue, self).get(**kwargs)
self.outCounter.inc()
return out
def newWorker(self):
worker = QueueWorker(self)
worker.start()
return worker
def onError(self):
self.errorCounter.inc()
class AudioChopperProfile(ABC):
@abstractmethod
def getInterval(self):
pass
@abstractmethod
def getFileTimestampFormat(self):
pass
@abstractmethod
def decoder_commandline(self, file):
pass
class AudioWriter(object):
def __init__(self, dsp, source, profile: AudioChopperProfile):
self.dsp = dsp
self.source = source
self.profile = profile
self.tmp_dir = Config.get()["temporary_directory"]
self.wavefile = None
self.wavefilename = None
self.switchingLock = threading.Lock()
self.timer = None
(self.outputReader, self.outputWriter) = Pipe()
def getWaveFile(self):
filename = "{tmp_dir}/openwebrx-audiochopper-{id}-{timestamp}.wav".format(
tmp_dir=self.tmp_dir,
id=id(self),
timestamp=datetime.utcnow().strftime(self.profile.getFileTimestampFormat()),
)
wavefile = wave.open(filename, "wb")
wavefile.setnchannels(1)
wavefile.setsampwidth(2)
wavefile.setframerate(12000)
return filename, wavefile
def getNextDecodingTime(self):
t = datetime.utcnow()
zeroed = t.replace(minute=0, second=0, microsecond=0)
delta = t - zeroed
interval = self.profile.getInterval()
seconds = (int(delta.total_seconds() / interval) + 1) * interval
t = zeroed + timedelta(seconds=seconds)
logger.debug("scheduling: {0}".format(t))
return t
def cancelTimer(self):
if self.timer:
self.timer.cancel()
self.timer = None
def _scheduleNextSwitch(self):
self.cancelTimer()
delta = self.getNextDecodingTime() - datetime.utcnow()
self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
self.timer.start()
def switchFiles(self):
self.switchingLock.acquire()
file = self.wavefile
filename = self.wavefilename
(self.wavefilename, self.wavefile) = self.getWaveFile()
self.switchingLock.release()
file.close()
job = QueueJob(self, filename, self.dsp.get_operating_freq())
try:
DecoderQueue.getSharedInstance().put(job)
except Full:
logger.warning("decoding queue overflow; dropping one file")
job.unlink()
self._scheduleNextSwitch()
def decode(self, job: QueueJob):
logger.debug("processing file %s", job.file)
decoder = subprocess.Popen(
["nice", "-n", "10"] + self.profile.decoder_commandline(job.file),
stdout=subprocess.PIPE,
cwd=self.tmp_dir,
close_fds=True,
)
for line in decoder.stdout:
self.outputWriter.send((job.freq, line))
try:
rc = decoder.wait(timeout=10)
if rc != 0:
logger.warning("decoder return code: %i", rc)
except subprocess.TimeoutExpired:
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
decoder.kill()
def start(self):
(self.wavefilename, self.wavefile) = self.getWaveFile()
self._scheduleNextSwitch()
def write(self, data):
self.switchingLock.acquire()
self.wavefile.writeframes(data)
self.switchingLock.release()
def stop(self):
self.outputReader.close()
self.outputWriter.close()
self.cancelTimer()
try:
os.unlink(self.wavefilename)
except Exception:
logger.exception("error removing undecoded file")
class AudioChopper(threading.Thread, metaclass=ABCMeta):
def __init__(self, dsp, source, *profiles: AudioChopperProfile):
self.source = source
self.writers = [AudioWriter(dsp, source, p) for p in profiles]
self.doRun = True
super().__init__()
def run(self) -> None:
logger.debug("Audio chopper starting up")
for w in self.writers:
w.start()
while self.doRun:
data = self.source.read(256)
if data is None or (isinstance(data, bytes) and len(data) == 0):
self.doRun = False
else:
for w in self.writers:
w.write(data)
logger.debug("Audio chopper shutting down")
for w in self.writers:
w.stop()
def read(self):
try:
readers = wait([w.outputReader for w in self.writers])
return [r.recv() for r in readers]
except EOFError:
return None

View File

@ -1,4 +1,4 @@
from owrx.config import PropertyManager from owrx.config import Config
from owrx.metrics import Metrics, DirectMetric from owrx.metrics import Metrics, DirectMetric
import threading import threading
@ -33,7 +33,7 @@ class ClientRegistry(object):
c.write_clients(n) c.write_clients(n)
def addClient(self, client): def addClient(self, client):
pm = PropertyManager.getSharedInstance() pm = Config.get()
if len(self.clients) >= pm["max_clients"]: if len(self.clients) >= pm["max_clients"]:
raise TooManyClientsException() raise TooManyClientsException()
self.clients.append(client) self.clients.append(client)

View File

@ -33,6 +33,9 @@ class CommandMapper(object):
self.static = static self.static = static
return self return self
def keys(self):
return self.mappings.keys()
class CommandMapping(ABC): class CommandMapping(ABC):
@abstractmethod @abstractmethod

View File

@ -1,149 +1,134 @@
from owrx.property import PropertyManager, PropertyLayer
import importlib.util import importlib.util
import os
import logging import logging
import json
from abc import ABC, abstractmethod
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Subscription(object):
def __init__(self, subscriptee, subscriber):
self.subscriptee = subscriptee
self.subscriber = subscriber
def call(self, *args, **kwargs):
self.subscriber(*args, **kwargs)
def cancel(self):
self.subscriptee.unwire(self)
class Property(object):
def __init__(self, value=None):
self.value = value
self.subscribers = []
def getValue(self):
return self.value
def setValue(self, value):
if self.value == value:
return self
self.value = value
for c in self.subscribers:
try:
c.call(self.value)
except Exception as e:
logger.exception(e)
return self
def wire(self, callback):
sub = Subscription(self, callback)
self.subscribers.append(sub)
if not self.value is None:
sub.call(self.value)
return sub
def unwire(self, sub):
try:
self.subscribers.remove(sub)
except ValueError:
# happens when already removed before
pass
return self
class ConfigNotFoundException(Exception): class ConfigNotFoundException(Exception):
pass pass
class PropertyManager(object): class ConfigError(object):
sharedInstance = None def __init__(self, key, message):
self.key = key
self.message = message
def __str__(self):
return "Configuration Error (key: {0}): {1}".format(self.key, self.message)
class ConfigMigrator(ABC):
@abstractmethod
def migrate(self, config):
pass
def renameKey(self, config, old, new):
if old in config and not new in config:
config[new] = config[old]
del config[old]
class ConfigMigratorVersion1(ConfigMigrator):
def migrate(self, config):
if "receiver_gps" in config:
gps = config["receiver_gps"]
config["receiver_gps"] = {"lat": gps[0], "lon": gps[1]}
if "waterfall_auto_level_margin" in config:
levels = config["waterfall_auto_level_margin"]
config["waterfall_auto_level_margin"] = {"min": levels[0], "max": levels[1]}
self.renameKey(config, "wsjt_queue_workers", "decoding_queue_workers")
self.renameKey(config, "wsjt_queue_length", "decoding_queue_length")
config["version"] = 2
return config
class Config:
sharedConfig = None
currentVersion = 2
migrators = {
1: ConfigMigratorVersion1()
}
@staticmethod @staticmethod
def getSharedInstance(): def _loadPythonFile(file):
if PropertyManager.sharedInstance is None: spec = importlib.util.spec_from_file_location("config_webrx", file)
PropertyManager.sharedInstance = PropertyManager() cfg = importlib.util.module_from_spec(spec)
return PropertyManager.sharedInstance spec.loader.exec_module(cfg)
pm = PropertyLayer()
for name, value in cfg.__dict__.items():
if name.startswith("__"):
continue
pm[name] = value
return pm
def collect(self, *props): @staticmethod
return PropertyManager( def _loadJsonFile(file):
{name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props} with open(file, "r") as f:
) pm = PropertyLayer()
for k, v in json.load(f).items():
pm[k] = v
return pm
def __init__(self, properties=None): @staticmethod
self.properties = {} def _loadConfig():
self.subscribers = [] for file in ["./settings.json", "/etc/openwebrx/config_webrx.py", "./config_webrx.py"]:
if properties is not None:
for (name, prop) in properties.items():
self.add(name, prop)
def add(self, name, prop):
self.properties[name] = prop
def fireCallbacks(value):
for c in self.subscribers:
try:
c.call(name, value)
except Exception as e:
logger.exception(e)
prop.wire(fireCallbacks)
return self
def __contains__(self, name):
return self.hasProperty(name)
def __getitem__(self, name):
return self.getPropertyValue(name)
def __setitem__(self, name, value):
if not self.hasProperty(name):
self.add(name, Property())
self.getProperty(name).setValue(value)
def __dict__(self):
return {k: v.getValue() for k, v in self.properties.items()}
def hasProperty(self, name):
return name in self.properties
def getProperty(self, name):
if not self.hasProperty(name):
self.add(name, Property())
return self.properties[name]
def getPropertyValue(self, name):
return self.getProperty(name).getValue()
def wire(self, callback):
sub = Subscription(self, callback)
self.subscribers.append(sub)
return sub
def unwire(self, sub):
try:
self.subscribers.remove(sub)
except ValueError:
# happens when already removed before
pass
return self
def defaults(self, other_pm):
for (key, p) in self.properties.items():
if p.getValue() is None:
p.setValue(other_pm[key])
return self
def loadConfig(self):
for file in ["/etc/openwebrx/config_webrx.py", "./config_webrx.py"]:
try: try:
spec = importlib.util.spec_from_file_location("config_webrx", file) if file.endswith(".py"):
cfg = importlib.util.module_from_spec(spec) return Config._loadPythonFile(file)
spec.loader.exec_module(cfg) elif file.endswith(".json"):
for name, value in cfg.__dict__.items(): return Config._loadJsonFile(file)
if name.startswith("__"): else:
continue logger.warning("unsupported file type: %s", file)
self[name] = value
return self
except FileNotFoundError: except FileNotFoundError:
pass pass
raise ConfigNotFoundException("no usable config found! please make sure you have a valid configuration file!") raise ConfigNotFoundException("no usable config found! please make sure you have a valid configuration file!")
@staticmethod
def get():
if Config.sharedConfig is None:
Config.sharedConfig = Config._migrate(Config._loadConfig())
return Config.sharedConfig
@staticmethod
def store():
with open("settings.json", "w") as file:
json.dump(Config.get().__dict__(), file, indent=4)
@staticmethod
def validateConfig():
pm = Config.get()
errors = [
Config.checkTempDirectory(pm)
]
return [e for e in errors if e is not None]
@staticmethod
def checkTempDirectory(pm: PropertyManager):
key = "temporary_directory"
if key not in pm or pm[key] is None:
return ConfigError(key, "temporary directory is not set")
if not os.path.exists(pm[key]):
return ConfigError(key, "temporary directory doesn't exist")
if not os.path.isdir(pm[key]):
return ConfigError(key, "temporary directory path is not a directory")
if not os.access(pm[key], os.W_OK):
return ConfigError(key, "temporary directory is not writable")
return None
@staticmethod
def _migrate(config):
version = config["version"] if "version" in config else 1
if version == Config.currentVersion:
return config
logger.debug("migrating config from version %i", version)
migrator = Config.migrators[version]
return migrator.migrate(config)

View File

@ -1,4 +1,5 @@
from owrx.config import PropertyManager from owrx.config import Config
from owrx.details import ReceiverDetails
from owrx.dsp import DspManager from owrx.dsp import DspManager
from owrx.cpu import CpuUsageThread from owrx.cpu import CpuUsageThread
from owrx.sdr import SdrService from owrx.sdr import SdrService
@ -9,9 +10,12 @@ from owrx.version import openwebrx_version
from owrx.bands import Bandplan from owrx.bands import Bandplan
from owrx.bookmarks import Bookmarks from owrx.bookmarks import Bookmarks
from owrx.map import Map from owrx.map import Map
from owrx.locator import Locator from owrx.property import PropertyStack
from owrx.modes import Modes, DigitalMode
from multiprocessing import Queue from multiprocessing import Queue
from queue import Full from queue import Full
from js8py import Js8Frame
from abc import ABC, ABCMeta, abstractmethod
import json import json
import threading import threading
@ -20,7 +24,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Client(object): class Client(ABC):
def __init__(self, conn): def __init__(self, conn):
self.conn = conn self.conn = conn
self.multiprocessingPipe = Queue(100) self.multiprocessingPipe = Queue(100)
@ -49,6 +53,7 @@ class Client(object):
except Full: except Full:
self.close() self.close()
@abstractmethod
def handleTextMessage(self, conn, message): def handleTextMessage(self, conn, message):
pass pass
@ -59,7 +64,25 @@ class Client(object):
self.close() self.close()
class OpenWebRxReceiverClient(Client): class OpenWebRxClient(Client, metaclass=ABCMeta):
def __init__(self, conn):
super().__init__(conn)
receiver_details = ReceiverDetails()
def send_receiver_info(*args):
receiver_info = receiver_details.__dict__()
self.write_receiver_details(receiver_info)
# TODO unsubscribe
receiver_details.wire(send_receiver_info)
send_receiver_info()
def write_receiver_details(self, details):
self.send({"type": "receiver_details", "value": details})
class OpenWebRxReceiverClient(OpenWebRxClient):
config_keys = [ config_keys = [
"waterfall_colors", "waterfall_colors",
"waterfall_min_level", "waterfall_min_level",
@ -67,7 +90,6 @@ class OpenWebRxReceiverClient(Client):
"waterfall_auto_level_margin", "waterfall_auto_level_margin",
"samp_rate", "samp_rate",
"fft_size", "fft_size",
"fft_fps",
"audio_compression", "audio_compression",
"fft_compression", "fft_compression",
"max_clients", "max_clients",
@ -93,28 +115,16 @@ class OpenWebRxReceiverClient(Client):
self.close() self.close()
raise raise
pm = PropertyManager.getSharedInstance()
self.setSdr() self.setSdr()
# send receiver info
receiver_keys = [
"receiver_name",
"receiver_location",
"receiver_asl",
"receiver_gps",
"photo_title",
"photo_desc",
]
receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys)
receiver_details["locator"] = Locator.fromCoordinates(receiver_details["receiver_gps"])
self.write_receiver_details(receiver_details)
self.__sendProfiles()
features = FeatureDetector().feature_availability() features = FeatureDetector().feature_availability()
self.write_features(features) self.write_features(features)
modes = Modes.getModes()
self.write_modes(modes)
self.__sendProfiles()
CpuUsageThread.getSharedInstance().add_client(self) CpuUsageThread.getSharedInstance().add_client(self)
def __sendProfiles(self): def __sendProfiles(self):
@ -134,8 +144,12 @@ class OpenWebRxReceiverClient(Client):
self.startDsp() self.startDsp()
if "params" in message: if "params" in message:
params = message["params"] dsp = self.getDsp()
self.setDspProperties(params) if dsp is None:
logger.warning("DSP not available; discarding client data")
else:
params = message["params"]
dsp.setProperties(params)
elif message["type"] == "config": elif message["type"] == "config":
if "params" in message: if "params" in message:
@ -152,7 +166,7 @@ class OpenWebRxReceiverClient(Client):
if "params" in message: if "params" in message:
self.connectionProperties = message["params"] self.connectionProperties = message["params"]
if self.dsp: if self.dsp:
self.setDspProperties(self.connectionProperties) self.getDsp().setProperties(self.connectionProperties)
else: else:
logger.warning("received message without type: {0}".format(message)) logger.warning("received message without type: {0}".format(message))
@ -169,6 +183,7 @@ class OpenWebRxReceiverClient(Client):
next = SdrService.getFirstSource() next = SdrService.getFirstSource()
if next is None: if next is None:
# exit condition: no sdrs available # exit condition: no sdrs available
logger.warning("no more SDR devices available")
self.handleNoSdrsAvailable() self.handleNoSdrsAvailable()
return return
@ -184,25 +199,25 @@ class OpenWebRxReceiverClient(Client):
self.sdr = next self.sdr = next
self.startDsp() self.getDsp()
# keep trying until we find a suitable SDR # found a working sdr, exit the loop
if self.sdr.getState() == SdrSource.STATE_FAILED: if self.sdr.getState() != SdrSource.STATE_FAILED:
self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
else:
break break
# send initial config logger.warning('SDR device "%s" has failed, selecing new device', self.sdr.getName())
self.setDspProperties(self.connectionProperties) self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
configProps = ( # send initial config
self.sdr.getProps() self.getDsp().setProperties(self.connectionProperties)
.collect(*OpenWebRxReceiverClient.config_keys)
.defaults(PropertyManager.getSharedInstance()) stack = PropertyStack()
) stack.addLayer(0, self.sdr.getProps())
stack.addLayer(1, Config.get())
configProps = stack.filter(*OpenWebRxReceiverClient.config_keys)
def sendConfig(key, value): def sendConfig(key, value):
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys) config = configProps.__dict__()
# TODO mathematical properties? hmmmm # TODO mathematical properties? hmmmm
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
# TODO this is a hack to support multiple sdrs # TODO this is a hack to support multiple sdrs
@ -226,9 +241,7 @@ class OpenWebRxReceiverClient(Client):
self.write_sdr_error("No SDR Devices available") self.write_sdr_error("No SDR Devices available")
def startDsp(self): def startDsp(self):
if self.dsp is None and self.sdr is not None: self.getDsp().start()
self.dsp = DspManager(self, self.sdr)
self.dsp.start()
def close(self): def close(self):
self.stopDsp() self.stopDsp()
@ -247,18 +260,28 @@ class OpenWebRxReceiverClient(Client):
self.sdr.removeSpectrumClient(self) self.sdr.removeSpectrumClient(self)
def setParams(self, params): def setParams(self, params):
config = Config.get()
# allow direct configuration only if enabled in the config
if "configurable_keys" not in config:
return
keys = config["configurable_keys"]
if not keys:
return
# only the keys in the protected property manager can be overridden from the web # only the keys in the protected property manager can be overridden from the web
protected = ( stack = PropertyStack()
self.sdr.getProps() stack.addLayer(0, self.sdr.getProps())
.collect("samp_rate", "center_freq", "rf_gain", "type") stack.addLayer(1, config)
.defaults(PropertyManager.getSharedInstance()) protected = stack.filter(*keys)
)
for key, value in params.items(): for key, value in params.items():
protected[key] = value try:
protected[key] = value
except KeyError:
pass
def setDspProperties(self, params): def getDsp(self):
for key, value in params.items(): if self.dsp is None and self.sdr is not None:
self.dsp.setProperty(key, value) self.dsp = DspManager(self, self.sdr)
return self.dsp
def write_spectrum_data(self, data): def write_spectrum_data(self, data):
self.mp_send(bytes([0x01]) + data) self.mp_send(bytes([0x01]) + data)
@ -288,9 +311,6 @@ class OpenWebRxReceiverClient(Client):
def write_config(self, cfg): def write_config(self, cfg):
self.send({"type": "config", "value": cfg}) self.send({"type": "config", "value": cfg})
def write_receiver_details(self, details):
self.send({"type": "receiver_details", "value": details})
def write_profiles(self, profiles): def write_profiles(self, profiles):
self.send({"type": "profiles", "value": profiles}) self.send({"type": "profiles", "value": profiles})
@ -324,13 +344,44 @@ class OpenWebRxReceiverClient(Client):
def write_backoff_message(self, reason): def write_backoff_message(self, reason):
self.send({"type": "backoff", "reason": reason}) self.send({"type": "backoff", "reason": reason})
def write_js8_message(self, frame: Js8Frame, freq: int):
self.send({"type": "js8_message", "value": {
"msg": str(frame),
"timestamp": frame.timestamp,
"db": frame.db,
"dt": frame.dt,
"freq": freq + frame.freq,
"thread_type": frame.thread_type,
"mode": frame.mode
}})
class MapConnection(Client): def write_modes(self, modes):
def to_json(m):
res = {
"modulation": m.modulation,
"name": m.name,
"type": "digimode" if isinstance(m, DigitalMode) else "analog",
"requirements": m.requirements,
"squelch": m.squelch,
}
if m.bandpass is not None:
res["bandpass"] = {
"low_cut": m.bandpass.low_cut,
"high_cut": m.bandpass.high_cut
}
if isinstance(m, DigitalMode):
res["underlying"] = m.underlying
return res
self.send({"type": "modes", "value": [to_json(m) for m in modes]})
class MapConnection(OpenWebRxClient):
def __init__(self, conn): def __init__(self, conn):
super().__init__(conn) super().__init__(conn)
pm = PropertyManager.getSharedInstance() pm = Config.get()
self.write_config(pm.collect("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__()) self.write_config(pm.filter("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__())
Map.getSharedInstance().addClient(self) Map.getSharedInstance().addClient(self)

View File

@ -1,168 +0,0 @@
import os
import mimetypes
import json
import pkg_resources
from datetime import datetime
from string import Template
from owrx.websocket import WebSocketConnection
from owrx.config import PropertyManager
from owrx.client import ClientRegistry
from owrx.connection import WebSocketMessageHandler
from owrx.version import openwebrx_version
from owrx.feature import FeatureDetector
from owrx.metrics import Metrics
import logging
logger = logging.getLogger(__name__)
class Controller(object):
def __init__(self, handler, request):
self.handler = handler
self.request = request
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None):
self.handler.send_response(code)
if content_type is not None:
self.handler.send_header("Content-Type", content_type)
if last_modified is not None:
self.handler.send_header("Last-Modified", last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT"))
if max_age is not None:
self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age))
self.handler.end_headers()
if type(content) == str:
content = content.encode()
self.handler.wfile.write(content)
class StatusController(Controller):
def handle_request(self):
pm = PropertyManager.getSharedInstance()
# TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna
vars = {
"status": "active",
"name": pm["receiver_name"],
"op_email": pm["receiver_admin"],
"users": ClientRegistry.getSharedInstance().clientCount(),
"users_max": pm["max_clients"],
"gps": pm["receiver_gps"],
"asl": pm["receiver_asl"],
"loc": pm["receiver_location"],
"sw_version": openwebrx_version,
"avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png"),
}
self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()]))
class AssetsController(Controller):
def getModified(self, file):
return None
def openFile(self, file):
pass
def serve_file(self, file, content_type=None):
try:
modified = self.getModified(file)
if modified is not None and "If-Modified-Since" in self.handler.headers:
client_modified = datetime.strptime(
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
)
if modified <= client_modified:
self.send_response("", code=304)
return
f = self.openFile(file)
data = f.read()
f.close()
if content_type is None:
(content_type, encoding) = mimetypes.MimeTypes().guess_type(file)
self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600)
except FileNotFoundError:
self.send_response("file not found", code=404)
def handle_request(self):
filename = self.request.matches.group(1)
self.serve_file(filename)
class OwrxAssetsController(AssetsController):
def openFile(self, file):
return pkg_resources.resource_stream("htdocs", file)
class AprsSymbolsController(AssetsController):
def __init__(self, handler, request):
pm = PropertyManager.getSharedInstance()
path = pm["aprs_symbols_path"]
if not path.endswith("/"):
path += "/"
self.path = path
super().__init__(handler, request)
def getFilePath(self, file):
return self.path + file
def getModified(self, file):
return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)))
def openFile(self, file):
return open(self.getFilePath(file), "rb")
class TemplateController(Controller):
def render_template(self, file, **vars):
file_content = pkg_resources.resource_string("htdocs", file).decode("utf-8")
template = Template(file_content)
return template.safe_substitute(**vars)
def serve_template(self, file, **vars):
self.send_response(self.render_template(file, **vars), content_type="text/html")
def default_variables(self):
return {}
class WebpageController(TemplateController):
def template_variables(self):
header = self.render_template("include/header.include.html")
return {"header": header}
class IndexController(WebpageController):
def handle_request(self):
self.serve_template("index.html", **self.template_variables())
class MapController(WebpageController):
def handle_request(self):
# TODO check if we have a google maps api key first?
self.serve_template("map.html", **self.template_variables())
class FeatureController(WebpageController):
def handle_request(self):
self.serve_template("features.html", **self.template_variables())
class ApiController(Controller):
def handle_request(self):
data = json.dumps(FeatureDetector().feature_report())
self.send_response(data, content_type="application/json")
class MetricsController(Controller):
def handle_request(self):
data = json.dumps(Metrics.getSharedInstance().getMetrics())
self.send_response(data, content_type="application/json")
class WebSocketController(Controller):
def handle_request(self):
conn = WebSocketConnection(self.handler, WebSocketMessageHandler())
# enter read loop
conn.handle()

View File

@ -0,0 +1,44 @@
from datetime import datetime
class Controller(object):
def __init__(self, handler, request, options):
self.handler = handler
self.request = request
self.options = options
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None):
self.handler.send_response(code)
if headers is None:
headers = {}
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")
if max_age is not None:
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()
if type(content) == str:
content = content.encode()
self.handler.wfile.write(content)
def send_redirect(self, location, code=303, cookies=None):
self.handler.send_response(code)
if cookies is not None:
self.handler.send_header("Set-Cookie", cookies.output(header=''))
self.handler.send_header("Location", location)
self.handler.end_headers()
def get_body(self):
if "Content-Length" not in self.handler.headers:
return None
length = int(self.handler.headers["Content-Length"])
return self.handler.rfile.read(length)
def handle_request(self):
action = "indexAction"
if "action" in self.options:
action = self.options["action"]
getattr(self, action)()

33
owrx/controllers/admin.py Normal file
View File

@ -0,0 +1,33 @@
from .template import WebpageController
from .session import SessionStorage
from owrx.config import Config
from urllib import parse
import logging
logger = logging.getLogger(__name__)
class Authentication(object):
def isAuthenticated(self, request):
if "owrx-session" in request.cookies:
session = SessionStorage.getSharedInstance().getSession(request.cookies["owrx-session"].value)
return session is not None
return False
class AdminController(WebpageController):
def __init__(self, handler, request, options):
self.authentication = Authentication()
super().__init__(handler, request, options)
def handle_request(self):
config = Config.get()
if "webadmin_enabled" not in config or not config["webadmin_enabled"]:
self.send_response("Web Admin is disabled", code=403)
return
if self.authentication.isAuthenticated(self.request):
super().handle_request()
else:
target = "/login?{0}".format(parse.urlencode({"ref": self.request.path}))
self.send_redirect(target)

15
owrx/controllers/api.py Normal file
View File

@ -0,0 +1,15 @@
from . import Controller
from owrx.feature import FeatureDetector
from owrx.details import ReceiverDetails
import json
class ApiController(Controller):
def indexAction(self):
data = json.dumps(FeatureDetector().feature_report())
self.send_response(data, content_type="application/json")
def receiverDetails(self):
receiver_details = ReceiverDetails()
data = json.dumps(receiver_details.__dict__())
self.send_response(data, content_type="application/json")

129
owrx/controllers/assets.py Normal file
View File

@ -0,0 +1,129 @@
from . import Controller
from owrx.config import Config
from datetime import datetime
import mimetypes
import os
import pkg_resources
from abc import ABCMeta, abstractmethod
class AssetsController(Controller, metaclass=ABCMeta):
def getModified(self, file):
return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)))
def openFile(self, file):
return open(self.getFilePath(file), "rb")
@abstractmethod
def getFilePath(self, file):
pass
def serve_file(self, file, content_type=None):
try:
modified = self.getModified(file)
if modified is not None and "If-Modified-Since" in self.handler.headers:
client_modified = datetime.strptime(
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
)
if modified <= client_modified:
self.send_response("", code=304)
return
f = self.openFile(file)
data = f.read()
f.close()
if content_type is None:
(content_type, encoding) = mimetypes.MimeTypes().guess_type(file)
self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600)
except FileNotFoundError:
self.send_response("file not found", code=404)
def indexAction(self):
filename = self.request.matches.group(1)
self.serve_file(filename)
class OwrxAssetsController(AssetsController):
def getFilePath(self, file):
return pkg_resources.resource_filename("htdocs", file)
class AprsSymbolsController(AssetsController):
def __init__(self, handler, request, options):
pm = Config.get()
path = pm["aprs_symbols_path"]
if not path.endswith("/"):
path += "/"
self.path = path
super().__init__(handler, request, options)
def getFilePath(self, file):
return self.path + file
class CompiledAssetsController(Controller):
profiles = {
"receiver.js": [
"openwebrx.js",
"lib/jquery-3.2.1.min.js",
"lib/jquery.nanoscroller.js",
"lib/Header.js",
"lib/Demodulator.js",
"lib/DemodulatorPanel.js",
"lib/BookmarkBar.js",
"lib/BookmarkDialog.js",
"lib/AudioEngine.js",
"lib/ProgressBar.js",
"lib/Measurement.js",
"lib/FrequencyDisplay.js",
"lib/Js8Threads.js",
"lib/Modes.js",
],
"map.js": [
"lib/jquery-3.2.1.min.js",
"lib/chroma.min.js",
"lib/Header.js",
"map.js",
],
"settings.js": [
"lib/jquery-3.2.1.min.js",
"lib/Header.js",
"lib/settings/Input.js",
"lib/settings/SdrDevice.js",
"settings.js",
]
}
def indexAction(self):
profileName = self.request.matches.group(1)
if profileName not in CompiledAssetsController.profiles:
self.send_response("profile not found", code=404)
return
files = CompiledAssetsController.profiles[profileName]
files = [pkg_resources.resource_filename("htdocs", f) for f in files]
modified = self.getModified(files)
if modified is not None and "If-Modified-Since" in self.handler.headers:
client_modified = datetime.strptime(
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
)
if modified <= client_modified:
self.send_response("", code=304)
return
contents = [self.getContents(f) for f in files]
(content_type, encoding) = mimetypes.MimeTypes().guess_type(profileName)
self.send_response("\n".join(contents), content_type=content_type, last_modified=modified, max_age=3600)
def getContents(self, file):
with open(file) as f:
return f.read()
def getModified(self, files):
modified = [datetime.fromtimestamp(os.path.getmtime(f)) for f in files]
return max(*modified)

View File

@ -0,0 +1,9 @@
from . import Controller
from owrx.metrics import Metrics
import json
class MetricsController(Controller):
def indexAction(self):
data = json.dumps(Metrics.getSharedInstance().getMetrics())
self.send_response(data, content_type="application/json")

View File

@ -0,0 +1,59 @@
from .template import WebpageController
from urllib.parse import parse_qs
from uuid import uuid4
from http.cookies import SimpleCookie
from owrx.users import UserList
class SessionStorage(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if SessionStorage.sharedInstance is None:
SessionStorage.sharedInstance = SessionStorage()
return SessionStorage.sharedInstance
def __init__(self):
self.sessions = {}
def generateKey(self):
return str(uuid4())
def startSession(self, data):
key = self.generateKey()
self.updateSession(key, data)
return key
def getSession(self, key):
if key not in self.sessions:
return None
return self.sessions[key]
def updateSession(self, key, data):
self.sessions[key] = data
class SessionController(WebpageController):
def loginAction(self):
self.serve_template("login.html", **self.template_variables())
def processLoginAction(self):
data = parse_qs(self.get_body().decode("utf-8"))
data = {k: v[0] for k, v in data.items()}
userlist = UserList.getSharedInstance()
if "user" in data and "password" in data:
if data["user"] in userlist:
user = userlist[data["user"]]
if user.password.is_valid(data["password"]):
# TODO evaluate password force_change and redirect to password change
key = SessionStorage.getSharedInstance().startSession({"user": user.name})
cookie = SimpleCookie()
cookie["owrx-session"] = key
target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings"
self.send_redirect(target, cookies=cookie)
return
self.send_redirect("/login")
def logoutAction(self):
self.send_redirect("logout happening here")

View File

@ -0,0 +1,279 @@
from .admin import AdminController
from owrx.config import Config
from urllib.parse import parse_qs
from owrx.form import (
TextInput,
NumberInput,
FloatInput,
LocationInput,
TextAreaInput,
CheckboxInput,
DropdownInput,
Option,
ServicesCheckboxInput,
Js8ProfileCheckboxInput,
)
from urllib.parse import quote
import json
import logging
logger = logging.getLogger(__name__)
class Section(object):
def __init__(self, title, *inputs):
self.title = title
self.inputs = inputs
def render_inputs(self):
config = Config.get()
return "".join([i.render(config) for i in self.inputs])
def render(self):
return """
<div class="col-12 settings-category">
<h3 class="settings-header">
{title}
</h3>
{inputs}
</div>
""".format(
title=self.title, inputs=self.render_inputs()
)
def parse(self, data):
return {k: v for i in self.inputs for k, v in i.parse(data).items()}
class SettingsController(AdminController):
def indexAction(self):
self.serve_template("settings.html", **self.template_variables())
class SdrSettingsController(AdminController):
def template_variables(self):
variables = super().template_variables()
variables["devices"] = self.render_devices()
return variables
def render_devices(self):
return "".join(self.render_device(key, value) for key, value in Config.get()["sdrs"].items())
def render_device(self, device_id, config):
return """
<div class="card device bg-dark text-white">
<div class="card-header">
{device_name}
</div>
<div class="card-body">
{form}
</div>
</div>
""".format(device_name=config["name"], form=self.render_form(device_id, config))
def render_form(self, device_id, config):
return """
<form class="sdrdevice" data-config="{formdata}"></form>
""".format(device_id=device_id, formdata=quote(json.dumps(config)))
def indexAction(self):
self.serve_template("sdrsettings.html", **self.template_variables())
class GeneralSettingsController(AdminController):
sections = [
Section(
"General settings",
TextInput("receiver_name", "Receiver name"),
TextInput("receiver_location", "Receiver location"),
NumberInput(
"receiver_asl",
"Receiver elevation",
infotext="Elevation in meters above mean see level",
),
TextInput("receiver_admin", "Receiver admin"),
LocationInput("receiver_gps", "Receiver coordinates"),
TextInput("photo_title", "Photo title"),
TextAreaInput("photo_desc", "Photo description"),
),
Section(
"Waterfall settings",
NumberInput(
"fft_fps",
"FFT frames per second",
infotext="This setting specifies how many lines are being added to the waterfall per second. "
+ "Higher values will give you a faster waterfall, but will also use more CPU.",
),
NumberInput("fft_size", "FFT size"),
FloatInput(
"fft_voverlap_factor",
"FFT vertical overlap factor",
infotext="If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the "
+ "diagram.",
),
NumberInput("waterfall_min_level", "Lowest waterfall level"),
NumberInput("waterfall_max_level", "Highest waterfall level"),
),
Section(
"Compression",
DropdownInput(
"audio_compression",
"Audio compression",
options=[Option("adpcm", "ADPCM"), Option("none", "None"),],
),
DropdownInput(
"fft_compression",
"Waterfall compression",
options=[Option("adpcm", "ADPCM"), Option("none", "None"),],
),
),
Section(
"Digimodes",
CheckboxInput("digimodes_enable", "", checkboxText="Enable Digimodes"),
NumberInput("digimodes_fft_size", "Digimodes FFT size"),
),
Section(
"Digital voice",
NumberInput(
"digital_voice_unvoiced_quality",
"Quality of unvoiced sounds in synthesized voice",
infotext="Determines the quality, and thus the cpu usage, for the ambe codec used by digital voice"
+ "modes.<br />If you're running on a Raspi (up to 3B+) you should leave this set at 1",
),
CheckboxInput(
"digital_voice_dmr_id_lookup",
"DMR id lookup",
checkboxText="Enable lookup of DMR ids in the radioid database to show callsigns and names",
),
),
Section(
"Experimental pipe settings",
CheckboxInput(
"csdr_dynamic_bufsize",
"",
checkboxText="Enable dynamic buffer sizes",
infotext="This allows you to change the buffering mode of csdr.",
),
CheckboxInput(
"csdr_print_bufsizes",
"",
checkboxText="Print buffer sizez",
infotext="This prints the buffer sizes used for csdr processes.",
),
CheckboxInput(
"csdr_through",
"",
checkboxText="Print throughput",
infotext="Enabling this will print out how much data is going into the DSP chains.",
),
),
Section(
"Map settings",
TextInput(
"google_maps_api_key",
"Google Maps API key",
infotext="Google Maps requires an API key, check out "
+ '<a href="https://developers.google.com/maps/documentation/embed/get-api-key" target="_blank">'
+ "their documentation</a> on how to obtain one.",
),
NumberInput(
"map_position_retention_time",
"Map retention time",
infotext="Unit is seconds<br/>Specifies how log markers / grids will remain visible on the map",
),
),
Section(
"Decoding settings",
NumberInput("decoding_queue_workers", "Number of decoding workers"),
NumberInput("decoding_queue_length", "Maximum length of decoding job queue"),
NumberInput(
"wsjt_decoding_depth",
"Default WSJT decoding depth",
infotext="A higher decoding depth will allow more results, but will also consume more cpu",
),
NumberInput(
"js8_decoding_depth",
"Js8Call decoding depth",
infotext="A higher decoding depth will allow more results, but will also consume more cpu",
),
Js8ProfileCheckboxInput(
"js8_enabled_profiles",
"Js8Call enabled modes"
),
),
Section(
"Background decoding",
CheckboxInput(
"services_enabled",
"Service",
checkboxText="Enable background decoding services",
),
ServicesCheckboxInput("services_decoders", "Enabled services"),
),
Section(
"APRS settings",
TextInput(
"aprs_callsign",
"APRS callsign",
infotext="This callsign will be used to send data to the APRS-IS network",
),
CheckboxInput(
"aprs_igate_enabled",
"APRS I-Gate",
checkboxText="Enable APRS receive-only I-Gate",
),
TextInput("aprs_igate_server", "APRS-IS server"),
TextInput("aprs_igate_password", "APRS-IS network password"),
CheckboxInput(
"aprs_igate_beacon",
"APRS beacon",
checkboxText="Send the receiver position to the APRS-IS network",
infotext="Please check that your receiver location is setup correctly",
),
),
Section(
"pskreporter settings",
CheckboxInput(
"pskreporter_enabled",
"Reporting",
checkboxText="Enable sending spots to pskreporter.info",
),
TextInput(
"pskreporter_callsign",
"pskreporter callsign",
infotext="This callsign will be used to send spots to pskreporter.info",
),
),
]
def render_sections(self):
sections = "".join(section.render() for section in GeneralSettingsController.sections)
return """
<form class="settings-body" method="POST">
{sections}
<div class="buttons">
<button type="submit" class="btn btn-primary">Apply</button>
</div>
</form>
""".format(
sections=sections
)
def indexAction(self):
self.serve_template("generalsettings.html", **self.template_variables())
def template_variables(self):
variables = super().template_variables()
variables["sections"] = self.render_sections()
return variables
def processFormData(self):
data = parse_qs(self.get_body().decode("utf-8"))
data = {
k: v for i in GeneralSettingsController.sections for k, v in i.parse(data).items()
}
config = Config.get()
for k, v in data.items():
config[k] = v
Config.store()
self.send_redirect("/admin")

View File

@ -0,0 +1,50 @@
from . import Controller
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
logger = logging.getLogger(__name__)
class StatusController(Controller):
def getProfileStats(self, profile):
return {
"name": profile["name"],
"center_freq": profile["center_freq"],
"sample_rate": profile["samp_rate"],
}
def getReceiverStats(self, receiver):
stats = {
"name": receiver.getName(),
# TODO would be better to have types from the config here
"type": type(receiver).__name__,
"profiles": [self.getProfileStats(p) for p in receiver.getProfiles().values()]
}
return stats
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"],
"admin": pm["receiver_admin"],
"gps": pm["receiver_gps"],
"asl": pm["receiver_asl"],
"location": pm["receiver_location"],
},
"max_clients": pm["max_clients"],
"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)

View File

@ -0,0 +1,44 @@
from . import Controller
import pkg_resources
from string import Template
from owrx.config import Config
class TemplateController(Controller):
def render_template(self, file, **vars):
file_content = pkg_resources.resource_string("htdocs", file).decode("utf-8")
template = Template(file_content)
return template.safe_substitute(**vars)
def serve_template(self, file, **vars):
self.send_response(self.render_template(file, **vars), content_type="text/html")
def default_variables(self):
return {}
class WebpageController(TemplateController):
def template_variables(self):
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>"""
header = self.render_template("include/header.include.html", settingslink=settingslink)
return {"header": header}
class IndexController(WebpageController):
def indexAction(self):
self.serve_template("index.html", **self.template_variables())
class MapController(WebpageController):
def indexAction(self):
# TODO check if we have a google maps api key first?
self.serve_template("map.html", **self.template_variables())
class FeatureController(WebpageController):
def indexAction(self):
self.serve_template("features.html", **self.template_variables())

View File

@ -0,0 +1,10 @@
from . import Controller
from owrx.websocket import WebSocketConnection
from owrx.connection import WebSocketMessageHandler
class WebSocketController(Controller):
def indexAction(self):
conn = WebSocketConnection(self.handler, WebSocketMessageHandler())
# enter read loop
conn.handle()

21
owrx/details.py Normal file
View File

@ -0,0 +1,21 @@
from owrx.config import Config
from owrx.locator import Locator
from owrx.property import PropertyFilter
class ReceiverDetails(PropertyFilter):
def __init__(self):
super().__init__(
Config.get(),
"receiver_name",
"receiver_location",
"receiver_asl",
"receiver_gps",
"photo_title",
"photo_desc",
)
def __dict__(self):
receiver_info = super().__dict__()
receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
return receiver_info

View File

@ -1,9 +1,11 @@
from owrx.config import PropertyManager
from owrx.meta import MetaParser from owrx.meta import MetaParser
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
from owrx.js8 import Js8Parser
from owrx.aprs import AprsParser from owrx.aprs import AprsParser
from owrx.pocsag import PocsagParser from owrx.pocsag import PocsagParser
from owrx.source import SdrSource from owrx.source import SdrSource
from owrx.property import PropertyStack, PropertyLayer
from owrx.modes import Modes
from csdr import csdr from csdr import csdr
import threading import threading
@ -21,26 +23,38 @@ class DspManager(csdr.output):
"wsjt_demod": WsjtParser(self.handler), "wsjt_demod": WsjtParser(self.handler),
"packet_demod": AprsParser(self.handler), "packet_demod": AprsParser(self.handler),
"pocsag_demod": PocsagParser(self.handler), "pocsag_demod": PocsagParser(self.handler),
"js8_demod": Js8Parser(self.handler),
} }
self.localProps = ( self.props = PropertyStack()
self.sdrSource.getProps() # local demodulator properties not forwarded to the sdr
.collect( self.props.addLayer(0, PropertyLayer().filter(
"audio_compression", "output_rate",
"fft_compression", "squelch_level",
"digimodes_fft_size", "secondary_mod",
"csdr_dynamic_bufsize", "low_cut",
"csdr_print_bufsizes", "high_cut",
"csdr_through", "offset_freq",
"digimodes_enable", "mod",
"samp_rate", "secondary_offset_freq",
"digital_voice_unvoiced_quality", "dmr_filter",
"dmr_filter", ))
"temporary_directory", # properties that we inherit from the sdr
"center_freq", self.props.addLayer(1, self.sdrSource.getProps().filter(
) "audio_compression",
.defaults(PropertyManager.getSharedInstance()) "fft_compression",
) "digimodes_fft_size",
"csdr_dynamic_bufsize",
"csdr_print_bufsizes",
"csdr_through",
"digimodes_enable",
"samp_rate",
"digital_voice_unvoiced_quality",
"temporary_directory",
"center_freq",
"start_mod",
"start_freq",
))
self.dsp = csdr.dsp(self) self.dsp = csdr.dsp(self)
self.dsp.nc_port = self.sdrSource.getPort() self.dsp.nc_port = self.sdrSource.getPort()
@ -56,34 +70,47 @@ class DspManager(csdr.output):
self.dsp.set_bpf(*bpf) self.dsp.set_bpf(*bpf)
def set_dial_freq(key, value): def set_dial_freq(key, value):
freq = self.localProps["center_freq"] + self.localProps["offset_freq"] freq = self.props["center_freq"] + self.props["offset_freq"]
for parser in self.parsers.values(): for parser in self.parsers.values():
parser.setDialFrequency(freq) parser.setDialFrequency(freq)
if "start_mod" in self.props:
self.dsp.set_demodulator(self.props["start_mod"])
mode = Modes.findByModulation(self.props["start_mod"])
if mode and mode.bandpass:
self.dsp.set_bpf(mode.bandpass.low_cut, mode.bandpass.high_cut)
else:
self.dsp.set_bpf(-4000, 4000)
if "start_freq" in self.props and "center_freq" in self.props:
self.dsp.set_offset_freq(self.props["start_freq"] - self.props["center_freq"])
else:
self.dsp.set_offset_freq(0)
self.subscriptions = [ self.subscriptions = [
self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression), self.props.wireProperty("audio_compression", self.dsp.set_audio_compression),
self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression), self.props.wireProperty("fft_compression", self.dsp.set_fft_compression),
self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size), self.props.wireProperty("digimodes_fft_size", self.dsp.set_secondary_fft_size),
self.localProps.getProperty("samp_rate").wire(self.dsp.set_samp_rate), self.props.wireProperty("samp_rate", self.dsp.set_samp_rate),
self.localProps.getProperty("output_rate").wire(self.dsp.set_output_rate), self.props.wireProperty("output_rate", self.dsp.set_output_rate),
self.localProps.getProperty("offset_freq").wire(self.dsp.set_offset_freq), self.props.wireProperty("offset_freq", self.dsp.set_offset_freq),
self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level), self.props.wireProperty("center_freq", self.dsp.set_center_freq),
self.localProps.getProperty("low_cut").wire(set_low_cut), self.props.wireProperty("squelch_level", self.dsp.set_squelch_level),
self.localProps.getProperty("high_cut").wire(set_high_cut), self.props.wireProperty("low_cut", set_low_cut),
self.localProps.getProperty("mod").wire(self.dsp.set_demodulator), self.props.wireProperty("high_cut", set_high_cut),
self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality), self.props.wireProperty("mod", self.dsp.set_demodulator),
self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter), self.props.wireProperty("digital_voice_unvoiced_quality", self.dsp.set_unvoiced_quality),
self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory), self.props.wireProperty("dmr_filter", self.dsp.set_dmr_filter),
self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq), self.props.wireProperty("temporary_directory", self.dsp.set_temporary_directory),
self.props.filter("center_freq", "offset_freq").wire(set_dial_freq),
] ]
self.dsp.set_offset_freq(0) self.dsp.csdr_dynamic_bufsize = self.props["csdr_dynamic_bufsize"]
self.dsp.set_bpf(-4000, 4000) self.dsp.csdr_print_bufsizes = self.props["csdr_print_bufsizes"]
self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"] self.dsp.csdr_through = self.props["csdr_through"]
self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"]
self.dsp.csdr_through = self.localProps["csdr_through"]
if self.localProps["digimodes_enable"]: if self.props["digimodes_enable"]:
def set_secondary_mod(mod): def set_secondary_mod(mod):
if mod == False: if mod == False:
@ -92,17 +119,19 @@ class DspManager(csdr.output):
if mod is not None: if mod is not None:
self.handler.write_secondary_dsp_config( self.handler.write_secondary_dsp_config(
{ {
"secondary_fft_size": self.localProps["digimodes_fft_size"], "secondary_fft_size": self.props["digimodes_fft_size"],
"if_samp_rate": self.dsp.if_samp_rate(), "if_samp_rate": self.dsp.if_samp_rate(),
"secondary_bw": self.dsp.secondary_bw(), "secondary_bw": self.dsp.secondary_bw(),
} }
) )
self.subscriptions += [ self.subscriptions += [
self.localProps.getProperty("secondary_mod").wire(set_secondary_mod), self.props.wireProperty("secondary_mod", set_secondary_mod),
self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq), self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq),
] ]
self.startOnAvailable = False
self.sdrSource.addClient(self) self.sdrSource.addClient(self)
super().__init__() super().__init__()
@ -110,6 +139,8 @@ class DspManager(csdr.output):
def start(self): def start(self):
if self.sdrSource.isAvailable(): if self.sdrSource.isAvailable():
self.dsp.start() self.dsp.start()
else:
self.startOnAvailable = True
def receive_output(self, t, read_fn): def receive_output(self, t, read_fn):
logger.debug("adding new output of type %s", t) logger.debug("adding new output of type %s", t)
@ -128,13 +159,18 @@ class DspManager(csdr.output):
def stop(self): def stop(self):
self.dsp.stop() self.dsp.stop()
self.startOnAvailable = False
self.sdrSource.removeClient(self) self.sdrSource.removeClient(self)
for sub in self.subscriptions: for sub in self.subscriptions:
sub.cancel() sub.cancel()
self.subscriptions = [] self.subscriptions = []
def setProperties(self, props):
for k, v in props.items():
self.setProperty(k, v)
def setProperty(self, prop, value): def setProperty(self, prop, value):
self.localProps.getProperty(prop).setValue(value) self.props[prop] = value
def getClientClass(self): def getClientClass(self):
return SdrSource.CLIENT_USER return SdrSource.CLIENT_USER
@ -142,7 +178,9 @@ class DspManager(csdr.output):
def onStateChange(self, state): def onStateChange(self, state):
if state == SdrSource.STATE_RUNNING: if state == SdrSource.STATE_RUNNING:
logger.debug("received STATE_RUNNING, attempting DspSource restart") logger.debug("received STATE_RUNNING, attempting DspSource restart")
self.dsp.start() if self.startOnAvailable:
self.dsp.start()
self.startOnAvailable = False
elif state == SdrSource.STATE_STOPPING: elif state == SdrSource.STATE_STOPPING:
logger.debug("received STATE_STOPPING, shutting down DspSource") logger.debug("received STATE_STOPPING, shutting down DspSource")
self.dsp.stop() self.dsp.stop()

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