680 Commits

Author SHA1 Message Date
0c9d37e381 Merge branch 'develop' into release-1.0 2021-05-08 21:10:58 +02:00
dc848a7006 fix bandwidth calculation for single-service groups 2021-05-08 00:59:57 +02:00
093ad6cd0d improve oversampling for resampling 2021-05-08 00:38:00 +02:00
fd26acca68 don't resample when there's only one service 2021-05-08 00:37:30 +02:00
3daf005c81 Merge branch 'develop' into release-1.0 2021-05-07 17:53:51 +02:00
1b31c5fc90 keep the spinner visible while the image loads 2021-05-07 17:44:24 +02:00
0206a6f94c introduce spinner during file uploads 2021-05-07 17:33:10 +02:00
484b829b90 fix problem when switching image file types 2021-05-07 17:19:11 +02:00
ad8877f83c add webp support for uploadable images 2021-05-07 16:57:54 +02:00
e205953bfc short description should be a question (lintian) 2021-05-06 19:39:58 +02:00
8a7182f9d5 update docker build versions 2021-05-06 19:30:07 +02:00
f86487f459 prepare release 1.0.0 2021-05-06 19:27:43 +02:00
7fc7fe5e82 fix audio chopper mode timestamp problem 2021-05-05 22:55:20 +02:00
3057c3ffd7 make the circle a little bit smaller to improve rendering 2021-05-05 20:00:27 +02:00
282ba4d095 move play button overlay to javascript to avoid downloading the image 2021-05-05 19:56:14 +02:00
1b4b87b14e replace play button with an svg 2021-05-05 19:27:03 +02:00
55254b1c44 compress png images for performance 2021-05-05 18:43:24 +02:00
cd935c0dcb check for empty return 2021-05-04 16:05:44 +02:00
a17690dc91 clear session cookie if invalid 2021-05-03 23:22:28 +02:00
fe1a1207e6 implement session timeout 2021-05-03 23:07:27 +02:00
041e8930bf don't send native deletions 2021-05-03 19:28:03 +02:00
d9fe604171 improve error handling on file switches 2021-05-02 00:07:24 +02:00
290f67735d improve decoding file switchover 2021-05-02 00:06:50 +02:00
0fa8774493 increase bandwidth for digital modes to 12.5 2021-05-01 18:27:15 +02:00
53c5c0f045 add a latencyHint to improve audio playback 2021-05-01 16:55:08 +02:00
11568256ed remove unused imports 2021-05-01 16:51:02 +02:00
2152184bf9 fix compatibility issues with python 3.5 2021-05-01 16:49:53 +02:00
f8971ac704 protect against low-level errors during switching 2021-04-30 01:20:33 +02:00
540198b12a 96kHz is reported as working, too - refs #201 2021-04-29 20:15:51 +02:00
48d498941e fix url for image replacement, too 2021-04-29 19:53:43 +02:00
318cb728e1 fix imageupload path 2021-04-29 19:41:06 +02:00
f481c3f8e3 implement image upload error handling 2021-04-29 19:07:10 +02:00
af553c422d implement file size upload limit 2021-04-29 18:18:18 +02:00
7115d5c951 prefer native sample rate, if good - closes #201 2021-04-29 16:23:51 +02:00
7642341b2e fix checkbox labels when removing their optional fields 2021-04-29 15:34:46 +02:00
29bce9e07a refactor: move form stuff out of source code 2021-04-29 15:28:18 +02:00
35dcff90ea refactor owrx.form -> owrx.form.input 2021-04-29 15:17:21 +02:00
bc193c834c use a number display to avoid wrong input and support locales 2021-04-28 23:03:03 +02:00
3bc39a9ca3 fix "NaN" display problem 2021-04-28 22:44:33 +02:00
4a77d2cc38 fill error variable with an empty string for the device list 2021-04-27 23:19:48 +02:00
a7e2aae292 reset initial demodulator params on reconnects 2021-04-27 23:13:44 +02:00
c6e01eed1a implement top-level error handling 2021-04-27 18:23:59 +02:00
118335b2b6 lock on dsp to avoid race conditions 2021-04-27 16:58:23 +02:00
0c7b0d2eaa improve dsp control handling 2021-04-27 01:58:20 +02:00
cb8ec3c760 improve sdr device state handling 2021-04-27 01:44:30 +02:00
e408c66702 switch condition sequencing to get better error messages 2021-04-27 01:15:56 +02:00
d97d66c787 move logging config to the top again 2021-04-27 00:53:45 +02:00
96ada02e38 initialize logging first 2021-04-27 00:47:33 +02:00
ae729990ca let's see if we can override the loglevel this way 2021-04-27 00:45:14 +02:00
afc4fc2d00 improve logging configuration 2021-04-27 00:33:52 +02:00
25d04f4cbc exclude keys that have been moved to openwebrx.conf 2021-04-26 21:27:15 +02:00
5a60869f8e check for contents of bookmark files to improve migration 2021-04-26 21:05:33 +02:00
7962da9454 initialize settings 2021-04-26 20:10:44 +02:00
4691987cc4 fix config command 2021-04-26 19:34:50 +02:00
05985ff46a add command for explicit migration 2021-04-26 19:27:12 +02:00
159c231884 types don't play that much of a role any more 2021-04-25 21:46:00 +02:00
86e64225bd credit @jancona in the changelog 2021-04-25 21:45:29 +02:00
1156916631 setup S6 to accept openwebrx arguments on docker run 2021-04-25 19:25:54 +02:00
a6ed578a0f handle sdr device and profile name changes 2021-04-25 00:48:45 +02:00
8c5546ad90 remove debugging again 2021-04-24 20:17:55 +02:00
f3ed4a719a fix command 2021-04-24 20:14:25 +02:00
2da2a57e13 change password if user already exists 2021-04-24 20:12:39 +02:00
6de91c0c4e let's try this way 2021-04-24 20:07:08 +02:00
cc3e43c6cd enable reconfigure 2021-04-24 20:04:07 +02:00
d04cf5f5a1 add the necessary template 2021-04-24 19:52:20 +02:00
b7e38960c0 handle config key not set 2021-04-24 19:48:42 +02:00
1e684f9bf1 debug config script, too 2021-04-24 19:46:19 +02:00
259d036083 enable debugging to see what's wrong 2021-04-24 19:42:42 +02:00
71b0fa968b merge openwebrx-admin into openwebrx 2021-04-24 19:39:48 +02:00
6ad3a80fc6 update digiham dependency to 0.4 (improved dc blocker) 2021-04-23 18:51:39 +02:00
b1cfe79ddd both ubuntu and debian have js8call packages, so add it to recommended 2021-04-23 18:35:52 +02:00
5e6508cd47 update with JS8 frequency on 11m 2021-04-23 17:20:37 +02:00
5f5881cdfa update owrx_connector in docker 2021-04-21 23:21:47 +02:00
f6b0e37664 add the ability to set admin user and pass via env variables for docker 2021-04-21 14:29:36 +02:00
1bc5633b27 update digiham 2021-04-20 17:49:41 +02:00
1c23fdf3ff update m17-cxx-demod 2021-04-20 01:17:18 +02:00
bd29f9c572 dc_block is part of the digiham package 2021-04-20 01:07:06 +02:00
89cd17042a re-introduce (improved) dc blocker to allow slightly off-frequency
signals
2021-04-20 01:06:01 +02:00
8b5cf9983e display a hint if no bookmarks are in the system 2021-04-18 21:15:02 +02:00
04a5e6705f remove bookmarks from distribution 2021-04-18 19:30:49 +02:00
77de488521 mark last breadcrumb active 2021-04-18 19:25:29 +02:00
52b535c608 remove id input from new profile page 2021-04-18 19:17:27 +02:00
05ea11f5d1 introduce generated device ids 2021-04-18 19:04:43 +02:00
e8cf014903 introduce breadcrumbs in the web config 2021-04-18 17:49:13 +02:00
1968e15237 fix for submit for path routed environments 2021-04-18 16:30:02 +02:00
da698e7a3c fix login for path routed environment 2021-04-18 15:59:05 +02:00
b9db64d4f9 fix device links for path-route environments 2021-04-18 15:40:46 +02:00
51af299aa2 merge in updates from receiverbook 2021-04-18 01:41:13 +02:00
440b3a3822 remove config_webrx.py from docker images, too 2021-04-18 01:01:48 +02:00
5ec0005f81 remove digimodes_enable setting since it no longer works 2021-04-18 00:50:13 +02:00
11b0d2d90a add deprecation notice 2021-04-18 00:49:38 +02:00
322a52e854 remove config_webrx.py from debian package 2021-04-18 00:08:34 +02:00
1b8153c461 rename default profiles 2021-04-18 00:03:18 +02:00
dae32f2e95 return an empty layer if no config is available 2021-04-17 23:56:32 +02:00
b4c2923dd2 add some info text 2021-04-17 18:00:13 +02:00
68739724d4 make the sdr type dropdown show beautiful names 2021-04-17 17:42:08 +02:00
4993a56235 use a single connection to avoid the managing overhead 2021-04-11 21:04:13 +02:00
cb3cb50cbd fix chopper startup 2021-04-11 20:10:49 +02:00
7e4671afe4 Improve profile handling
* introduce profile sources
* subscriptions can handle config change events
* web config changes to profile changes will now take effect immediately
2021-04-11 18:46:21 +02:00
19c8432371 always perform shutdown tasks 2021-04-11 18:42:35 +02:00
9351e4793c merge AudioHandler and AudioChopper; split audio module 2021-04-11 14:40:28 +02:00
1f91908e06 maybe this will suit the unittest loader better 2021-04-10 02:12:18 +02:00
907359df82 fix js8 parser 2021-04-09 22:40:30 +02:00
e210c3a667 group audio writers by interval 2021-04-09 20:15:03 +02:00
9c4d7377d0 more type hints that don't work... circular imports... broken :( 2021-04-09 18:37:00 +02:00
8ce1192811 type hinting is invalid. this shouldn't work, but obviously type hinting
is broken. remove :(
2021-04-09 18:29:36 +02:00
d18a4c83ac don't send bookmarks if the parameters are not available 2021-04-09 18:29:08 +02:00
bbad34cec3 move wsjt/js8 decisions out of csdr 2021-04-09 18:16:25 +02:00
22ec80c8ea make decoding queue settings work from the web config 2021-04-07 18:57:42 +02:00
5487861da1 make wsprnet and pskreporter settings work from the web config 2021-04-07 17:54:14 +02:00
ebd4d93908 add note about background decoding 2021-04-07 16:23:13 +02:00
fcbaa4f22a implement aprs config changes 2021-04-07 16:20:10 +02:00
c0ca216e4d make "digimodes_fft_size" work from web config 2021-04-05 17:18:30 +02:00
a9990f1f41 remove redpitaya source (working with hpsdr_connector) 2021-04-05 15:48:03 +02:00
b877d8439a fix "remote" mappings for rtl_tcp and soapy_remote 2021-04-02 21:46:21 +02:00
6cca37a9df fix runds "remote" input mapping 2021-04-02 21:44:51 +02:00
7a2f62a307 fix name 2021-04-02 21:43:46 +02:00
1932890dd0 extended "blur" behavior (using body click events) 2021-03-31 02:01:13 +02:00
02e699c597 add pointer to frequency display to indicate clickability 2021-03-31 01:40:35 +02:00
46d742a12c add cursor to indicate scrollability 2021-03-31 01:38:53 +02:00
b3e99e0a3d prefix -> suffix; no tabstop 2021-03-31 01:36:02 +02:00
96cce831ef don't tab into the exponent selector input 2021-03-31 01:28:38 +02:00
3e00a4f390 remove map file declaration (not working) 2021-03-31 01:23:18 +02:00
0abd121fda inline location-picker 2021-03-31 01:22:39 +02:00
b605927207 update changelogs 2021-03-31 00:34:55 +02:00
3696272ef7 inline nmux_memory since i've never seen the need to change it 2021-03-31 00:23:36 +02:00
5a7c12dfac expose waterfall auto adjustment settings in web config 2021-03-31 00:18:06 +02:00
170b720e48 restructure config 2021-03-31 00:00:38 +02:00
c6962b4f42 change headline wording 2021-03-30 23:41:26 +02:00
8e7b758ef8 send personal bookmarks to the server 2021-03-30 18:50:30 +02:00
1b9e77982d make "new bookmark" api work with arrays 2021-03-30 18:30:08 +02:00
2d142e45ed implement dialog to import personal bookmarks 2021-03-30 18:19:23 +02:00
620ba11565 update wsjt-x patchset 2021-03-30 16:15:05 +02:00
e297cffbfe update to wsjt-x 2.3.1 2021-03-30 15:14:35 +02:00
af211739fb confirmation modal before deleting bookmarks 2021-03-28 16:51:34 +02:00
a86a2f31cd styling 2021-03-27 23:50:39 +01:00
6796699e35 don't redirect XHR calls to the login page, 403 instead 2021-03-27 23:45:21 +01:00
df72147b93 handle only successful results 2021-03-27 23:40:30 +01:00
65443eb0ba improve event handling 2021-03-27 23:40:10 +01:00
29c0f7148a re-work the bookmarks table to incorporate the improved frequency input 2021-03-27 23:08:43 +01:00
e1dd9d32f4 prevent javascript errors if frequency is NaN 2021-03-25 16:08:02 +01:00
287a04be94 send updated bookmarks to clients on the fly 2021-03-25 15:25:15 +01:00
20cd3f6efe more inputs that can display errors 2021-03-25 15:02:59 +01:00
69237c0bb4 make more inputs display errors 2021-03-25 14:48:09 +01:00
383c08ed48 implement tuning precision dropdown 2021-03-24 23:43:19 +01:00
19496d46a3 fix form evaluation for optional fields 2021-03-24 23:17:50 +01:00
6ddced4689 implement basic error handling and validation for forms 2021-03-24 22:46:51 +01:00
4cbce9c840 always remove device props on switch, fixes device failover 2021-03-24 20:47:04 +01:00
b01792c3d2 fix deletion of sdrs when there's no changes 2021-03-24 17:25:59 +01:00
5f7daba3b2 move the default sdrs to the new defaults file 2021-03-24 17:19:49 +01:00
a90f77e545 retain the redirect url on login failure 2021-03-24 16:53:01 +01:00
d50d08ad2c add a robots.txt to exclude certain routes for search engines 2021-03-24 16:08:13 +01:00
deeaccba12 profile as properties, live sync additions and removals with the client 2021-03-24 15:57:25 +01:00
62e67afc9c update config to version 6 2021-03-21 15:23:26 +01:00
c9d303c43e remove "configurable_keys" hack 2021-03-21 15:19:40 +01:00
5fc8672dd6 fix profile detection 2021-03-21 00:18:35 +01:00
acee318dae make the frontend resume when an sdr device becomes present 2021-03-21 00:14:18 +01:00
8fa1796037 re-start connection sdr if no sdr was available before 2021-03-20 23:30:09 +01:00
2a82f4e452 wire profile transmission into active sdr device hash 2021-03-20 23:14:29 +01:00
341e254640 fix shutdown iteration 2021-03-20 17:24:00 +01:00
d872152cc8 restore python 3.5 compatibility 2021-03-20 17:23:35 +01:00
3b9763eee5 fix device deletion 2021-03-20 02:16:08 +01:00
cfeab98620 hook up service handling to new device events 2021-03-20 01:56:07 +01:00
792f76f831 turn the dict of active sources into a living PropertyManager 2021-03-20 01:10:18 +01:00
c58ebfa657 readonly also prevents deletion 2021-03-20 00:54:45 +01:00
c50473fea5 implement device shutdown on deletion or lack of profiles 2021-03-18 22:59:46 +01:00
f1619b81fe use the right method 2021-03-18 22:24:53 +01:00
364c7eb505 show more information on the sdr settings page 2021-03-18 21:53:59 +01:00
9dcf342b13 fix scheduler behavior on enable / disable 2021-03-18 21:17:23 +01:00
d573561c67 activate enable / disable cycle 2021-03-18 19:59:10 +01:00
37e7331627 fix device failover (concurrent modification problem) 2021-03-18 19:47:11 +01:00
b25a673829 refactor state handling: uncouple failed and enabled flags 2021-03-18 19:34:53 +01:00
916f19ac60 mapping sdr device layer 2021-03-18 18:59:38 +01:00
620771eaf2 use a property layer right from the start 2021-03-18 18:58:29 +01:00
161408dbf4 handle deletions correctly 2021-03-06 23:48:31 +01:00
e0985c3802 fix status page 2021-03-06 23:34:27 +01:00
3d20e3ed80 simplify api by abstracting layer changes 2021-03-06 22:20:47 +01:00
6af0ad0262 fix frequency unit dropdown for firefox 2021-03-05 20:31:23 +01:00
b4460f4f70 fix receiver appearance in firefox 2021-03-05 20:20:22 +01:00
ff9f771e1b handle the resampler 2021-03-05 19:44:45 +01:00
4c5ec23ba7 remove profile list from sdr device index 2021-03-05 19:44:25 +01:00
1b44229ec3 clean up profile handling 2021-03-05 19:28:54 +01:00
2e28694b49 implement profile removal behaviour 2021-03-05 19:09:51 +01:00
2ba2ec38e0 new profile carousel implementation reacts to new profiles 2021-03-05 18:57:09 +01:00
a3cfde02c4 re-wire profile add & delete 2021-03-05 18:32:16 +01:00
a14f247859 make the add button look more like the remove button 2021-03-05 18:07:19 +01:00
45e9bd12a5 hightlight "new profile" link 2021-03-05 17:51:19 +01:00
190c90ccdf tab styling 2021-03-05 17:43:15 +01:00
60df3afe26 add tab navigation to profile and device pages 2021-03-04 22:14:10 +01:00
4e14b29537 apply type="button" on all buttons to make submit on enter work 2021-03-03 23:25:00 +01:00
3814200452 implement device and profile deletion 2021-03-03 23:07:41 +01:00
a9dbedee6d consistent wording 2021-03-03 22:35:57 +01:00
8671f98c14 implement "add profile" sequence 2021-03-03 22:33:37 +01:00
400ed3541d update "new sdr" routing too to avoid conflicts 2021-03-03 22:10:19 +01:00
03315d7960 switch url scheme to avoid conflicts 2021-03-03 21:55:49 +01:00
d123232f28 implement device and profile delete modals 2021-03-03 21:51:33 +01:00
eab1c6ce80 remove profile list from device page; make links work 2021-03-03 15:38:15 +01:00
fdbb76bca1 add working redirect after device add completes 2021-03-03 15:30:33 +01:00
c0b7cf5f8d resolve the ugly assets_prefix hack 2021-03-03 15:24:18 +01:00
37d89c074b implement "new device" page (redirects not working yet) 2021-03-03 00:16:28 +01:00
2b1dc76e48 add profile list to the device page 2021-03-02 20:28:49 +01:00
e0b289b6a5 remove debugging message 2021-03-02 20:19:48 +01:00
d81f0ae96c change display precision behavior to reference Hertz 2021-03-01 01:19:06 +01:00
6bd47cf914 implement property carousel for profile switching 2021-03-01 00:26:56 +01:00
c7db144f7b add name input for profiles 2021-02-28 21:26:55 +01:00
d0ddf72b10 fix typo 2021-02-28 21:04:43 +01:00
92cce78320 fix panel switching 2021-02-28 18:23:35 +01:00
1871fc359a apply some styling 2021-02-28 18:07:25 +01:00
a92ead3261 implement exponential frequency input on the receiver, too 2021-02-28 17:28:22 +01:00
094f470ebb automatically switch SI prefixes based on frequency 2021-02-28 15:51:07 +01:00
06b6054071 improve floating point handling 2021-02-27 23:21:14 +01:00
0537e23e38 make a more generic ExponentialInput and use that for the sample_rate input 2021-02-27 23:14:41 +01:00
7a0c934af5 use frequency input for the other inputs, too 2021-02-27 22:44:48 +01:00
e787336fc4 fix empty input 2021-02-27 22:43:18 +01:00
71acad3b4f add keyboard shortcuts for quicker input 2021-02-27 22:30:48 +01:00
c389d3b619 implement a frequency input with switchable exponent 2021-02-27 22:15:19 +01:00
ccdb010e9d more information on the sdr list 2021-02-27 20:48:37 +01:00
6a9bbf7bc9 wording change 2021-02-27 20:17:58 +01:00
ccba3e8597 fix positioning (still absolute, but not moving any more) 2021-02-27 17:23:03 +01:00
beb3d696c9 use transform / will-change properties for waterfall
* prevents expensive layout events in the browser
* allows the browser to optimize rendering
2021-02-27 17:06:53 +01:00
54142f4f15 allow squelch_auto_margin = 0 2021-02-27 01:23:59 +01:00
b6ed06dff4 use the new bottom bar for the bookmarks, too 2021-02-27 01:18:08 +01:00
36c4a16fb5 move to settings module 2021-02-27 01:16:03 +01:00
1b44c31a89 more space at the bottom 2021-02-27 01:13:57 +01:00
45d4d868d7 clear waterfall on fft_size change so that a setting change becomes visible immediately 2021-02-27 01:09:51 +01:00
e9cb5d54be send changed keys over websocket connection for the map 2021-02-27 01:00:38 +01:00
7dcafab2c1 restart on fft_compression changes, too 2021-02-27 00:29:04 +01:00
baef88bd94 restart demodulator on compression changes 2021-02-27 00:17:37 +01:00
ad3ed1e626 disconnect clients if the max_clients setting is lowered 2021-02-27 00:01:21 +01:00
0a76801a03 activate "service_decoder" setting 2021-02-26 23:50:58 +01:00
3164683e74 handle device shudown when schedule is off 2021-02-26 22:36:15 +01:00
4e7f02fc2c activate more scheduler and service settings 2021-02-26 21:27:42 +01:00
0231d98ab8 wire "services_enabled" setting 2021-02-26 17:53:32 +01:00
6822475674 exclude template inputs when moving to the visible section 2021-02-26 01:12:48 +01:00
412e0a51c7 implement property deletion handling; activate scheduler deletion 2021-02-26 01:12:03 +01:00
91c4d6f568 make scheduler respond to config changes 2021-02-25 22:19:05 +01:00
d8b3974728 use floats; explicit conversion 2021-02-25 20:50:40 +01:00
5cd9d386a6 combine waterfall_[min|max]_level into a single config 2021-02-25 15:13:39 +01:00
f6f0a87002 this todo is resolved 2021-02-25 00:38:23 +01:00
8c767be53a add inputs for perseus 2021-02-24 23:54:46 +01:00
bccb87e660 handle deletions in the top layer 2021-02-24 23:04:57 +01:00
0c1dc70217 Make the apply button always visible 2021-02-24 23:04:23 +01:00
388d9d46fe prevent runtime properties in the config 2021-02-24 22:30:28 +01:00
2785f43c6a implement adding and removing scheduler slots 2021-02-24 21:09:19 +01:00
45a70a1079 parse values from form 2021-02-24 20:17:43 +01:00
2d823b2945 render scheduler profile inputs 2021-02-24 19:56:07 +01:00
65758a0098 start implementing scheduler input (daylight works) 2021-02-24 17:12:23 +01:00
ea96038201 remove unused imports 2021-02-24 12:31:53 +01:00
ed3d84b974 use the container instead of a (potentially missing) canvas 2021-02-24 00:59:31 +01:00
710a18aae3 initialize canvas on demand to avoid overlap when changing parameters 2021-02-24 00:58:50 +01:00
f69d78926e create filtering that prevents overwriting the device name 2021-02-24 00:09:57 +01:00
4199a583f8 fix agc parameter 2021-02-23 23:24:30 +01:00
dfaecdb357 use hierarchical property layers to make config changes effective
immediately
2021-02-23 23:23:37 +01:00
631232fe7c make AGC optional 2021-02-23 20:02:38 +01:00
f9772faa6f add separator before the optional inputs dropdown 2021-02-23 19:23:54 +01:00
4e32d724c4 fix storing profiles 2021-02-23 18:41:49 +01:00
c5df6a1527 implement profile editing page 2021-02-23 18:32:23 +01:00
ed258cc9a0 fill in gain stages for hackrf 2021-02-23 17:40:06 +01:00
437943c26c fill in airspy gain stages 2021-02-23 17:36:16 +01:00
d15d9d8c76 remove implicit optional handling for optional fields 2021-02-23 00:27:29 +01:00
436010ffe3 implement explicit removal of non-present keys 2021-02-23 00:12:22 +01:00
679f99d701 change checkbox handling to detect presence 2021-02-23 00:11:51 +01:00
1eff7a3b69 fix typo 2021-02-22 23:52:57 +01:00
54a34b2084 implement optional device fields 2021-02-22 23:49:28 +01:00
f8beae5f46 fix javascript errors 2021-02-22 23:47:19 +01:00
9beb3b9168 remove the label attribute from the checkboxes 2021-02-22 00:57:02 +01:00
770fd749cd introduce the basic concept of optional keys 2021-02-22 00:35:47 +01:00
683a711b49 fix bias_tee for hackrf 2021-02-21 18:11:28 +01:00
bd31fa5149 add the ability to disable devices 2021-02-21 18:11:08 +01:00
7f3d421b25 introduce profile list 2021-02-20 23:45:06 +01:00
44250f9719 add some device details on the list page 2021-02-20 22:57:17 +01:00
c2e8ac516c introduce enums for state management 2021-02-20 22:54:07 +01:00
dd5ab32b47 set always-on default to false 2021-02-20 19:43:04 +01:00
361ed55b93 add more device-specific options 2021-02-20 19:20:31 +01:00
8b24eff72e add sdrplay specific options 2021-02-20 19:00:28 +01:00
18e8ca5e43 add bias_tee and direct_sampling options 2021-02-20 18:48:12 +01:00
0ab6729fcc create device descriptions for all 2021-02-20 18:09:24 +01:00
0e64f15e65 add more device inputs 2021-02-20 17:54:19 +01:00
058463a9b3 fix display and parsing issues 2021-02-20 00:36:18 +01:00
bd7e5b7166 implement individual gain stages option 2021-02-20 00:16:32 +01:00
d0d946e09f implement gain dialog with AGC option 2021-02-19 21:07:13 +01:00
86278ff44d wire data parsing and storage 2021-02-19 18:45:29 +01:00
039b57d28b add more inputs, bind to actual data 2021-02-19 18:18:25 +01:00
27c16c3720 add more inputs 2021-02-19 16:29:30 +01:00
3aa238727e start building device forms 2021-02-19 15:29:17 +01:00
4316832b95 input merging mechanism 2021-02-19 14:53:30 +01:00
bec61465c9 move device descriptions to owrx.source 2021-02-19 14:44:16 +01:00
012952f6f3 implement some basic infrastructure to present device forms 2021-02-19 00:46:52 +01:00
872c7a4bfd setup device list and routing for device pages 2021-02-19 00:03:25 +01:00
d65743f2ea rename template variable 2021-02-18 23:05:43 +01:00
c5585e290a undo javascript device configuration 2021-02-18 22:24:31 +01:00
54fde2c1c0 reuse existing template 2021-02-18 22:12:13 +01:00
d612792593 update permissions on write 2021-02-18 21:07:45 +01:00
0d77aaff26 restrict access to openwebrx users file 2021-02-18 20:57:41 +01:00
b06a629ffb fix variable substitution 2021-02-18 18:41:39 +01:00
a29d72d67f more details in the password dialog 2021-02-18 18:38:37 +01:00
1a6f738c97 fix permission problems on initial install 2021-02-18 18:28:12 +01:00
50e19085b0 don't use full path (lintian) 2021-02-18 17:28:00 +01:00
e70ff075ca fix pasword prompt (lintian) 2021-02-18 17:25:33 +01:00
34b369b200 restore unconditional confmodule 2021-02-18 17:09:08 +01:00
fc5d560345 don't need to check for command, if it's not there the result will be
the same
2021-02-18 17:04:45 +01:00
e8ad4588ce add debhelper token to postrm script (lintian) 2021-02-18 17:02:14 +01:00
74aea63b9b always remove password, no matter what the value 2021-02-18 16:14:45 +01:00
a750726459 new mechanism doesn't require any dummy values in the db 2021-02-18 16:14:15 +01:00
eb8b8c4a5a include confmodule only when needed, avoiding potential warnings 2021-02-18 16:08:22 +01:00
1956907d6d suppress errors during check 2021-02-18 16:04:56 +01:00
8f49337b81 don't use expansion to test 2021-02-18 16:01:13 +01:00
5e37b75cfb test for existence of admin user before asking questions 2021-02-18 15:55:55 +01:00
c09f17579c implement a command to test for a user's existence 2021-02-18 15:42:12 +01:00
06d4b24b09 handle config key not set 2021-02-18 15:27:05 +01:00
9492bbebbb un-silence 2021-02-18 01:42:06 +01:00
ad5166cf9e allow reconfigure in postinst 2021-02-18 01:36:04 +01:00
0714ce5703 parse password from env if available 2021-02-18 01:32:27 +01:00
2eec29db05 change debconf priority to high 2021-02-18 01:28:40 +01:00
3122077603 fix debconf password questions 2021-02-18 01:12:26 +01:00
518588885c make postrm executable 2021-02-18 01:00:47 +01:00
8271eddefb rename templates file 2021-02-18 00:26:52 +01:00
404f995e39 confmodule doesn't work with our bash parameters 2021-02-18 00:22:37 +01:00
8fcfa689ae add postinst/postrm integration 2021-02-18 00:13:58 +01:00
f488a01c78 linitian also finds spelling errors?!? 2021-02-17 23:45:22 +01:00
06361754b3 add config script 2021-02-17 23:39:16 +01:00
b7688c3c97 add infotext for custom html colors 2021-02-16 18:39:42 +01:00
691d88f841 waterfall config fine-adjustments
* hide the waterfall colors input when pre-defined color scheme is
  selected
* skip unparseable lines on custom color input
* fallback to black and white if custom color config is unusable
* always use the waterfall classes when sending changes to the client
2021-02-16 18:35:18 +01:00
9aebeb51f8 remove waterfall_colors unless scheme is custom 2021-02-16 18:12:10 +01:00
8d2763930b implement input for custom waterfall colors 2021-02-16 18:07:13 +01:00
409370aba2 implement custom waterfall option 2021-02-16 17:48:12 +01:00
9175629838 send waterfall colors to the client 2021-02-16 17:34:04 +01:00
3c0a26eaa8 prevent file corruption during json.dump 2021-02-16 17:17:09 +01:00
496e771e17 implement new waterfall color selection 2021-02-16 17:12:57 +01:00
c8496a2547 remove unused import 2021-02-16 15:59:31 +01:00
d3ba866800 comment config since it is now supported in the web config 2021-02-15 22:58:02 +01:00
8267aa8d9d implement removal 2021-02-15 22:57:21 +01:00
c2617fcfaf use a converter -> parsing done 2021-02-15 22:22:07 +01:00
1112334ea8 render inputs, mode dropdown 2021-02-15 22:14:56 +01:00
578f165bdc wording change 2021-02-15 20:20:53 +01:00
a664770881 change link targets to _blank 2021-02-15 20:20:32 +01:00
c0193e677c add an input for wsjt_decoding_depths 2021-02-15 20:19:43 +01:00
819790cbc8 prevent an endless loop when client has problematic audio 2021-02-15 18:03:16 +01:00
b2d4046d8a apply z-index layering to status bars to make them render correctly 2021-02-15 18:00:46 +01:00
28b1abfa40 fix missing unit 2021-02-15 17:33:47 +01:00
a72a11d3c7 fix old unsubscription todo 2021-02-15 17:25:46 +01:00
2d37f63f2c title should be a header for SEO 2021-02-15 17:16:55 +01:00
48a9c76c18 inline header variables 2021-02-15 17:12:17 +01:00
7f9c0539bb break out demodulation and decoding settings 2021-02-15 16:06:14 +01:00
e61dde7d0e separate background decoding 2021-02-15 15:56:17 +01:00
d998ab5c61 break out reporting into its own settings page 2021-02-15 15:49:44 +01:00
49640b5e33 generalize settings controller 2021-02-15 15:40:37 +01:00
391069653a split settings controller module (preparation to split general settings) 2021-02-15 15:29:02 +01:00
830d7ae656 fix ios 14.2 bug 2021-02-15 00:04:43 +01:00
48c594fdae implement bookmark deletion 2021-02-14 16:51:16 +01:00
29a161b7b7 add the "add bookmarks" function 2021-02-14 16:21:09 +01:00
9b1659d3dd remove index (unused) 2021-02-14 14:48:32 +01:00
dbf23baa45 wait for successful ajax call 2021-02-14 00:44:36 +01:00
3d97d362b5 implement bookmark storage 2021-02-14 00:41:03 +01:00
8ea4d11e9c make the bookmarks table editable 2021-02-13 23:53:16 +01:00
48f26d00d6 add action column 2021-02-13 18:41:42 +01:00
3b60e0b737 display existing bookmarks in table 2021-02-13 18:35:15 +01:00
3e4ba42aab style settings page; add bookmark editor page 2021-02-13 17:08:56 +01:00
cda43b5c5c re-route settings urls 2021-02-13 16:44:14 +01:00
ae76470612 auto-reload bookmarks from file 2021-02-13 01:29:21 +01:00
5e51beac46 implement auto-reloading for bookmarks 2021-02-13 01:10:36 +01:00
8acfb8c1cf add configuration for max_client limit 2021-02-13 00:52:08 +01:00
ad0ca114f5 switch to subparsers 2021-02-12 18:34:28 +01:00
3f3f5eacfe no need to be verbose here 2021-02-12 17:45:10 +01:00
dd2fda54d1 add logging setup for owrxadmin 2021-02-12 17:00:51 +01:00
7d88d83c36 handle empty file 2021-02-12 17:00:35 +01:00
5068bcd347 run black 2021-02-11 23:08:19 +01:00
024a6684ce fix undefined variable 2021-02-11 23:07:45 +01:00
aad757df36 remove experimental csdr settings 2021-02-11 22:51:50 +01:00
690eed5d58 update changelog 2021-02-11 22:44:55 +01:00
c3d459558a prevent accidental text selection 2021-02-11 21:59:30 +01:00
fb457ce9f1 comment all config keys that are now in the web config 2021-02-11 19:42:23 +01:00
a8c93fd8d1 enable web config 2021-02-11 19:37:45 +01:00
f23fa59ac3 implement config layering 2021-02-11 19:31:44 +01:00
e926611307 break config module apart 2021-02-11 13:55:06 +01:00
1cc4b13ba6 add newline (lintian) 2021-02-11 00:29:31 +01:00
fdfaed005b add data directory volume definition (for whatever it's worth) 2021-02-11 00:25:31 +01:00
0cf67d5e2c don't use recursive (lintian) 2021-02-11 00:24:02 +01:00
0fd172edc3 check file contents; work with file extensions 2021-02-11 00:20:17 +01:00
64f827d235 loopify 2021-02-10 22:25:43 +01:00
1e72485425 implement temporary file cleanup 2021-02-10 22:24:43 +01:00
7097dc1cd8 ability to restore original image 2021-02-10 21:29:46 +01:00
8cf9b509c1 apply authorization to image upload 2021-02-10 20:32:07 +01:00
17c20d12e0 refactor authentication / authorization into a mixin 2021-02-10 20:21:45 +01:00
8422a33081 add information note about caching 2021-02-09 18:06:32 +01:00
75418baf06 apply cachebuster for form 2021-02-09 18:00:56 +01:00
9f17c941d1 generalize image upload form element 2021-02-09 17:54:02 +01:00
779aa33a4a add and resolve todos 2021-02-09 00:47:09 +01:00
7aa0f8b35d improve image handling 2021-02-09 00:38:59 +01:00
3b670016be implement uploading of top panorama, too 2021-02-09 00:12:53 +01:00
ad5daaae95 add exception for uploaded images 2021-02-08 23:44:10 +01:00
16d0e1a0d7 implement handling of uploaded files on save 2021-02-08 23:36:46 +01:00
4df5f19bd6 add todos 2021-02-08 23:30:44 +01:00
a1c024bfe2 implement dynamic file upload 2021-02-08 23:29:24 +01:00
2d72055070 organize 2021-02-08 20:30:12 +01:00
331e9627d6 implement forced password change for generated passwords 2021-02-08 18:30:54 +01:00
ed6594401c monitor user file modifications & reload if necessary 2021-02-08 17:24:59 +01:00
d9578cc5f4 thoroughly validate user 2021-02-08 17:09:22 +01:00
2c6b0e3d30 implement user list, enable, disable 2021-02-08 17:04:55 +01:00
b0c7abe362 implement form result parsing for q65 matrix 2021-02-08 16:32:00 +01:00
346f2af2fb update matrix generation with new abilities 2021-02-08 16:22:23 +01:00
902fc666c2 stricter q65 mode parsing and availability check 2021-02-08 15:58:37 +01:00
3a1e5ee73c avoid using tuples, they don't work in json (future config system) 2021-02-08 15:34:55 +01:00
a083042002 implement display of Q65 mode matrix 2021-02-08 15:16:04 +01:00
ce48892173 make dropdowns work with enums directly 2021-02-08 01:16:02 +01:00
5cfacac6c0 add aprs_igate_dir option 2021-02-08 01:00:00 +01:00
4758672c94 add aprs_igate_symbol 2021-02-08 00:43:39 +01:00
23fceb2998 add optional aprs fields and todos 2021-02-07 23:15:57 +01:00
e5bd78fd0c add fst4 and fst4w interval settings 2021-02-07 22:49:11 +01:00
8c4b9dd08a add settings for frequency_display_resolution and squelch_auto_margin 2021-02-07 22:40:03 +01:00
0517a59308 fix login page layout 2021-02-07 22:36:03 +01:00
ba3a68c3fa a bit of styling for the settings 2021-02-07 22:09:06 +01:00
d920540021 fix receiver_keys textarea 2021-02-07 21:45:02 +01:00
47ecc26f28 add a wfm tau dropdown to the web settings 2021-02-07 21:36:08 +01:00
689cd49694 drop "experimental pipe settings" (will become unavailable in the
future)
2021-02-07 18:23:17 +01:00
b60a8a1af0 add the ability to put append a unit to inputs 2021-02-07 18:21:57 +01:00
8de70cd523 add receiver_keys to the settings page 2021-02-07 18:04:46 +01:00
25db7c716d change heading 2021-02-07 17:36:44 +01:00
88020b894e move aprs_symbols_path to new config 2021-02-07 00:21:57 +01:00
ee687d4e27 fix copy&paste fail 2021-02-06 23:17:43 +01:00
b318b5e88a remove temporary directory from old config 2021-02-06 22:53:12 +01:00
8a25718d29 create config overrides directory 2021-02-06 22:31:02 +01:00
617bed91c4 fix config verification 2021-02-06 22:08:27 +01:00
9357d57a28 move temporary_directyr to core config; implement override logic 2021-02-06 21:55:47 +01:00
5d291b5b36 add pskreporter settings mappings 2021-02-06 21:01:59 +01:00
01c58327aa implement password reset command 2021-02-06 19:12:44 +01:00
635bf55465 format 2021-02-06 19:03:28 +01:00
732985c529 add help 2021-02-06 19:02:50 +01:00
9c5858e1e5 change wording 2021-02-06 19:01:14 +01:00
1fed499b7f create initial user in postinst script 2021-02-06 18:59:01 +01:00
d99669b3aa add "silent" flag to openwebrx-admin 2021-02-06 18:57:51 +01:00
e548d6a5de random salt for passwords 2021-02-06 18:43:37 +01:00
8806dc538e implement hashed passwords 2021-02-06 18:38:49 +01:00
f6f01ebee5 default password implementation 2021-02-06 18:22:13 +01:00
1d9ab1494f remove web_port from config 2021-02-06 18:17:37 +01:00
7054ec5d59 remove old users file from distribution 2021-02-06 18:15:55 +01:00
d72027e630 implement user deletion 2021-02-06 18:15:02 +01:00
99fe232a21 include command to create a user 2021-02-06 18:04:32 +01:00
dd2f0629d3 rename 2021-02-06 16:44:40 +01:00
ffcf5c0c27 create owrxadmin 2021-02-06 16:43:54 +01:00
3226c01f60 introduce core config file (settings that cannot be edited from the web) 2021-02-06 16:38:03 +01:00
54fb58755d add openwebrx data directory for persistent files 2021-02-06 15:50:50 +01:00
d9b662106c rename class 2021-02-05 17:58:27 +01:00
53faca64c0 clean up header styles 2021-02-05 17:56:02 +01:00
c23acc1513 automatically align 2021-02-05 17:22:43 +01:00
8e4716f241 drop empty Q65 decodes 2021-02-05 01:07:09 +01:00
e8fca853df unsubscribe on close; self-referencing prevents unsubscription 2021-02-04 18:00:03 +01:00
d6d6d97a13 add Q65 mode integration 2021-02-03 20:11:07 +01:00
e66be7c12d add feature definition for wsjt-x 2.4 2021-02-03 19:33:02 +01:00
56a42498a5 add frequencies for Q65 on available bands 2021-02-03 19:26:41 +01:00
bda718cbee update runds_connector 2021-02-03 17:09:51 +01:00
13eaee5ee9 replace eb200 with runds 2021-02-03 03:21:09 +01:00
44270af88f remove unused files to save space 2021-02-01 23:56:47 +01:00
bb680293a1 update m17 2021-02-01 23:56:35 +01:00
1ee75295e5 update to wsjtx 2.3.0 2021-02-01 23:56:09 +01:00
5e1c4391c6 include prometheus metrics, refs #200 2021-02-01 18:43:14 +01:00
998092f377 reroute /metrics to /metrics.json 2021-02-01 18:26:26 +01:00
dea07cd49b update connectors again 2021-02-01 13:37:01 +01:00
e3f99d6985 update eb200_connector, too 2021-01-31 23:35:05 +01:00
081b63def3 update connector with 32bit fixes 2021-01-31 23:05:36 +01:00
3c91f3cc2f add a timeout to wspr uploads 2021-01-31 20:31:54 +01:00
61a5250792 fix typos 2021-01-30 16:18:30 +01:00
881637811f switch when profile OR sdr has changed 2021-01-30 16:17:05 +01:00
142ca578ec truncate waterfall only when profile has changed 2021-01-30 16:04:29 +01:00
ad8ff1c2f7 send "sdr_id" to be able to detect changes 2021-01-30 16:04:13 +01:00
8372f198db add the ability to make a layer readonly 2021-01-30 16:03:35 +01:00
2a5448f5c1 update dsd feature detection to avoid start-up hangs 2021-01-30 15:03:52 +01:00
c8695a8e62 Merge branch 'master' into develop 2021-01-26 17:34:41 +01:00
477b457be9 update the version 2021-01-26 16:53:22 +01:00
58b35ec0f9 update changelogs for 0.20.3 2021-01-26 16:28:56 +01:00
9b2947827a Merge branch 'release-0.20' into develop 2021-01-25 19:40:28 +01:00
ae0748952f remove unused import, too 2021-01-25 19:40:06 +01:00
bee0f67efd Merge branch 'release-0.20' into develop 2021-01-25 19:37:57 +01:00
f81cf3570a don't check the type since older python doesn't have re.Pattern 2021-01-25 19:36:55 +01:00
612345f0b2 Merge branch 'master' into develop 2021-01-25 14:34:03 +01:00
4a86af69d1 Fix merging error 2021-01-24 23:20:17 +01:00
bf31a27dca Merge branch 'fix_arbitrary_code_execution' into develop 2021-01-24 22:55:11 +01:00
a5bdf6c3ac Merge branch 'fix_arbitrary_code_execution' into develop 2021-01-24 22:47:08 +01:00
9258e76468 fix typo 2021-01-24 00:37:49 +01:00
1d9b2729ef add server version to log information 2021-01-23 16:43:51 +01:00
999d32fd8a Merge pull request #210 from legacycode/add-documentation
Added documentation to APRS section
2021-01-23 15:11:37 +01:00
642552cc08 Added documentation to APRS 2021-01-23 08:47:39 +01:00
a0d219d120 protect against parser errors to prevent queue backlogging 2021-01-22 19:48:31 +01:00
68a1abd37e keep intermediate sample rate down to a minimum 2021-01-22 18:47:34 +01:00
bcab2b2288 update copyright notices 2021-01-22 18:10:51 +01:00
b8868cb55a move overlays to separate z-index to fix locator grid colors 2021-01-22 18:07:02 +01:00
f29f7b20e3 change shutdown handling to be able to join() 2021-01-22 17:34:35 +01:00
ae1287b8a2 remove faulty dependency 2021-01-22 17:34:09 +01:00
185fdb67cb handle SIGTERM 2021-01-22 17:33:53 +01:00
0ed69ef2f7 add viewport declaration 2021-01-20 23:09:56 +01:00
655b6849b7 prevent labels from being selected 2021-01-20 22:26:19 +01:00
39757b00b2 update changelog 2021-01-20 22:24:16 +01:00
64b7b485b3 run the code formatter over all 2021-01-20 17:01:46 +01:00
f0dc2f8ebe format code 2021-01-20 16:46:55 +01:00
55e1aa5857 use the property stack the way it's intended for better consistency 2021-01-20 16:46:29 +01:00
fe45d139ad fix an unset property error 2021-01-20 16:41:53 +01:00
181855e7a4 add filtering capability to the map 2021-01-20 00:39:34 +01:00
5d3d6423ed fix ysf images; remove obsolete code 2021-01-19 22:04:33 +01:00
6e60247026 apply CSS magic to DMR, too 2021-01-19 20:54:35 +01:00
6e416d0839 set prefixes using CSS 2021-01-19 00:36:55 +01:00
23bf1df72a update list of features with recent development 2021-01-17 19:51:04 +01:00
413c02f272 add discord to readme 2021-01-17 19:45:39 +01:00
502d324cd4 fix dmr mute overlay 2021-01-17 19:41:17 +01:00
3246e5ab3a move ysf metadata parsing to server; improve map pin behavior 2021-01-17 19:21:13 +01:00
c59c5b76d8 fix callsign highlight on map for mobile / portable calls 2021-01-17 18:50:55 +01:00
e917b920c8 remove failing stop() implementation on destructor 2021-01-17 18:11:10 +01:00
a0eeea8fe3 improve queue shutdown to avoid stale files 2021-01-17 17:49:03 +01:00
0f81964598 reserve one line of space to stop the icons from jumping 2021-01-17 01:49:10 +01:00
9c52219ca3 use gap instead of margins 2021-01-16 22:32:48 +01:00
8a73f2c9df rewrite DMR panel, too 2021-01-16 22:07:55 +01:00
98da3a6d99 delegate, don't duplicate. better this way 2021-01-16 21:20:21 +01:00
667fe596dc ysf does not need autoclear any more 2021-01-16 21:19:00 +01:00
f3444a4edb setup autoclear 2021-01-16 21:17:12 +01:00
946866319c improve location handling & clearing 2021-01-16 21:16:49 +01:00
8be0092f61 rewrite ysf panel update to make it less jumpy 2021-01-16 21:07:58 +01:00
3f94832d00 use flex layout 2021-01-16 19:46:39 +01:00
41f9407024 re-package code for meta panels into classes 2021-01-16 19:40:22 +01:00
13215960c4 show header buttons conditionally 2021-01-16 18:06:37 +01:00
9f702f5d14 let's try to make the header somewhat responsive 2021-01-16 17:34:17 +01:00
992a5c33a2 check for keys' existence 2021-01-16 15:45:33 +01:00
ae217f9ded specify flex-direction explicitly 2021-01-15 19:55:37 +01:00
00631d7349 hide map overlay until map is loaded 2021-01-15 19:43:16 +01:00
163ebcd327 actually position text in the center 2021-01-15 19:33:55 +01:00
a31b246924 restructure header 2021-01-15 19:06:00 +01:00
a8ef3a0e6a get rid of the e() function 2021-01-15 18:09:18 +01:00
b9f0c91ced update changelog 2021-01-15 16:28:38 +01:00
966a404700 don't spot FST4W on pskreporter (same as WSPR?) 2021-01-15 16:27:15 +01:00
885e361bab implement reporting of FST4W spots (in theory) 2021-01-15 16:19:45 +01:00
a65f15869b add wsprnet metrics 2021-01-15 00:11:20 +01:00
1b36baad88 extend default WFM bandwidth to 150kHz, allowing up to 200kHz 2021-01-14 23:47:12 +01:00
3273716706 add some info to the config 2021-01-14 23:02:34 +01:00
2c3586a92a add changelog 2021-01-14 22:58:40 +01:00
74a4f5b272 add wsprnet config variables 2021-01-14 22:56:52 +01:00
747a5ce7ef fix reporting system shutdown 2021-01-14 22:55:35 +01:00
e3aa3fa4c6 implement wsprnet reporting, refs #62 2021-01-14 22:54:59 +01:00
132bd2b445 create reporting engine to distribute spots 2021-01-14 20:52:56 +01:00
2334ad1d5b try a list of sample rates; prefer 48kHz 2021-01-14 17:07:43 +01:00
57efdff43e try enforcing 44100 samples/s for audio to avoid problems with odd defautl sampling rates 2021-01-14 16:51:00 +01:00
c5323f8d54 validate start_freq, use center_freq if invalid 2021-01-14 00:12:53 +01:00
7f3071336b check if new value is undefined 2021-01-13 23:50:36 +01:00
db98590985 implement profile validation 2021-01-13 23:44:00 +01:00
a90ef4efec add m17-demod as recommended package 2021-01-10 02:15:23 +01:00
b27c03c1c4 restore autostart to avoid unused thread 2021-01-09 20:08:40 +01:00
502546f9d3 improve cpu usage thread instance protection 2021-01-09 20:01:39 +01:00
113c06fae4 introduce separate wsjt-x version check based on wsjtx_app_version 2021-01-09 19:19:53 +01:00
73b75edc14 remove duplicate import 2021-01-09 19:10:08 +01:00
5337c20744 remove duplicate 2021-01-09 19:01:39 +01:00
57e5923a4d apply performance optimizations to s-meter, too 2021-01-02 18:16:25 +01:00
9d89cbceed use transform for better performance 2021-01-02 17:53:54 +01:00
44f4532452 add debug logging 2021-01-02 02:25:07 +01:00
c1245308bd make this more robust 2021-01-01 23:37:10 +01:00
a1cbc45b88 prevent multiple creation of cpu usage thread 2020-12-31 23:18:01 +01:00
90f319ebda split config into global and device config
* less config properties sent to the client
2020-12-31 23:03:36 +01:00
9674af10ce Merge pull request #202 from ewsandor/develop
User Customization of APRS IGate Beacon Details
2020-12-30 21:44:40 +01:00
5a77b6a8e5 show bandplan bookmarks only when mode is available 2020-12-30 21:37:25 +01:00
53553fcce2 fix subscription handling 2020-12-30 21:33:02 +01:00
1730ef27da Remove POWER from pbeacon string 2020-12-30 12:21:07 -07:00
57a6db5df2 Removing inapplicable fields 2020-12-30 12:16:12 -07:00
32fe01f128 Round instead of floor height conversion 2020-12-30 11:03:59 -07:00
b85d801121 create separate subscription for bookmarks 2020-12-30 18:45:13 +01:00
daa499ab93 PR comments edits 2020-12-30 10:33:21 -07:00
68fcb8522e fix typo 2020-12-30 18:05:10 +01:00
341b94b9ff prevent KeyError by checking for key existence 2020-12-30 17:46:13 +01:00
f4b9decd23 more animation performance optimizations 2020-12-30 17:45:32 +01:00
cf0c6e7f9d adapt to config event api changes 2020-12-30 17:18:46 +01:00
29703d10b2 server side: send only changed config keys 2020-12-30 17:17:14 +01:00
abb0813948 send only necessary config changes 2020-12-30 17:15:48 +01:00
2c3146314b send property changes in bulk to global subscribers 2020-12-30 17:14:06 +01:00
eb34c45145 apply transform trick to get GPU optimized animations 2020-12-28 21:16:54 +01:00
993aa87776 use css animations for the progressbar (better performance?) 2020-12-28 20:55:02 +01:00
71043d4305 update m17-cxx-demod in docker 2020-12-27 20:10:41 +01:00
eb981c04e9 Merge branch 'm17' into develop 2020-12-27 19:51:32 +01:00
ecf934864a move dc_block to individual chains since it interferes with m17 demod 2020-12-27 19:49:59 +01:00
686eeb706b add external reference control 2020-12-27 13:52:49 +01:00
94575d2212 update m17-cxx-demod 2020-12-22 12:51:36 +01:00
ca9e9601ab update m17-cxx-demod and dependencies 2020-12-21 19:42:15 +01:00
06f3499b6d Merge branch 'develop' into m17 2020-12-21 17:04:09 +01:00
db3d662dae fix typo 2020-12-13 16:31:19 +01:00
dee050f338 Fix comment 2020-12-12 11:38:50 -07:00
ae00a14a35 Fix comment formatting 2020-12-12 11:38:15 -07:00
86fdbe45e9 Add examples and comments to default config 2020-12-12 11:23:35 -07:00
b04dcc18d0 This is Python not C 2020-12-12 11:10:15 -07:00
1cc88ff362 if check fix 2020-12-12 11:09:12 -07:00
3435052e27 sanitize empty comment 2020-12-12 11:08:47 -07:00
4c3d037e58 Cleanup debug logging 2020-12-12 11:07:50 -07:00
f83790a5be debug comment length 2020-12-12 10:15:26 -07:00
11bb04419b fix parenthesis 2020-12-12 10:13:46 -07:00
519b02da79 improve quotes check 2020-12-12 10:12:43 -07:00
fdbbbcb64c Sanitize comment closing quote 2020-12-12 10:04:42 -07:00
0fb4ae4fc0 sanitize comment for opening quote 2020-12-12 09:59:34 -07:00
181511bc8e remove maps link from header 2020-12-11 17:53:31 +01:00
e062412e60 show receiver location pin on the map 2020-12-11 17:47:17 +01:00
bdb6d75f83 better sorting for the legend 2020-12-10 22:22:08 +01:00
433111124f reset default to 4 2020-12-10 20:59:40 +01:00
23080dbe22 allow frequency display precision to be set via configuration 2020-12-10 20:58:07 +01:00
05096c2a16 fully initialize sdr devices
* makes always-on work as expected
* prevents race conditions when multiple clients connect at the same
  time
2020-12-10 18:28:10 +01:00
5559cded85 Add quotes around default pbeacon comment 2020-12-09 23:17:42 -07:00
9f45e8880a formating pbeacon string 2020-12-09 23:09:37 -07:00
dc128662da log pbeacon string 2020-12-09 23:05:04 -07:00
dc3fd24903 Correcting key check 2020-12-09 22:59:16 -07:00
b2efa81b0d Formatting additional PBEACON details 2020-12-09 22:54:06 -07:00
2c04d40c53 allow antenna information to be sent to pskreporter 2020-12-10 01:36:09 +01:00
fcff9d16ff filter out problematic spots instead of breaking completely 2020-12-09 23:38:27 +01:00
eef61f9d1e break the pskreporter loop if there's nothing to upload 2020-12-09 22:59:45 +01:00
8f9f9e8397 Merge pull request #197 from acfnews/develop
correct typo, and prevent warning during postinstall
2020-12-09 21:53:13 +01:00
d0e7747c7f correct typo, and prevent warning during postinstall 2020-12-09 21:38:59 +01:00
9e45cfd02a implement clear function for js8, too - refs #187 2020-12-09 21:19:22 +01:00
aa66e69c15 implement clear button for packet, pocsag, wsjt-x, refs #187 2020-12-09 20:08:50 +01:00
9bf4b149aa move pocsag message panel 2020-12-09 19:53:37 +01:00
5474973752 move aprs message panel 2020-12-09 19:42:46 +01:00
3e30ab57a6 move wsjt message panel logic to own class 2020-12-09 19:26:34 +01:00
9d6099b6d8 FST4[W] frontend work 2020-12-09 17:38:37 +01:00
a7f667779a allow roaming / portable / mobile calls in wsjt-x message 2020-12-09 12:47:08 +01:00
f71240c9a6 handle exception when output is missing 2020-12-09 12:31:01 +01:00
f8fc61e9bd streamline datetime parsing 2020-12-09 12:02:40 +01:00
a8011e3a1a use profiles instead of parsing to detect mode 2020-12-09 11:38:46 +01:00
e8fcf05775 add m17-demod to docker builds (preliminary) 2020-12-08 19:43:50 +01:00
cfb6fb5b30 add changelog message 2020-12-08 17:01:04 +01:00
fb68ca3c66 add documentation 2020-12-08 16:59:49 +01:00
6af19f44e8 Merge branch 'develop' into m17 2020-12-08 16:57:00 +01:00
3291dbe8d2 fix fst4w profile 2020-12-08 01:06:21 +01:00
efac5b0449 change job decoder error handling so errors reflect in metrics 2020-12-08 00:28:34 +01:00
519155a12f fix "R" callsign problem 2020-12-08 00:01:00 +01:00
603c3df1b6 fix fst4(w) filenames 2020-12-08 00:00:21 +01:00
05ca541a8e fix jt9 parameters 2020-12-07 20:29:22 +01:00
917884b5f5 add changelog message 2020-12-07 11:59:43 +01:00
22a2bd1de1 default config for FST4/FST4W intervals 2020-12-07 11:57:34 +01:00
af4923c741 enable reporting of FST4/FST4W to pskreporter 2020-12-07 11:56:21 +01:00
ac4401175f add FST4 and FST4W modes 2020-12-07 11:56:01 +01:00
71c649b016 add and definitions for VLF bands including frequencies for FST4/FST4W 2020-12-07 11:52:46 +01:00
cbdb143966 allow 25kHz packet when manually adjusting 2020-12-06 23:24:57 +01:00
8c105b0c40 fix direwolf build (uses cmake now) 2020-12-06 01:44:14 +01:00
8e760a0fcc use new direwolf 1.6 2020-12-06 00:43:45 +01:00
6f46e4d376 remove debugging 2020-12-06 00:42:48 +01:00
bee6ddc843 use information from the mode registry to set up services 2020-12-06 00:36:20 +01:00
a3fd931931 demodulate digimodes starting at 0 Hz 2020-12-06 00:21:46 +01:00
e2fa293c74 fix paths 2020-12-04 00:39:05 +01:00
c4ed481ce2 update aprs-symbols location for docker 2020-12-04 00:12:51 +01:00
e6ea3832fc add aprs-symbols debian package 2020-12-04 00:11:47 +01:00
9a8c0ce442 update list of device types in config 2020-12-02 23:36:30 +01:00
49ec66e27c add hpsdr change log notice 2020-12-02 23:35:37 +01:00
2b6456168e add libc6-dev for header files 2020-12-02 17:59:24 +01:00
e6cbe6ffc8 add hpsdr build 2020-12-02 16:51:52 +01:00
00d496086e add gcc as it seems to be required on arm (?) 2020-12-02 16:50:14 +01:00
1894ed50d1 add hpsdrconnector docker build 2020-12-01 23:33:05 +01:00
7ad5ca03b0 add eb200 to full build 2020-12-01 21:42:20 +01:00
b380187453 add docker build for eb200 devices 2020-12-01 21:39:22 +01:00
2022c53fad make soapysdr-tools a hard dependency; recommend eb200_connector 2020-12-01 17:41:17 +01:00
46b7660e2d add long flag mapping for eb200 2020-12-01 14:14:52 +01:00
e90b10abfd Merge branch 'master' into develop 2020-11-30 20:30:30 +01:00
a8bd13f7e6 increase bandwidth for packet to 12.5kHz 2020-11-30 17:13:03 +01:00
daf2848c4d increase package dependency version 2020-11-30 13:24:25 +01:00
0614637342 add eb200 support 2020-11-30 00:34:44 +01:00
865ffb28af Merge branch 'rf103' into develop 2020-11-29 16:01:56 +01:00
8b89d1e062 update changelog 2020-11-29 16:01:35 +01:00
e4cf95856e update connectors 2020-11-28 22:11:12 +01:00
74be25f656 rename all occurences to SDDC 2020-11-27 18:49:33 +01:00
b5d56eaec2 update to use new sddc_connector 2020-11-27 18:39:10 +01:00
8bb6e91597 Merge branch 'develop' into rf103 2020-11-23 18:42:17 +01:00
d72f2d9e5c update changelog 2020-11-23 15:34:44 +01:00
781b4383d6 remove port flag and turn rtltcp_compat into an option 2020-11-23 15:26:01 +01:00
017bbc3748 attempt to include m17-demod (untested due to lack of signals) 2020-11-23 01:00:25 +01:00
69a5e0bc5d Merge branch 'develop' into rf103 2020-11-12 23:45:39 +01:00
2579b9be26 remove specific instructions, complete list 2020-11-12 23:44:02 +01:00
9bfef01438 Merge pull request #188 from jancona/hpsdr_connector
Support for HPSDR radios (specifically, the Hermes-Lite 2)
2020-11-12 23:39:28 +01:00
c0d4b2f6a5 Remove debug option, mention in config_webrx.py 2020-11-12 17:36:36 -05:00
529e9c3c60 Merge branch 'develop' into rf103 2020-11-12 18:01:59 +01:00
504c256b3e make auto squelch level margin configurable 2020-11-12 18:00:24 +01:00
91572c56e2 Make hpsdrconnector into a true connector. 2020-11-09 19:24:13 -05:00
3b229b95b6 Merge branch 'develop' into rf103 2020-11-04 22:45:46 +01:00
0f4b8dc794 fill-in undeclared variable, refs #192 2020-11-04 22:38:24 +01:00
e700f0a9e4 replace nanoscroller with compressed version, drop map reference.
closes #191
2020-11-04 22:32:13 +01:00
c85400063c Initial HPSDR radio support 2020-11-02 07:11:54 -05:00
dc03639cad add source for rf103 (experimental) 2020-10-25 16:53:18 +01:00
e6a04aa5e9 use agc on wsjtx/js8 since the levels are too low on some sdrs 2020-10-25 14:41:53 +01:00
1bc3830e5e guard against the case where receiver_keys are missing in the
configuration
2020-10-16 22:53:57 +02:00
93f7195429 Merge pull request #173 from jwt27/jwt27/highlight-freq-digits
Highlight frequency digits on mouse hover
2020-10-16 20:29:52 +02:00
d04e0d2a2a link feature report to the wiki for DRM and FreeDV 2020-10-16 19:52:51 +02:00
259eef2e68 move develop to the next version 0.21 2020-10-11 23:16:59 +02:00
325eab35a9 highlight frequency digits on mouse hover 2020-08-30 23:13:26 +02:00
209 changed files with 11094 additions and 4882 deletions

View File

@ -1,3 +1,30 @@
**1.0.0**
- Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level
- Removed `port` configuration option; `rtltcp_compat` takes the port number with the new connectors
- Added support for new WSJT-X modes FST4, FST4W (only available with WSJT-X 2.3) and Q65 (only avilable with
WSJT-X 2.4)
- Added support for demodulating M17 digital voice signals using m17-cxx-demod
- New reporting infrastructure, allowing WSPR and FST4W spots to be sent to wsprnet.org
- Add some basic filtering capabilities to the map
- New arguments to the `openwebrx` command-line to facilitate the administration of users (try `openwebrx admin`)
- Default bandwidth changes:
- "WFM" changed to 150kHz
- "Packet" (APRS) changed to 12.5kHz
- Configuration rework:
- New: fully web-based configuration interface
- System configuration parameters have been moved to a new, separate `openwebrx.conf` file
- Remaining parameters are now editable in the web configuration
- Existing `config_webrx.py` files will still be read, but changes made in the web configuration will be written to
a new storage system
- Added upload of avatar and panorama image via web configuration
- New devices supported:
- HPSDR devices (Hermes Lite 2) thanks to @jancona
- BBRF103 / RX666 / RX888 devices supported by libsddc
- R&S devices using the EB200 or Ammos protocols
**0.20.3**
- Fix a compatibility issue with python versions <= 3.6
**0.20.2**
- Fix a security problem that allowed arbitrary commands to be executed on the receiver
([See github issue #215](https://github.com/jketterl/openwebrx/issues/215))

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ config_webrx: configuration options for OpenWebRX
This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@ -32,32 +32,45 @@ config_webrx: configuration options for OpenWebRX
and use them for running your web service with OpenWebRX.)
"""
"""
DEPRECATION notice
As of OpenWebRX 0.21, the configuration system has been completely overhauled.
The configuration of OpenWebRX should now be done in the new web-based
configuration interface exclusively.
Existing configurations can still be used, but their values will be migrated
to the new storage infrastructure as soon as the web configuration is used to
edit them.
The new configuration storage is not intended to be edited manually.
"""
# configuration version. please only modify if you're able to perform the associated migration steps.
version = 3
version = 7
# NOTE: you can find additional information about configuring OpenWebRX in the Wiki:
# https://github.com/jketterl/openwebrx/wiki/Configuration-guide
# ==== Server settings ====
web_port = 8073
max_clients = 20
#max_clients = 20
# ==== Web GUI configuration ====
receiver_name = "[Callsign]"
receiver_location = "Budapest, Hungary"
receiver_asl = 200
receiver_admin = "example@example.com"
receiver_gps = {"lat": 47.000000, "lon": 19.000000}
photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
#receiver_name = "[Callsign]"
#receiver_location = "Budapest, Hungary"
#receiver_asl = 200
#receiver_admin = "example@example.com"
#receiver_gps = {"lat": 47.000000, "lon": 19.000000}
#photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
# photo_desc allows you to put pretty much any HTML you like into the receiver description.
# The lines below should give you some examples of what's possible.
photo_desc = """
You can add your own background photo and receiver information.<br />
Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
Device: Receiver Device<br />
Antenna: Receiver Antenna<br />
Website: <a href="http://localhost" target="_blank">http://localhost</a>
"""
#photo_desc = """
#You can add your own background photo and receiver information.<br />
#Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
#Device: Receiver Device<br />
#Antenna: Receiver Antenna<br />
#Website: <a href="http://localhost" target="_blank">http://localhost</a>
#"""
# ==== Public receiver listings ====
# You can publish your receiver on online receiver directories, like https://www.receiverbook.de
@ -65,7 +78,7 @@ Website: <a href="http://localhost" target="_blank">http://localhost</a>
# Please note that you not share your receiver keys publicly since anyone that obtains your receiver key can take over
# your public listing.
# Your receiver keys should be placed into this array:
receiver_keys = []
#receiver_keys = []
# If you list your receiver on multiple sites, you can place all your keys into the array above, or you can append
# keys to the arraylike this:
# receiver_keys += ["my-receiver-key"]
@ -73,33 +86,32 @@ receiver_keys = []
# If you're not sure, simply copy & paste the code you received from your listing site below this line:
# ==== DSP/RX settings ====
fft_fps = 9
fft_size = 4096 # Should be power of 2
fft_voverlap_factor = (
0.3 # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
)
#fft_fps = 9
#fft_size = 4096 # Should be power of 2
#fft_voverlap_factor = (
# 0.3 # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
#)
audio_compression = "adpcm" # valid values: "adpcm", "none"
fft_compression = "adpcm" # valid values: "adpcm", "none"
#audio_compression = "adpcm" # valid values: "adpcm", "none"
#fft_compression = "adpcm" # valid values: "adpcm", "none"
# Tau setting for WFM (broadcast FM) deemphasis\
# Quote from wikipedia https://en.wikipedia.org/wiki/FM_broadcasting#Pre-emphasis_and_de-emphasis
# "In most of the world a 50 µs time constant is used. In the Americas and South Korea, 75 µs is used"
# Enable one of the following lines, depending on your location:
# wfm_deemphasis_tau = 75e-6 # for US and South Korea
wfm_deemphasis_tau = 50e-6 # for the rest of the world
#wfm_deemphasis_tau = 50e-6 # for the rest of the world
digimodes_enable = True # Decoding digimodes come with higher CPU usage.
digimodes_fft_size = 2048
#digimodes_fft_size = 2048
# determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes
# if you're running on a Raspi (up to 3B+) you'll want to leave this on 1
digital_voice_unvoiced_quality = 1
#digital_voice_unvoiced_quality = 1
# enables lookup of DMR ids using the radioid api
digital_voice_dmr_id_lookup = True
#digital_voice_dmr_id_lookup = True
"""
Note: if you experience audio underruns while CPU usage is 100%, you can:
Note: if you experience audio underruns while CPU usage is 100%, you can:
- decrease `samp_rate`,
- set `fft_voverlap_factor` to 0,
- decrease `fft_fps` and `fft_size`,
@ -116,230 +128,262 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
# Currently supported types of sdr receivers:
# "rtl_sdr", "rtl_sdr_soapy", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr",
# "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote"
#
# In order to use rtl_sdr, you will need to install librtlsdr-dev and the connector.
# In order to use sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the
# connector.
#
# https://github.com/jketterl/owrx_connector
#
# In order to use Perseus HF you need to install the libperseus-sdr
#
# https://github.com/Microtelecom/libperseus-sdr
#
# and do the proper changes to the sdrs object below
# (see also Wiki in https://github.com/jketterl/openwebrx/wiki/Sample-configuration-for-Perseus-HF-receiver).
#
# "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote", "hpsdr", "uhd",
# "radioberry", "fcdpp", "rtl_tcp", "sddc", "runds"
sdrs = {
"rtlsdr": {
"name": "RTL-SDR USB Stick",
"type": "rtl_sdr",
"ppm": 0,
# you can change this if you use an upconverter. formula is:
# center_freq + lfo_offset = actual frequency on the sdr
# "lfo_offset": 0,
"profiles": {
"70cm": {
"name": "70cm Relais",
"center_freq": 438800000,
"rf_gain": 29,
"samp_rate": 2400000,
"start_freq": 439275000,
"start_mod": "nfm",
},
"2m": {
"name": "2m komplett",
"center_freq": 145000000,
"rf_gain": 29,
"samp_rate": 2048000,
"start_freq": 145725000,
"start_mod": "nfm",
},
},
},
"airspy": {
"name": "Airspy HF+",
"type": "airspyhf",
"ppm": 0,
"rf_gain": "auto",
"profiles": {
"20m": {
"name": "20m",
"center_freq": 14150000,
"samp_rate": 384000,
"start_freq": 14070000,
"start_mod": "usb",
},
"30m": {
"name": "30m",
"center_freq": 10125000,
"samp_rate": 192000,
"start_freq": 10142000,
"start_mod": "usb",
},
"40m": {
"name": "40m",
"center_freq": 7100000,
"samp_rate": 256000,
"start_freq": 7070000,
"start_mod": "lsb",
},
"80m": {
"name": "80m",
"center_freq": 3650000,
"samp_rate": 384000,
"start_freq": 3570000,
"start_mod": "lsb",
},
"49m": {
"name": "49m Broadcast",
"center_freq": 6050000,
"samp_rate": 384000,
"start_freq": 6070000,
"start_mod": "am",
},
},
},
"sdrplay": {
"name": "SDRPlay RSP2",
"type": "sdrplay",
"ppm": 0,
"antenna": "Antenna A",
"profiles": {
"20m": {
"name": "20m",
"center_freq": 14150000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 14070000,
"start_mod": "usb",
},
"30m": {
"name": "30m",
"center_freq": 10125000,
"rf_gain": 0,
"samp_rate": 250000,
"start_freq": 10142000,
"start_mod": "usb",
},
"40m": {
"name": "40m",
"center_freq": 7100000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 7070000,
"start_mod": "lsb",
},
"80m": {
"name": "80m",
"center_freq": 3650000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 3570000,
"start_mod": "lsb",
},
"49m": {
"name": "49m Broadcast",
"center_freq": 6000000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 6070000,
"start_mod": "am",
},
},
},
}
# For more details on specific types, please checkout the wiki:
# https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices
#sdrs = {
# "rtlsdr": {
# "name": "RTL-SDR USB Stick",
# "type": "rtl_sdr",
# "ppm": 0,
# # you can change this if you use an upconverter. formula is:
# # center_freq + lfo_offset = actual frequency on the sdr
# # "lfo_offset": 0,
# "profiles": {
# "70cm": {
# "name": "70cm Relais",
# "center_freq": 438800000,
# "rf_gain": 29,
# "samp_rate": 2400000,
# "start_freq": 439275000,
# "start_mod": "nfm",
# },
# "2m": {
# "name": "2m komplett",
# "center_freq": 145000000,
# "rf_gain": 29,
# "samp_rate": 2048000,
# "start_freq": 145725000,
# "start_mod": "nfm",
# },
# },
# },
# "airspy": {
# "name": "Airspy HF+",
# "type": "airspyhf",
# "ppm": 0,
# "rf_gain": "auto",
# "profiles": {
# "20m": {
# "name": "20m",
# "center_freq": 14150000,
# "samp_rate": 384000,
# "start_freq": 14070000,
# "start_mod": "usb",
# },
# "30m": {
# "name": "30m",
# "center_freq": 10125000,
# "samp_rate": 192000,
# "start_freq": 10142000,
# "start_mod": "usb",
# },
# "40m": {
# "name": "40m",
# "center_freq": 7100000,
# "samp_rate": 256000,
# "start_freq": 7070000,
# "start_mod": "lsb",
# },
# "80m": {
# "name": "80m",
# "center_freq": 3650000,
# "samp_rate": 384000,
# "start_freq": 3570000,
# "start_mod": "lsb",
# },
# "49m": {
# "name": "49m Broadcast",
# "center_freq": 6050000,
# "samp_rate": 384000,
# "start_freq": 6070000,
# "start_mod": "am",
# },
# },
# },
# "sdrplay": {
# "name": "SDRPlay RSP2",
# "type": "sdrplay",
# "ppm": 0,
# "antenna": "Antenna A",
# "profiles": {
# "20m": {
# "name": "20m",
# "center_freq": 14150000,
# "rf_gain": 0,
# "samp_rate": 500000,
# "start_freq": 14070000,
# "start_mod": "usb",
# },
# "30m": {
# "name": "30m",
# "center_freq": 10125000,
# "rf_gain": 0,
# "samp_rate": 250000,
# "start_freq": 10142000,
# "start_mod": "usb",
# },
# "40m": {
# "name": "40m",
# "center_freq": 7100000,
# "rf_gain": 0,
# "samp_rate": 500000,
# "start_freq": 7070000,
# "start_mod": "lsb",
# },
# "80m": {
# "name": "80m",
# "center_freq": 3650000,
# "rf_gain": 0,
# "samp_rate": 500000,
# "start_freq": 3570000,
# "start_mod": "lsb",
# },
# "49m": {
# "name": "49m Broadcast",
# "center_freq": 6000000,
# "rf_gain": 0,
# "samp_rate": 500000,
# "start_freq": 6070000,
# "start_mod": "am",
# },
# },
# },
#}
# ==== Color themes ====
### google turbo colormap (see: https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html)
waterfall_colors = [0x30123b, 0x311542, 0x33184a, 0x341b51, 0x351e58, 0x36215f, 0x372466, 0x38266c, 0x392973, 0x3a2c79, 0x3b2f80, 0x3c3286, 0x3d358b, 0x3e3891, 0x3e3a97, 0x3f3d9c, 0x4040a2, 0x4043a7, 0x4146ac, 0x4248b1, 0x424bb6, 0x434eba, 0x4351bf, 0x4453c3, 0x4456c7, 0x4559cb, 0x455bcf, 0x455ed3, 0x4561d7, 0x4663da, 0x4666dd, 0x4669e1, 0x466be4, 0x466ee7, 0x4671e9, 0x4673ec, 0x4676ee, 0x4678f1, 0x467bf3, 0x467df5, 0x4680f7, 0x4682f9, 0x4685fa, 0x4587fc, 0x458afd, 0x448cfe, 0x448ffe, 0x4391ff, 0x4294ff, 0x4196ff, 0x3f99ff, 0x3e9bff, 0x3d9efe, 0x3ba1fd, 0x3aa3fd, 0x38a6fb, 0x36a8fa, 0x35abf9, 0x33adf7, 0x31b0f6, 0x2fb2f4, 0x2db5f2, 0x2cb7f0, 0x2ab9ee, 0x28bcec, 0x26beea, 0x25c0e7, 0x23c3e5, 0x21c5e2, 0x20c7e0, 0x1fc9dd, 0x1dccdb, 0x1cced8, 0x1bd0d5, 0x1ad2d3, 0x19d4d0, 0x18d6cd, 0x18d8cb, 0x18dac8, 0x17dbc5, 0x17ddc3, 0x17dfc0, 0x18e0be, 0x18e2bb, 0x19e3b9, 0x1ae5b7, 0x1be6b4, 0x1de8b2, 0x1ee9af, 0x20eaad, 0x22ecaa, 0x24eda7, 0x27eea4, 0x29efa1, 0x2cf09e, 0x2ff19b, 0x32f298, 0x35f394, 0x38f491, 0x3cf58e, 0x3ff68b, 0x43f787, 0x46f884, 0x4af980, 0x4efa7d, 0x51fa79, 0x55fb76, 0x59fc73, 0x5dfc6f, 0x61fd6c, 0x65fd69, 0x69fe65, 0x6dfe62, 0x71fe5f, 0x75ff5c, 0x79ff59, 0x7dff56, 0x80ff53, 0x84ff50, 0x88ff4e, 0x8bff4b, 0x8fff49, 0x92ff46, 0x96ff44, 0x99ff42, 0x9cfe40, 0x9ffe3e, 0xa2fd3d, 0xa4fd3b, 0xa7fc3a, 0xaafc39, 0xacfb38, 0xaffa37, 0xb1f936, 0xb4f835, 0xb7f835, 0xb9f634, 0xbcf534, 0xbff434, 0xc1f334, 0xc4f233, 0xc6f033, 0xc9ef34, 0xcbee34, 0xceec34, 0xd0eb34, 0xd2e934, 0xd5e835, 0xd7e635, 0xd9e435, 0xdbe236, 0xdde136, 0xe0df37, 0xe2dd37, 0xe4db38, 0xe6d938, 0xe7d738, 0xe9d539, 0xebd339, 0xedd139, 0xeecf3a, 0xf0cd3a, 0xf1cb3a, 0xf3c93a, 0xf4c73a, 0xf5c53a, 0xf7c33a, 0xf8c13a, 0xf9bf39, 0xfabd39, 0xfaba38, 0xfbb838, 0xfcb637, 0xfcb436, 0xfdb135, 0xfdaf35, 0xfeac34, 0xfea933, 0xfea732, 0xfea431, 0xffa12f, 0xff9e2e, 0xff9c2d, 0xff992c, 0xfe962b, 0xfe932a, 0xfe9028, 0xfe8d27, 0xfd8a26, 0xfd8724, 0xfc8423, 0xfc8122, 0xfb7e20, 0xfb7b1f, 0xfa781e, 0xf9751c, 0xf8721b, 0xf86f1a, 0xf76c19, 0xf66917, 0xf56616, 0xf46315, 0xf36014, 0xf25d13, 0xf05b11, 0xef5810, 0xee550f, 0xed530e, 0xeb500e, 0xea4e0d, 0xe94b0c, 0xe7490b, 0xe6470a, 0xe4450a, 0xe34209, 0xe14009, 0xdf3e08, 0xde3c07, 0xdc3a07, 0xda3806, 0xd83606, 0xd63405, 0xd43205, 0xd23105, 0xd02f04, 0xce2d04, 0xcc2b03, 0xca2903, 0xc82803, 0xc62602, 0xc32402, 0xc12302, 0xbf2102, 0xbc1f01, 0xba1e01, 0xb71c01, 0xb41b01, 0xb21901, 0xaf1801, 0xac1601, 0xaa1501, 0xa71401, 0xa41201, 0xa11101, 0x9e1001, 0x9b0f01, 0x980d01, 0x950c01, 0x920b01, 0x8e0a01, 0x8b0901, 0x880801, 0x850701, 0x810602, 0x7e0502, 0x7a0402]
#waterfall_scheme = "GoogleTurboWaterfall"
### original theme by teejez:
#waterfall_colors = [0x000000, 0x0000FF, 0x00FFFF, 0x00FF00, 0xFFFF00, 0xFF0000, 0xFF00FF, 0xFFFFFF]
#waterfall_scheme = "TeejeezWaterfall"
### old theme by HA7ILM:
#waterfall_colors = [0x000000, 0x2e6893, 0x69a5d0, 0x214b69, 0x9dc4e0, 0xfff775, 0xff8a8a, 0xb20000]
# waterfall_min_level = -115 #in dB
# waterfall_max_level = 0
# waterfall_auto_level_margin = {"min": 20, "max": 30}
#waterfall_scheme = "Ha7ilmWaterfall"
##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
waterfall_min_level = -88 # in dB
waterfall_max_level = -20
waterfall_auto_level_margin = {"min": 3, "max": 10, "min_range": 50}
### custom waterfall schemes can be configured like this:
#waterfall_scheme = "CustomWaterfall"
#waterfall_colors = [0x0000FF, 0x00FF00, 0xFF0000]
### Waterfall calibration
#waterfall_levels = {"min": -88, "max": -20} # in dB
#waterfall_auto_levels = {"min": 3, "max": 10}
#waterfall_auto_min_range = 50
# Note: When the auto waterfall level button is clicked, the following happens:
# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin["min"]]
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin["max"]]
# [waterfall_levels.min] = [current_min_power_level] - [waterfall_auto_levels["min"]]
# [waterfall_levels.max] = [current_max_power_level] + [waterfall_auto_levels["max"]]
#
# ___|________________________________________|____________________________________|________________________________________|___> signal power
# \_waterfall_auto_level_margin["min"]_/ |__ current_min_power_level | \_waterfall_auto_level_margin["max"]_/
# current_max_power_level __|
# ___|__________________________________|____________________________________|__________________________________|___> signal power
# \_waterfall_auto_levels["min"]_/ |__ current_min_power_level | \_waterfall_auto_levels["max"]_/
# current_max_power_level __|
# === Experimental settings ===
# Warning! The settings below are very experimental.
csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr.
csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes.
csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
# This setting allows you to modify the precision of the frequency displays in OpenWebRX.
# Set this to exponent of 10 to select the most precise digit in Hz you'd like to see
# examples:
# a value of 2 selects 10^2 = 100Hz tuning precision (default):
#tuning_precision = 2
# a value of 1 selects 10^1 = 10Hz tuning precision:
#tuning_precision = 1
nmux_memory = 50 # in megabytes. This sets the approximate size of the circular buffer used by nmux.
# This setting tells the auto-squelch the offset to add to the current signal level to use as the new squelch level.
# Lowering this setting will give you a more sensitive squelch, but it may also cause unwanted squelch openings when
# using the auto squelch.
#squelch_auto_margin = 10 # in dB
google_maps_api_key = ""
#google_maps_api_key = ""
# how long should positions be visible on the map?
# they will start fading out after half of that
# in seconds; default: 2 hours
map_position_retention_time = 2 * 60 * 60
#map_position_retention_time = 2 * 60 * 60
# decoder queue configuration
# due to the nature of some operating modes (ft8, ft8, jt9, jt65, wspr and js8), the data is recorded for a given amount
# of time (6 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
# to mitigate this, the recordings will be queued and processed in sequence.
# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
decoding_queue_workers = 2
#decoding_queue_workers = 2
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up
# if you are running background services, make sure this number is high enough to accept the task influx during peaks
# i.e. this should be higher than the number of decoding services running at the same time
decoding_queue_length = 10
#decoding_queue_length = 10
# 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
# 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}
# FST4 can be transmitted in different intervals. This setting determines which intervals will be decoded.
# available values (in seconds): 15, 30, 60, 120, 300, 900, 1800
#fst4_enabled_intervals = [15, 30]
# FST4W can be transmitted in different intervals. This setting determines which intervals will be decoded.
# available values (in seconds): 120, 300, 900, 1800
#fst4w_enabled_intervals = [120, 300]
# Q65 allows many combinations of intervals and submodes. This setting determines which combinations will be decoded.
# Please use the mode letter followed by the decode interval in seconds to specify the combinations. For example:
#q65_enabled_combinations = ["A30", "E120", "C60"]
# JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled.
js8_enabled_profiles = ["normal", "slow"]
#js8_enabled_profiles = ["normal", "slow"]
# JS8 decoding depth; higher value will get more results, but will also consume more cpu
js8_decoding_depth = 3
#js8_decoding_depth = 3
temporary_directory = "/tmp"
services_enabled = False
services_decoders = ["ft8", "ft4", "wspr", "packet"]
# Enable background service for decoding digital data. You can find more information at:
# https://github.com/jketterl/openwebrx/wiki/Background-decoding
#services_enabled = False
#services_decoders = ["ft8", "ft4", "wspr", "packet"]
# === aprs igate settings ===
# if you want to share your APRS decodes with the aprs network, configure these settings accordingly
aprs_callsign = "N0CALL"
aprs_igate_enabled = False
aprs_igate_server = "euro.aprs2.net"
aprs_igate_password = ""
# If you want to share your APRS decodes with the aprs network, configure these settings accordingly.
# Make sure that you have set services_enabled to true and customize services_decoders to your needs.
#aprs_callsign = "N0CALL"
#aprs_igate_enabled = False
#aprs_igate_server = "euro.aprs2.net"
#aprs_igate_password = ""
# beacon uses the receiver_gps setting, so if you enable this, make sure the location is correct there
aprs_igate_beacon = False
#aprs_igate_beacon = False
# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols)
aprs_symbols_path = "/opt/aprs-symbols/png"
# Uncomment the following to customize gateway beacon details reported to the aprs network
# Plese see Dire Wolf's documentation on PBEACON configuration for complete details:
# https://github.com/wb2osz/direwolf/raw/master/doc/User-Guide.pdf
# === PSK Reporter setting ===
# Symbol in its two-character form as specified by the APRS spec at http://www.aprs.org/symbols/symbols-new.txt
# Default: Receive only IGate (do not send msgs back to RF)
# aprs_igate_symbol = "R&"
# Custom comment about igate
# Default: OpenWebRX APRS gateway
# aprs_igate_comment = "OpenWebRX APRS gateway"
# Antenna Height and Gain details
# Unspecified by default
# Antenna height above average terrain (HAAT) in meters
# aprs_igate_height = "5"
# Antenna gain in dBi
# aprs_igate_gain = "0"
# Antenna direction (N, NE, E, SE, S, SW, W, NW). Omnidirectional by default
# aprs_igate_dir = "NE"
# === PSK Reporter settings ===
# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator
pskreporter_enabled = False
pskreporter_callsign = "N0CALL"
#pskreporter_enabled = False
#pskreporter_callsign = "N0CALL"
# optional antenna information, uncomment to enable
#pskreporter_antenna_information = "Dipole"
# === 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
# === WSPRNet reporting settings
# enable this if you want to upload WSPR spots to wsprnet.ort
# in addition to these settings also make sure that receiver_gps contains your correct location
#wsprnet_enabled = False
#wsprnet_callsign = "N0CALL"

View File

@ -4,7 +4,7 @@ OpenWebRX csdr plugin: do the signal processing with csdr
This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@ -28,10 +28,10 @@ import threading
import math
from functools import partial
from owrx.kiss import KissClient, DirewolfConfig
from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile
from owrx.js8 import Js8Profiles
from owrx.audio import AudioChopper
from csdr.output import Output
from owrx.kiss import KissClient, DirewolfConfig, DirewolfConfigSubscriber
from owrx.audio.chopper import AudioChopper
from csdr.pipe import Pipe
@ -40,40 +40,8 @@ import logging
logger = logging.getLogger(__name__)
class output(object):
def send_output(self, t, read_fn):
if not self.supports_type(t):
# TODO rewrite the output mechanism in a way that avoids producing unnecessary data
logger.warning("dumping output of type %s since it is not supported.", t)
threading.Thread(target=self.pump(read_fn, lambda x: None), name="csdr_pump_thread").start()
return
self.receive_output(t, read_fn)
def receive_output(self, t, read_fn):
pass
def pump(self, read, write):
def copy():
run = True
while run:
data = None
try:
data = read()
except ValueError:
pass
if data is None or (isinstance(data, bytes) and len(data) == 0):
run = False
else:
write(data)
return copy
def supports_type(self, t):
return True
class dsp(object):
def __init__(self, output):
class Dsp(DirewolfConfigSubscriber):
def __init__(self, output: Output):
self.samp_rate = 250000
self.output_rate = 11025
self.hd_output_rate = 44100
@ -95,9 +63,6 @@ class dsp(object):
self.decimation = None
self.last_decimation = None
self.nc_port = None
self.csdr_dynamic_bufsize = False
self.csdr_print_bufsizes = False
self.csdr_through = False
self.squelch_level = -150
self.fft_averages = 50
self.wfm_deemphasis_tau = 50e-6
@ -130,7 +95,7 @@ class dsp(object):
self.is_service = False
self.direwolf_config = None
self.direwolf_port = None
self.direwolf_config_path = None
self.process = None
def set_service(self, flag=True):
@ -142,10 +107,6 @@ class dsp(object):
def chain(self, which):
chain = ["nc -v 127.0.0.1 {nc_port}"]
if self.csdr_dynamic_bufsize:
chain += ["csdr setbuf {start_bufsize}"]
if self.csdr_through:
chain += ["csdr through"]
if which == "fft":
chain += [
"csdr fft_cc {fft_size} {fft_block_size}",
@ -198,16 +159,13 @@ class dsp(object):
"csdr limit_ff",
]
chain += last_decimation_block
chain += [
"csdr deemphasis_wfm_ff {audio_rate} {wfm_deemphasis_tau}",
"csdr convert_f_s16"
]
chain += ["csdr deemphasis_wfm_ff {audio_rate} {wfm_deemphasis_tau}", "csdr convert_f_s16"]
elif self.isDigitalVoice(which):
chain += ["csdr fmdemod_quadri_cf", "dc_block "]
chain += ["csdr fmdemod_quadri_cf"]
chain += last_decimation_block
# dsd modes
if which in ["dstar", "nxdn"]:
chain += ["csdr limit_ff", "csdr convert_f_s16"]
chain += ["dc_block", "csdr limit_ff", "csdr convert_f_s16"]
if which == "dstar":
chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "]
elif which == "nxdn":
@ -217,9 +175,19 @@ class dsp(object):
"CSDR_FIXED_BUFSIZE=32 csdr agc_s16 --max 30 --initial 3",
"sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
]
# m17
elif which == "m17":
chain += [
"dc_block",
"csdr limit_ff",
"csdr convert_f_s16",
"m17-demod",
"CSDR_FIXED_BUFSIZE=32 csdr agc_s16 --max 30 --initial 3",
"sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
]
# digiham modes
else:
chain += ["rrc_filter", "gfsk_demodulator"]
chain += ["dc_block", "rrc_filter", "gfsk_demodulator"]
if which == "dmr":
chain += [
"dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}",
@ -301,7 +269,7 @@ class dsp(object):
chain += ["csdr realpart_cf"]
if self.last_decimation != 1.0:
chain += ["csdr fractional_decimator_ff {last_decimation}"]
return chain + ["csdr limit_ff", "csdr convert_f_s16"]
return chain + ["csdr agc_ff", "csdr convert_f_s16"]
elif which == "packet":
chain += ["csdr fmdemod_quadri_cf"]
if self.last_decimation != 1.0:
@ -374,14 +342,10 @@ class dsp(object):
if_samp_rate=self.if_samp_rate(),
last_decimation=self.last_decimation,
audio_rate=self.get_audio_rate(),
direwolf_config=self.direwolf_config,
direwolf_config=self.direwolf_config_path,
)
logger.debug("secondary command (demod) = %s", secondary_command_demod)
my_env = os.environ.copy()
# if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
if self.csdr_print_bufsizes:
my_env["CSDR_PRINT_BUFSIZES"] = "1"
if self.output.supports_type("secondary_fft"):
secondary_command_fft = " | ".join(self.secondary_chain("fft"))
secondary_command_fft = secondary_command_fft.format(
@ -394,7 +358,7 @@ class dsp(object):
logger.debug("secondary command (fft) = %s", secondary_command_fft)
self.secondary_process_fft = subprocess.Popen(
secondary_command_fft, stdout=subprocess.PIPE, shell=True, start_new_session=True, env=my_env
secondary_command_fft, stdout=subprocess.PIPE, shell=True, start_new_session=True
)
self.output.send_output(
"secondary_fft",
@ -406,34 +370,18 @@ class dsp(object):
# it would block if not read. by piping it to devnull, we avoid a potential pitfall here.
secondary_output = subprocess.DEVNULL if self.isPacket() else subprocess.PIPE
self.secondary_process_demod = subprocess.Popen(
secondary_command_demod, stdout=secondary_output, shell=True, start_new_session=True, env=my_env
secondary_command_demod, stdout=secondary_output, shell=True, start_new_session=True
)
self.secondary_processes_running = True
if self.isWsjtMode():
smd = self.get_secondary_demodulator()
chopper_profile = None
if smd == "ft8":
chopper_profile = Ft8Profile()
elif smd == "wspr":
chopper_profile = WsprProfile()
elif smd == "jt65":
chopper_profile = Jt65Profile()
elif smd == "jt9":
chopper_profile = Jt9Profile()
elif smd == "ft4":
chopper_profile = Ft4Profile()
if chopper_profile is not None:
chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile)
chopper.start()
self.output.send_output("wsjt_demod", chopper.read)
elif self.isJs8():
chopper = AudioChopper(self, self.secondary_process_demod.stdout, *Js8Profiles.getEnabledProfiles())
chopper.start()
self.output.send_output("js8_demod", chopper.read)
if self.isWsjtMode() or self.isJs8():
chopper = AudioChopper(self, self.get_secondary_demodulator())
chopper.send_output("audio", self.secondary_process_demod.stdout.read)
output_type = "js8_demod" if self.isJs8() else "wsjt_demod"
self.output.send_output(output_type, chopper.read)
elif self.isPacket():
# we best get the ax25 packets from the kiss socket
kiss = KissClient(self.direwolf_port)
kiss = KissClient(self.direwolf_config.getPort())
self.output.send_output("packet_demod", kiss.read)
elif self.isPocsag():
self.output.send_output("pocsag_demod", self.secondary_process_demod.stdout.readline)
@ -447,7 +395,9 @@ class dsp(object):
def set_secondary_offset_freq(self, value):
self.secondary_offset_freq = value
if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"):
self.pipes["secondary_shift_pipe"].write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
self.pipes["secondary_shift_pipe"].write(
"%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())
)
def stop_secondary_demodulator(self):
if not self.secondary_processes_running:
@ -478,11 +428,16 @@ class dsp(object):
return self.secondary_demodulator
def set_secondary_fft_size(self, secondary_fft_size):
# to change this, restart is required
if self.secondary_fft_size == secondary_fft_size:
return
self.secondary_fft_size = secondary_fft_size
self.restart()
def set_audio_compression(self, what):
if self.audio_compression == what:
return
self.audio_compression = what
self.restart()
def get_audio_bytes_to_read(self):
# desired latency: 5ms
@ -494,7 +449,10 @@ class dsp(object):
return int(base)
def set_fft_compression(self, what):
if self.fft_compression == what:
return
self.fft_compression = what
self.restart()
def get_fft_bytes_to_read(self):
if self.fft_compression == "none":
@ -515,25 +473,22 @@ class dsp(object):
self.restart()
def calculate_decimation(self):
(self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate())
(self.decimation, self.last_decimation) = self.get_decimation(self.samp_rate, self.get_audio_rate())
def get_decimation(self, input_rate, output_rate):
if output_rate <= 0:
raise ValueError("invalid output rate: {rate}".format(rate=output_rate))
decimation = 1
correction = 1
target_rate = output_rate
# wideband fm has a much higher frequency deviation (75kHz).
# we cannot cover this if we immediately decimate to the sample rate the audio will have later on, so we need
# to compensate here.
# the factor of 5 is by experimentation only, with a minimum audio rate of 36kHz (enforced by the client)
# this allows us to cover at least +/- 80kHz of frequency spectrum (may be higher, but that's the worst case).
# the correction factor is automatically compensated for by the secondary decimation stage, which comes
# after the demodulator.
if self.get_demodulator() == "wfm":
correction = 5
while input_rate / (decimation + 1) >= output_rate * correction:
if self.get_demodulator() == "wfm" and output_rate < 200000:
target_rate = 200000
while input_rate / (decimation + 1) >= target_rate:
decimation += 1
fraction = float(input_rate / decimation) / output_rate
intermediate_rate = input_rate / decimation
return decimation, fraction, intermediate_rate
return decimation, fraction
def if_samp_rate(self):
return self.samp_rate / self.decimation
@ -561,14 +516,14 @@ class dsp(object):
def isDigitalVoice(self, demodulator=None):
if demodulator is None:
demodulator = self.get_demodulator()
return demodulator in ["dmr", "dstar", "nxdn", "ysf"]
return demodulator in ["dmr", "dstar", "nxdn", "ysf", "m17"]
def isWsjtMode(self, demodulator=None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]
def isJs8(self, demodulator = None):
def isJs8(self, demodulator=None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator == "js8"
@ -625,6 +580,8 @@ class dsp(object):
return self.demodulator
def set_fft_size(self, fft_size):
if self.fft_size == fft_size:
return
self.fft_size = fft_size
self.restart()
@ -656,6 +613,9 @@ class dsp(object):
def get_operating_freq(self):
return self.center_freq + self.offset_freq
def set_bandpass(self, bandpass):
self.set_bpf(bandpass.low_cut, bandpass.high_cut)
def set_bpf(self, low_cut, high_cut):
self.low_cut = low_cut
self.high_cut = high_cut
@ -673,7 +633,11 @@ class dsp(object):
def set_squelch_level(self, squelch_level):
self.squelch_level = squelch_level
# no squelch required on digital voice modes
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV() else self.squelch_level
actual_squelch = (
-150
if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV()
else self.squelch_level
)
if self.running:
self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
@ -724,27 +688,34 @@ class dsp(object):
def try_create_configs(self, command):
if "{direwolf_config}" in command:
self.direwolf_config = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
self.direwolf_config_path = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
tmp_dir=self.temporary_directory, myid=id(self)
)
self.direwolf_port = KissClient.getFreePort()
file = open(self.direwolf_config, "w")
file.write(DirewolfConfig().getConfig(self.direwolf_port, self.is_service))
self.direwolf_config = DirewolfConfig()
self.direwolf_config.wire(self)
file = open(self.direwolf_config_path, "w")
file.write(self.direwolf_config.getConfig(self.is_service))
file.close()
else:
self.direwolf_config = None
self.direwolf_port = None
self.direwolf_config_path = None
def try_delete_configs(self):
if self.direwolf_config:
if self.direwolf_config is not None:
self.direwolf_config.unwire(self)
self.direwolf_config = None
if self.direwolf_config_path is not None:
try:
os.unlink(self.direwolf_config)
os.unlink(self.direwolf_config_path)
except FileNotFoundError:
# result suits our expectations. fine :)
pass
except Exception:
logger.exception("try_delete_configs()")
self.direwolf_config = None
self.direwolf_config_path = None
def onConfigChanged(self):
self.restart()
def start(self):
with self.modification_lock:
@ -795,14 +766,9 @@ class dsp(object):
)
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)
self.process = subprocess.Popen(command, stdout=out, shell=True, start_new_session=True)
def watch_thread():
rc = self.process.wait()
@ -826,6 +792,7 @@ class dsp(object):
self.start_secondary_demodulator()
if self.has_pipe("smeter_pipe"):
def read_smeter():
raw = self.pipes["smeter_pipe"].readline()
if len(raw) == 0:
@ -835,6 +802,7 @@ class dsp(object):
self.output.send_output("smeter", read_smeter)
if self.has_pipe("meta_pipe"):
def read_meta():
raw = self.pipes["meta_pipe"].readline()
if len(raw) == 0:
@ -844,10 +812,6 @@ class dsp(object):
self.output.send_output("meta", read_meta)
if self.csdr_dynamic_bufsize:
self.process.stdout.read(8) # dummy read to skip bufsize & preamble
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
def stop(self):
with self.modification_lock:
self.running = False
@ -863,12 +827,10 @@ class dsp(object):
self.stop_secondary_demodulator()
self.try_delete_pipes(self.pipe_names)
self.try_delete_configs()
def restart(self):
if not self.running:
return
self.stop()
self.start()
def __del__(self):
self.stop()

36
csdr/output.py Normal file
View File

@ -0,0 +1,36 @@
import threading
import logging
logger = logging.getLogger(__name__)
class Output(object):
def send_output(self, t, read_fn):
if not self.supports_type(t):
# TODO rewrite the output mechanism in a way that avoids producing unnecessary data
logger.warning("dumping output of type %s since it is not supported.", t)
threading.Thread(target=self.pump(read_fn, lambda x: None), name="csdr_pump_thread").start()
return
self.receive_output(t, read_fn)
def receive_output(self, t, read_fn):
pass
def pump(self, read, write):
def copy():
run = True
while run:
data = None
try:
data = read()
except ValueError:
pass
if data is None or (isinstance(data, bytes) and len(data) == 0):
run = False
else:
write(data)
return copy
def supports_type(self, t):
return True

View File

@ -42,6 +42,7 @@ class Pipe(object):
immediately here), resulting in empty reads until data is available. This is handled specially in the
ReadingPipe class.
"""
def opener(path, flags):
fd = os.open(path, flags | os.O_NONBLOCK)
os.set_blocking(fd, True)
@ -88,7 +89,7 @@ class WritingPipe(Pipe):
except OSError as error:
# ENXIO = FIFO has not been opened for reading
if error.errno == 6:
time.sleep(.1)
time.sleep(0.1)
retries += 1
else:
raise

40
debian/changelog vendored
View File

@ -1,3 +1,43 @@
openwebrx (1.0.0) buster hirsute; urgency=low
* Introduced `squelch_auto_margin` config option that allows configuring the
auto squelch level
* Removed `port` configuration option; `rtltcp_compat` takes the port number
with the new connectors
* Added support for new WSJT-X modes FST4, FST4W (only available with WSJT-X
2.3) and Q65 (only available with WSJT-X 2.4)
* Added support for demodulating M17 digital voice signals using
m17-cxx-demod
* New reporting infrastructure, allowing WSPR and FST4W spots to be sent to
wsprnet.org
* Add some basic filtering capabilities to the map
* New arguments to the `openwebrx` command-line to facilitate the
administration of users (try `openwebrx admin`)
* New command-line tool `openwebrx-admin` that facilitates the
administration of users
* Default bandwidth changes:
- "WFM" changed to 150kHz
- "Packet" (APRS) changed to 12.5kHz
* Configuration rework:
- New: fully web-based configuration interface
- System configuration parameters have been moved to a new, separate
`openwebrx.conf` file
- Remaining parameters are now editable in the web configuration
- Existing `config_webrx.py` files will still be read, but changes made in
the web configuration will be written to a new storage system
- Added upload of avatar and panorama image via web configuration
* New devices supported:
- HPSDR devices (Hermes Lite 2) thanks to @jancona
- BBRF103 / RX666 / RX888 devices supported by libsddc
- R&S devices using the EB200 or Ammos protocols
-- Jakob Ketterl <jakob.ketterl@gmx.de> Thu, 06 May 2021 17:22:00 +0000
openwebrx (0.20.3) buster focal; urgency=low
* Fix a compatibility issue with python versions <= 3.6
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 26 Jan 2021 15:28:00 +0000
openwebrx (0.20.2) buster focal; urgency=high
* Fix a security problem that allowed arbitrary commands to be executed on

6
debian/control vendored
View File

@ -10,7 +10,7 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git
Package: openwebrx
Architecture: all
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.17), netcat, owrx-connector (>= 0.3), python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends}
Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.17), netcat, owrx-connector (>= 0.4), soapysdr-tools, python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends}
Recommends: digiham (>= 0.4), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, runds-connector, hpsdrconnector, aprs-symbols, m17-demod, js8call
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

8
debian/openwebrx.config vendored Executable file
View File

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

1
debian/openwebrx.dirs vendored Normal file
View File

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

View File

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

59
debian/openwebrx.postinst vendored Executable file
View File

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

8
debian/openwebrx.postrm vendored Executable file
View File

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

23
debian/openwebrx.templates vendored Normal file
View File

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

7
debian/postinst vendored
View File

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

View File

@ -2,7 +2,7 @@
set -euo pipefail
ARCH=$(uname -m)
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-redpitaya openwebrx-rtltcp openwebrx-full openwebrx"
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-full openwebrx"
ALL_ARCHS="x86_64 armv7l aarch64"
TAG=${TAG:-"latest"}
ARCHTAG="$TAG-$ARCH"

View File

@ -4,6 +4,7 @@ COPY docker/files/js8call/js8call-hamlib.patch \
docker/files/wsjtx/wsjtx.patch \
docker/files/wsjtx/wsjtx-hamlib.patch \
docker/files/dream/dream.patch \
docker/files/direwolf/direwolf-hamlib.patch \
docker/scripts/install-dependencies.sh /
RUN /install-dependencies.sh && \
rm /install-dependencies.sh && \
@ -17,7 +18,9 @@ ENTRYPOINT ["/init"]
WORKDIR /opt/openwebrx
VOLUME /etc/openwebrx
VOLUME /var/lib/openwebrx
CMD [ "/opt/openwebrx/docker/scripts/run.sh" ]
ENV S6_CMD_ARG0="/opt/openwebrx/docker/scripts/run.sh"
CMD []
EXPOSE 8073

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,17 @@
--- CMakeLists.txt.orig 2020-07-21 20:59:55.982026645 +0200
+++ CMakeLists.txt 2020-07-21 21:01:25.444836112 +0200
@@ -80,24 +80,6 @@
--- CMakeLists.txt.orig 2021-03-30 15:28:36.956587995 +0200
+++ CMakeLists.txt 2021-03-30 15:29:45.719326832 +0200
@@ -106,24 +106,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_upstream}
- GIT_SHALLOW False
- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/${__hamlib_upstream}.tar.gz
- URL_HASH MD5=${hamlib_md5sum}
- #UPDATE_COMMAND ${CMAKE_COMMAND} -E env "[ -f ./bootstrap ] && ./bootstrap"
- PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch
@ -22,10 +21,11 @@
- STEP_TARGETS update install
- )
-
#
-#
# custom target to make a hamlib source tarball
#
@@ -136,7 +118,6 @@
add_custom_target (hamlib_sources
@@ -161,7 +143,6 @@
# build and optionally install WSJT-X using the hamlib package built
# above
#
@ -33,11 +33,18 @@
ExternalProject_Add (wsjtx
GIT_REPOSITORY ${wsjtx_repo}
GIT_TAG ${WSJTX_TAG}
@@ -160,7 +141,6 @@
@@ -186,14 +167,8 @@
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)
-add_dependencies (wsjtx-configure hamlib-install)
-add_dependencies (wsjtx-build hamlib-install)
-add_dependencies (wsjtx-install hamlib-install)
-add_dependencies (wsjtx-package hamlib-install)
-
# export traditional targets
add_custom_target (build ALL DEPENDS wsjtx-build)
add_custom_target (install DEPENDS wsjtx-install)

View File

@ -1,6 +1,6 @@
diff -ur wsjtx-orig/CMake/Modules/Findhamlib.cmake wsjtx/CMake/Modules/Findhamlib.cmake
--- wsjtx-orig/CMake/Modules/Findhamlib.cmake 2020-07-21 21:10:43.124810140 +0200
+++ wsjtx/CMake/Modules/Findhamlib.cmake 2020-07-21 21:11:03.368019114 +0200
--- wsjtx-orig/CMake/Modules/Findhamlib.cmake 2021-02-01 20:38:00.947536514 +0100
+++ wsjtx/CMake/Modules/Findhamlib.cmake 2021-02-01 20:39:06.273680932 +0100
@@ -85,4 +85,4 @@
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
# TRUE if all listed variables are TRUE
@ -8,9 +8,18 @@ diff -ur wsjtx-orig/CMake/Modules/Findhamlib.cmake wsjtx/CMake/Modules/Findhamli
-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
+find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
--- wsjtx-orig/CMakeLists.txt 2020-07-21 21:10:43.124810140 +0200
+++ wsjtx/CMakeLists.txt 2020-07-21 22:14:04.454639589 +0200
@@ -871,7 +871,7 @@
--- wsjtx-orig/CMakeLists.txt 2021-02-01 20:38:00.947536514 +0100
+++ wsjtx/CMakeLists.txt 2021-02-01 23:02:22.503027275 +0100
@@ -122,7 +122,7 @@
option (WSJT_QDEBUG_TO_FILE "Redirect Qt debuging messages to a trace file.")
option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON)
option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.")
-option (WSJT_GENERATE_DOCS "Generate documentation files." ON)
+option (WSJT_GENERATE_DOCS "Generate documentation files.")
option (WSJT_RIG_NONE_CAN_SPLIT "Allow split operation with \"None\" as rig.")
option (WSJT_TRACE_UDP "Debugging option that turns on UDP message protocol diagnostics.")
option (WSJT_BUILD_UTILS "Build simulators and code demonstrators." ON)
@@ -856,7 +856,7 @@
#
# libhamlib setup
#
@ -19,31 +28,37 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
find_package (hamlib 3 REQUIRED)
find_program (RIGCTL_EXE rigctl)
find_program (RIGCTLD_EXE rigctld)
@@ -1348,53 +1348,10 @@
endif(WSJT_BUILD_UTILS)
@@ -1376,60 +1376,6 @@
target_link_libraries (jt9 wsjt_fort wsjt_cxx fort_qt)
endif (${OPENMP_FOUND} OR APPLE)
-# build the main application
-generate_version_info (wsjtx_VERSION_RESOURCES
- NAME wsjtx
- BUNDLE ${PROJECT_BUNDLE_NAME}
- ICON ${WSJTX_ICON_FILE}
- )
-
-add_executable (wsjtx MACOSX_BUNDLE
- ${wsjtx_CXXSRCS}
- ${wsjtx_GENUISRCS}
- wsjtx.rc
- ${WSJTX_ICON_FILE}
- ${wsjtx_RESOURCES_RCC}
- ${wsjtx_VERSION_RESOURCES}
- )
-
if (WSJT_CREATE_WINMAIN)
set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON)
endif (WSJT_CREATE_WINMAIN)
-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_INFO_STRING "${PROJECT_DESCRIPTION}"
- 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_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}
- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}"
- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${SCS_VERSION_STR}"
- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_BUNDLE_NAME}"
- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
- MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx"
@ -51,9 +66,9 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
-
-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS})
-if (APPLE)
- target_link_libraries (wsjtx Qt5::SerialPort wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
- target_link_libraries (wsjtx wsjt_fort)
-else ()
- target_link_libraries (wsjtx Qt5::SerialPort wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
- target_link_libraries (wsjtx wsjt_fort_omp)
- if (OpenMP_C_FLAGS)
- set_target_properties (wsjtx PROPERTIES
- COMPILE_FLAGS "${OpenMP_C_FLAGS}"
@ -65,15 +80,16 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
- )
- if (WIN32)
- set_target_properties (wsjtx PROPERTIES
- LINK_FLAGS -Wl,--stack,16777216
- LINK_FLAGS -Wl,--stack,0x1000000,--heap,0x20000000
- )
- endif ()
-endif ()
-target_link_libraries (wsjtx Qt5::SerialPort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES} ${LIBM_LIBRARIES})
-
# 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})
@@ -1437,24 +1394,9 @@
@@ -1492,24 +1438,9 @@
set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
endif (WSJT_CREATE_WINMAIN)
@ -98,21 +114,21 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# install (TARGETS wsjtx_udp EXPORT udp
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -1473,12 +1415,7 @@
@@ -1528,12 +1459,7 @@
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
# )
-install (TARGETS udp_daemon message_aggregator
-install (TARGETS udp_daemon message_aggregator wsjtx_app_version
- 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
+install (TARGETS wsjtx_app_version jt9 wsprd
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
)
@@ -1491,39 +1428,6 @@
@@ -1546,38 +1472,6 @@
)
endif(WSJT_BUILD_UTILS)
@ -143,13 +159,25 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
- 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
cty.dat
cty.dat_copyright.txt
@@ -1586,13 +1480,6 @@
#COMPONENT runtime
)
-install (DIRECTORY
- example_log_configurations
- DESTINATION ${CMAKE_INSTALL_DOCDIR}
- FILES_MATCHING REGEX "^.*[^~]$"
- #COMPONENT runtime
- )
-
#
# Mac installer files
#

View File

@ -24,7 +24,7 @@ apt-get update
apt-get -y install --no-install-recommends $BUILD_PACKAGES
git clone https://github.com/jketterl/owrx_connector.git
cmakebuild owrx_connector 0.3.0
cmakebuild owrx_connector 0.4.0
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean

View File

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

View File

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

View File

@ -18,8 +18,8 @@ function cmakebuild() {
cd /tmp
STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates libqt5gui5 libqt5sql5 libqt5printsupport5 libpulse0 libfaad2 libopus0"
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-default libfaad-dev libopus-dev"
STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates libqt5gui5 libqt5sql5 libqt5printsupport5 libpulse0 libfaad2 libopus0 libboost-program-options1.67.0 libboost-log1.67.0"
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-default libfaad-dev libopus-dev libgtest-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev"
apt-get update
apt-get -y install auto-apt-proxy
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
@ -60,7 +60,7 @@ rm /js8call-hamlib.patch
CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_HAMLIB_THREE" cmakebuild ${JS8CALL_DIR}
rm ${JS8CALL_TGZ}
WSJT_DIR=wsjtx-2.2.2
WSJT_DIR=wsjtx-2.3.1
WSJT_TGZ=${WSJT_DIR}.tgz
wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
tar xfz ${WSJT_TGZ}
@ -69,13 +69,17 @@ 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.6 https://github.com/wb2osz/direwolf.git
cd direwolf
# hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need.
# by setting enable_hamlib we prevent direwolf from linking to it, and it can be stripped at the end of the script.
make enable_hamlib=
# this patch prevents direwolf from linking to it, and it can be stripped at the end of the script.
patch -Np1 < /direwolf-hamlib.patch
mkdir build
cd build
cmake ..
make
make install
cd ..
cd ../..
rm -rf direwolf
# strip lots of generic documentation that will never be read inside a docker container
rm /usr/local/share/doc/direwolf/*.pdf
@ -106,9 +110,15 @@ popd
rm -rf dream
rm dream-2.1.1-svn808.tar.gz
git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols
pushd /opt/aprs-symbols
git clone https://github.com/mobilinkd/m17-cxx-demod.git
# latest master as of 2021-04-20
cmakebuild m17-cxx-demod c1d954fd5e5c53d28a2524e99484f832f9dcb826
git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols
pushd /usr/share/aprs-symbols
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
# remove unused files (including git meta information)
rm -rf .git aprs-symbols.ai aprs-sym-export.js
popd
apt-get -y purge --autoremove $BUILD_PACKAGES

View File

@ -41,7 +41,7 @@ cd ..
rm -rf csdr
git clone https://github.com/jketterl/digiham.git
cmakebuild digiham 0.3.0
cmakebuild digiham 0.4.0
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@ -36,15 +36,14 @@ input
vertical-align:middle;
}
input[type=range]
{
input[type=range] {
-webkit-appearance: none;
margin: 0 0;
background: transparent;
background: transparent !important;
--track-background: #B6B6B6;
}
input[type=range]:focus
{
input[type=range]:focus {
outline: none;
}
@ -297,11 +296,13 @@ input[type=range]:disabled {
#webrx-canvas-container canvas
{
position: absolute;
top: 0;
border-style: none;
image-rendering: crisp-edges;
image-rendering: -webkit-optimize-contrast;
width: 100%;
height: 200px;
will-change: transform;
}
#openwebrx-log-scroll
@ -336,12 +337,58 @@ input[type=range]:disabled {
margin: 0;
display: flex;
flex-direction: row;
cursor: pointer;
}
.webrx-actual-freq > * {
flex: 1;
}
.webrx-actual-freq .input-group {
display: flex;
flex-direction: row;
}
.webrx-actual-freq .input-group > * {
flex: 0 0 auto;
}
.webrx-actual-freq .input-group input {
flex: 1 0 auto;
margin-right: 0;
border-right: 1px solid #373737;
-moz-appearance: textfield;
}
.webrx-actual-freq .input-group input::-webkit-outer-spin-button,
.webrx-actual-freq .input-group input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.input-group > :not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > :not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group :first-child {
padding-left: 5px;
}
.input-group :last-child {
padding-right: 5px
}
.webrx-actual-freq .input-group input, .webrx-actual-freq .input-group select {
outline: none;
font-size: 16pt;
}
.webrx-actual-freq input {
font-family: 'roboto-mono';
width: 0;
@ -355,7 +402,17 @@ input[type=range]:disabled {
.webrx-actual-freq, .webrx-actual-freq input {
font-size: 16pt;
font-family: 'roboto-mono';
line-height: 22px;
}
.webrx-actual-freq .digit {
cursor: ns-resize;
}
.webrx-actual-freq .digit:hover {
color: #FFFF50;
border-radius: 5px;
background: -webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) );
background: -moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
}
.webrx-mouse-freq {
@ -544,21 +601,35 @@ img.openwebrx-mirror-img
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
overflow: hidden;
z-index: 1
}
.openwebrx-progressbar-bar
{
.openwebrx-progressbar-bar {
background-color: #00aba6;
border-radius: 5px;
height: 100%;
width: 100%;
transition-property: transform, background-color;
transition-duration: 1s;
transition-timing-function: ease-in-out;
transform: translate(-100%) translateZ(0);
will-change: transform, background-color;
z-index: 0;
}
.openwebrx-progressbar--over .openwebrx-progressbar-bar {
background-color: #ff6262;
}
.openwebrx-progressbar-text
{
position: absolute;
left:0px;
top:4px;
width: inherit;
left:50%;
top:50%;
transform: translate(-50%, -50%);
white-space: nowrap;
z-index: 2;
}
#openwebrx-panel-status
@ -583,6 +654,7 @@ img.openwebrx-mirror-img
#openwebrx-panel-receiver .frequencies-container {
display: flex;
flex-direction: row;
gap: 5px;
}
#openwebrx-panel-receiver .frequencies {
@ -658,8 +730,7 @@ img.openwebrx-mirror-img
}
}
#openwebrx-smeter-outer
{
#openwebrx-smeter {
border-color: #888;
border-style: solid;
border-width: 0px;
@ -667,16 +738,20 @@ img.openwebrx-mirror-img
height: 7px;
background-color: #373737;
border-radius: 3px;
position: relative;
overflow: hidden;
}
#openwebrx-smeter-bar
{
transition: all 0.2s linear;
width: 0px;
height: 7px;
.openwebrx-smeter-bar {
transition-property: transform;
transition-duration: 0.2s;
transition-timing-function: linear;
will-change: transform;
transform: translate(-100%) translateZ(0);
width: 100%;
height: 100%;
background: linear-gradient(to top, #ff5939 , #961700);
position: absolute;
margin: 0; padding: 0; left: 0;
margin: 0;
padding: 0;
border-radius: 3px;
}
@ -746,11 +821,14 @@ img.openwebrx-mirror-img
#openwebrx-digimode-canvas-container canvas
{
position: absolute;
top: 0;
pointer-events: none;
transition: width 500ms, left 500ms;
will-change: transform;
}
.openwebrx-panel select,
.openwebrx-panel input,
.openwebrx-dialog select,
.openwebrx-dialog input {
border-radius: 5px;
@ -759,11 +837,26 @@ img.openwebrx-mirror-img
font-weight: normal;
font-size: 13pt;
margin-right: 1px;
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) );
background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
background:linear-gradient(#373737, #4F4F4F);
border-color: transparent;
border-width: 0px;
-moz-appearance: none;
}
@supports(-moz-appearance: none) {
.openwebrx-panel select,
.openwebrx-dialog select {
-moz-appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%20%20xmlns%3Av%3D%22https%3A%2F%2Fvecta.io%2Fnano%22%3E%3Cpath%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8s-1.9-9.2-5.5-12.8z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E'),
linear-gradient(#373737, #4F4F4F);
background-repeat: no-repeat, repeat;
background-position: right .3em top 50%, 0 0;
background-size: .65em auto, 100%;
}
.openwebrx-panel .input-group select,
.openwebrx-dialog .input-group select {
padding-right: 1em;
}
}
.openwebrx-panel select option,
@ -886,20 +979,36 @@ img.openwebrx-mirror-img
border-color: Red;
}
.openwebrx-meta-panel {
display: flex;
flex-direction: row;
gap: 10px;
/* compatibility with iOS 14.2 */
flex: 0 0 auto;
}
.openwebrx-meta-slot {
flex: 1;
width: 145px;
height: 196px;
float: left;
margin-right: 10px;
background-color: #676767;
padding: 2px 0;
color: #333;
text-align: center;
display: flex;
flex-direction: column;
position: relative;
}
.openwebrx-meta-slot > * {
flex: 0;
flex-basis: 1.2em;
line-height: 1.2em;
}
.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before {
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
@ -910,8 +1019,6 @@ img.openwebrx-mirror-img
display: block;
content: "";
background-image: url("../gfx/openwebrx-mute.png");
width:100%;
height:133px;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
@ -939,27 +1046,44 @@ img.openwebrx-mirror-img
box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px;
}
.openwebrx-meta-slot:last-child {
margin-right: 0;
}
.openwebrx-meta-slot .openwebrx-meta-user-image {
width:100%;
height:133px;
flex: 1;
background-position: center;
background-repeat: no-repeat;
}
.openwebrx-meta-slot.active .openwebrx-meta-user-image {
.openwebrx-meta-slot.active.direct .openwebrx-meta-user-image,
#openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image {
background-image: url("../gfx/openwebrx-directcall.png");
}
.openwebrx-meta-slot.active .openwebrx-meta-user-image.group {
.openwebrx-meta-slot.active.group .openwebrx-meta-user-image {
background-image: url("../gfx/openwebrx-groupcall.png");
}
.openwebrx-meta-slot.group .openwebrx-dmr-target:not(:empty):before {
content: "Talkgroup: ";
}
.openwebrx-meta-slot.direct .openwebrx-dmr-target:not(:empty):before {
content: "Direct: ";
}
.openwebrx-dmr-timeslot-panel * {
cursor: pointer;
user-select: none;
}
.openwebrx-ysf-mode:not(:empty):before {
content: "Mode: ";
}
.openwebrx-ysf-up:not(:empty):before {
content: "Up: ";
}
.openwebrx-ysf-down:not(:empty):before {
content: "Down: ";
}
.openwebrx-maps-pin {
@ -974,6 +1098,7 @@ img.openwebrx-mirror-img
.openwebrx-message-panel {
height: 180px;
position: relative;
}
.openwebrx-message-panel tbody {
@ -1139,6 +1264,9 @@ img.openwebrx-mirror-img
#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="js8"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
@ -1146,7 +1274,10 @@ img.openwebrx-mirror-img
#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="pocsag"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-select-channel
{
display: none;
}
@ -1158,7 +1289,10 @@ img.openwebrx-mirror-img
#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="pocsag"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container
{
height: 200px;
margin: -10px;
@ -1205,12 +1339,12 @@ img.openwebrx-mirror-img
height: 15px;
}
#openwebrx-mute-on .sprite-speaker {
background-position: -117px -38px;
.openwebrx-mute-button .sprite-speaker {
background-position: -103px -38px;
}
#openwebrx-mute-off .sprite-speaker {
background-position: -103px -38px;
.openwebrx-mute-button.muted .sprite-speaker {
background-position: -117px -38px;
}
.sprite-squelch {

View File

@ -3,7 +3,6 @@
<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" />
<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="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
@ -11,6 +10,7 @@
</HEAD><BODY>
${header}
<div class="container">
${breadcrumb}
<h1>OpenWebRX Feature Report</h1>
<table class="features table">
<tr>
@ -20,5 +20,6 @@
<th>Available</th>
</tr>
</table>
${breadcrumb}
</div>
</BODY></HTML>

View File

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

View File

@ -1,20 +0,0 @@
<!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: 90 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1 @@
<svg width="700" height="700" xmlns="http://www.w3.org/2000/svg"><g class="layer"><circle cx="350" cy="350" r="330" stroke="#fff" stroke-width="36" fill="none"/><path d="M195 211v278l366-139-366-139z" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,26 +1,22 @@
<div id="webrx-top-container">
<div id="webrx-top-photo-clip">
<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">
<a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a>
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
<div id="webrx-rx-texts">
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
</div>
<div id="openwebrx-rx-details-arrow">
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger" style="display: none;"><span class="sprite sprite-rx-details-arrow-up"></span></a>
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><span class="sprite sprite-rx-details-arrow-down"></span></a>
</div>
<section id="openwebrx-main-buttons">
<div class="button" data-toggle-panel="openwebrx-panel-status"><span class="sprite sprite-panel-status"></span><br/>Status</div>
<div class="button" data-toggle-panel="openwebrx-panel-log"><span class="sprite sprite-panel-log"></span><br/>Log</div>
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><span class="sprite sprite-panel-receiver"></span><br/>Receiver</div>
<a class="button" href="map" target="openwebrx-map"><span class="sprite sprite-panel-map"></span><br/>Map</a>
${settingslink}
</section>
<div class="webrx-top-container">
<div class="webrx-top-bar">
<a href="https://www.openwebrx.de/" target="_blank"><img src="${document_root}static/gfx/openwebrx-top-logo.png" class="webrx-top-logo" alt="OpenWebRX Logo"/></a>
<img class="webrx-rx-avatar openwebrx-photo-trigger" src="${document_root}static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
<div class="webrx-rx-texts openwebrx-photo-trigger">
<h1 class="webrx-rx-title">${receiver_name}</h1>
<div class="webrx-rx-desc">${receiver_location} | Loc: ${locator}, ASL: ${receiver_asl} m</div>
</div>
<div id="webrx-rx-photo-title"></div>
<div id="webrx-rx-photo-desc"></div>
<section class="openwebrx-main-buttons">
<div class="button" data-toggle-panel="openwebrx-panel-status"><span class="sprite sprite-panel-status"></span><br/>Status</div>
<div class="button" data-toggle-panel="openwebrx-panel-log"><span class="sprite sprite-panel-log"></span><br/>Log</div>
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><span class="sprite sprite-panel-receiver"></span><br/>Receiver</div>
<a class="button" href="${document_root}map" target="openwebrx-map"><span class="sprite sprite-panel-map"></span><br/>Map</a>
<a class="button" href="${document_root}settings" target="openwebrx-settings"><span class="sprite sprite-panel-settings"></span><br/>Settings</a>
</section>
</div>
<div class="openwebrx-description-container">
<div class="webrx-rx-photo-title">${photo_title}</div>
<div class="webrx-rx-photo-desc">${photo_desc}</div>
</div>
<a class="openwebrx-rx-details-arrow openwebrx-rx-details-arrow--down openwebrx-photo-trigger"><span class="sprite sprite-rx-details-arrow"></span></a>
</div>

View File

@ -4,7 +4,7 @@
This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@ -28,6 +28,8 @@
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#222" />
</head>
<body onload="openwebrx_init();">
<div id="webrx-page-container">
@ -56,67 +58,33 @@
</div>
</div>
</div>
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message">
<thead><tr>
<th>UTC</th>
<th class="decimal">dB</th>
<th class="decimal">DT</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-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>
<th>UTC</th>
<th class="callsign">Callsign</th>
<th class="coord">Coord</th>
<th class="message">Comment</th>
</tr></thead>
<tbody></tbody>
</table>
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message">
<thead><tr>
<th class="address">Address</th>
<th class="message">Message</th>
</tr></thead>
<tbody></tbody>
</table>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"></div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" style="display: none;" data-panel-name="metadata-ysf">
<div class="openwebrx-meta-frame">
<div class="openwebrx-meta-slot">
<div class="openwebrx-ysf-mode openwebrx-meta-autoclear"></div>
<div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-ysf-source openwebrx-meta-autoclear"></div>
<div class="openwebrx-ysf-up openwebrx-meta-autoclear"></div>
<div class="openwebrx-ysf-down openwebrx-meta-autoclear"></div>
</div>
<div class="openwebrx-meta-slot">
<div class="openwebrx-ysf-mode"></div>
<div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-ysf-source"><span class="location"></span><span class="callsign"></span></div>
<div class="openwebrx-ysf-up"></div>
<div class="openwebrx-ysf-down"></div>
</div>
</div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" style="display: none;" data-panel-name="metadata-dmr">
<div class="openwebrx-meta-frame">
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
<div class="openwebrx-dmr-slot">Timeslot 1</div>
<div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
</div>
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
<div class="openwebrx-dmr-slot">Timeslot 2</div>
<div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
</div>
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
<div class="openwebrx-dmr-slot">Timeslot 1</div>
<div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-dmr-id"></div>
<div class="openwebrx-dmr-name"></div>
<div class="openwebrx-dmr-target"></div>
</div>
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
<div class="openwebrx-dmr-slot">Timeslot 2</div>
<div class="openwebrx-meta-user-image"></div>
<div class="openwebrx-dmr-id"></div>
<div class="openwebrx-dmr-name"></div>
<div class="openwebrx-dmr-target"></div>
</div>
</div>
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" style="width: 619px;">
@ -158,13 +126,13 @@
</div>
<div class="openwebrx-modes openwebrx-panel-line"></div>
<div class="openwebrx-panel-line">
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><span class="sprite sprite-speaker openwebrx-sliderbtn-img"></span></div>
<div title="Mute on/off" class="openwebrx-button openwebrx-mute-button" onclick="toggleMute();"><span class="sprite sprite-speaker openwebrx-sliderbtn-img"></span></div>
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
<div title="Auto-adjust waterfall colors (right-click for continuous)" id="openwebrx-waterfall-colors-auto" class="openwebrx-button"><span class="sprite sprite-waterfall-auto openwebrx-sliderbtn-img"></span></div>
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
</div>
<div class="openwebrx-panel-line">
<div title="Auto-set squelch level" class="openwebrx-squelch-default openwebrx-button"><span class="sprite sprite-squelch openwebrx-sliderbtn-img"></span></div>
<div title="Auto-set squelch level" class="openwebrx-squelch-auto openwebrx-button"><span class="sprite sprite-squelch openwebrx-sliderbtn-img"></span></div>
<input title="Squelch" class="openwebrx-squelch-slider openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1">
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><span class="sprite sprite-waterfall-default openwebrx-sliderbtn-img"></span></div>
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
@ -177,20 +145,14 @@
<div id="openwebrx-smeter-db">0 dB</div>
</div>
<div class="openwebrx-panel-line">
<div id="openwebrx-smeter-outer">
<div id="openwebrx-smeter-bar"></div>
<div id="openwebrx-smeter">
<div class="openwebrx-smeter-bar"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="openwebrx-autoplay-overlay" class="openwebrx-overlay" style="display:none;">
<div class="overlay-content">
<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" />
<div>Start OpenWebRX</div>
</div>
</div>
<div id="openwebrx-error-overlay" class="openwebrx-overlay" style="display:none;">
<div class="overlay-content">
<div>This receiver is currently unavailable due to technical issues.</div>

View File

@ -6,15 +6,15 @@ function AudioEngine(maxBufferLength, audioReporter) {
this.audioReporter = audioReporter;
this.initStats();
this.resetStats();
var ctx = window.AudioContext || window.webkitAudioContext;
if (!ctx) {
return;
}
this.onStartCallbacks = [];
this.started = false;
this.audioContext = new ctx();
this.audioContext = this.buildAudioContext();
if (!this.audioContext) {
return;
}
var me = this;
this.audioContext.onstatechange = function() {
if (me.audioContext.state !== 'running') return;
@ -31,6 +31,38 @@ function AudioEngine(maxBufferLength, audioReporter) {
this.maxBufferSize = maxBufferLength * this.getSampleRate();
}
AudioEngine.prototype.buildAudioContext = function() {
var ctxClass = window.AudioContext || window.webkitAudioContext;
if (!ctxClass) {
return;
}
// known good sample rates
var goodRates = [48000, 44100, 96000]
// let the browser chose the sample rate, if it is good, use it
var ctx = new ctxClass({latencyHint: 'playback'});
if (goodRates.indexOf(ctx.sampleRate) >= 0) {
return ctx;
}
// if that didn't work, try if any of the good rates work
if (goodRates.some(function(sr) {
try {
ctx = new ctxClass({sampleRate: sr, latencyHint: 'playback'});
return true;
} catch (e) {
return false;
}
}, this)) {
return ctx;
}
// fallback: let the browser decide
// this may cause playback problems down the line
return new ctxClass({latencyHint: 'playback'});
}
AudioEngine.prototype.resume = function(){
this.audioContext.resume();
}
@ -193,6 +225,7 @@ AudioEngine.prototype.resetStats = function() {
};
AudioEngine.prototype.setupResampling = function() { //both at the server and the client
var targetRate = this.audioContext.sampleRate;
var audio_params = this.findRate(8000, 12000);
if (!audio_params) {
this.resamplingFactor = 0;

View File

@ -145,21 +145,3 @@ BookmarkBar.prototype.getDemodulatorPanel = function() {
BookmarkBar.prototype.getDemodulator = function() {
return this.getDemodulatorPanel().getDemodulator();
};
BookmarkLocalStorage = function(){
};
BookmarkLocalStorage.prototype.getBookmarks = function(){
return JSON.parse(window.localStorage.getItem("bookmarks")) || [];
};
BookmarkLocalStorage.prototype.setBookmarks = function(bookmarks){
window.localStorage.setItem("bookmarks", JSON.stringify(bookmarks));
};
BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
if (data.id) data = data.id;
var bookmarks = this.getBookmarks();
bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
this.setBookmarks(bookmarks);
};

View File

@ -0,0 +1,17 @@
BookmarkLocalStorage = function(){
};
BookmarkLocalStorage.prototype.getBookmarks = function(){
return JSON.parse(window.localStorage.getItem("bookmarks")) || [];
};
BookmarkLocalStorage.prototype.setBookmarks = function(bookmarks){
window.localStorage.setItem("bookmarks", JSON.stringify(bookmarks));
};
BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
if (data.id) data = data.id;
var bookmarks = this.getBookmarks();
bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
this.setBookmarks(bookmarks);
};

View File

@ -5,12 +5,14 @@ function Filter(demodulator) {
Filter.prototype.getLimits = function() {
var max_bw;
if (this.demodulator.get_secondary_demod() === 'pocsag') {
if (['pocsag', 'packet'].indexOf(this.demodulator.get_secondary_demod()) >= 0) {
max_bw = 12500;
} else if (['dmr', 'dstar', 'nxdn', 'ysf', 'm17'].indexOf(this.demodulator.get_modulation()) >= 0) {
max_bw = 6250;
} else if (this.demodulator.get_modulation() === 'wfm') {
max_bw = 80000;
} else if (this.demodulator.get_modulation() === 'drm') {
max_bw = 100000;
} else if (this.demodulator.get_modulation() === 'drm') {
max_bw = 50000;
} else {
max_bw = (audioEngine.getOutputRate() / 2) - 1;
}
@ -234,7 +236,7 @@ Demodulator.prototype.emit = function(event, params) {
};
Demodulator.prototype.set_offset_frequency = function(to_what) {
if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
if (typeof(to_what) == 'undefined' || to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
to_what = Math.round(to_what);
if (this.offset_frequency === to_what) {
return;

View File

@ -3,6 +3,8 @@ function DemodulatorPanel(el) {
self.el = el;
self.demodulator = null;
self.mode = null;
self.squelchMargin = 10;
self.initialParams = {};
var displayEl = el.find('.webrx-actual-freq')
this.tuneableFrequencyDisplay = displayEl.tuneableFrequencyDisplay();
@ -10,6 +12,8 @@ function DemodulatorPanel(el) {
self.getDemodulator().set_offset_frequency(freq - self.center_freq);
});
this.mouseFrequencyDisplay = el.find('.webrx-mouse-freq').frequencyDisplay();
Modes.registerModePanel(this);
el.on('click', '.openwebrx-demodulator-button', function() {
var modulation = $(this).data('modulation');
@ -27,9 +31,9 @@ function DemodulatorPanel(el) {
self.setMode(value);
}
});
el.on('click', '.openwebrx-squelch-default', function() {
el.on('click', '.openwebrx-squelch-auto', function() {
if (!self.squelchAvailable()) return;
el.find('.openwebrx-squelch-slider').val(getLogSmeterValue(smeter_level) + 10);
el.find('.openwebrx-squelch-slider').val(getLogSmeterValue(smeter_level) + self.getSquelchMargin());
self.updateSquelch();
});
el.on('change', '.openwebrx-squelch-slider', function() {
@ -154,17 +158,20 @@ 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-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65"].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) {
var metaPanels = $(".openwebrx-meta-panel");
metaPanels.each(function (_, p) {
toggle_panel(p.id, p.id === showing);
});
clear_metadata();
metaPanels.metaPanel().each(function() {
this.clear();
});
};
DemodulatorPanel.prototype.getDemodulator = function() {
@ -177,7 +184,7 @@ DemodulatorPanel.prototype.collectParams = function() {
squelch_level: -150,
mod: 'nfm'
}
return $.extend(new Object(), defaults, this.initialParams || {}, this.transformHashParams(this.parseHash()));
return $.extend(new Object(), defaults, this.validateInitialParams(this.initialParams), this.transformHashParams(this.parseHash()));
};
DemodulatorPanel.prototype.startDemodulator = function() {
@ -203,7 +210,11 @@ DemodulatorPanel.prototype._apply = function(params) {
};
DemodulatorPanel.prototype.setInitialParams = function(params) {
this.initialParams = params;
$.extend(this.initialParams, params);
};
DemodulatorPanel.prototype.resetInitialParams = function() {
this.initialParams = {};
};
DemodulatorPanel.prototype.onHashChange = function() {
@ -247,7 +258,7 @@ DemodulatorPanel.prototype.updateButtons = function() {
}
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');
this.el.find('.openwebrx-squelch-auto')[squelch_disabled ? 'addClass' : 'removeClass']('disabled');
}
DemodulatorPanel.prototype.setCenterFrequency = function(center_freq) {
@ -283,7 +294,7 @@ DemodulatorPanel.prototype.validateHash = function(params) {
var self = this;
params = Object.keys(params).filter(function(key) {
if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') {
return params.freq && Math.abs(params.freq - self.center_freq) < bandwidth / 2;
return params.freq && Math.abs(params.freq - self.center_freq) <= bandwidth / 2;
}
return true;
}).reduce(function(p, key) {
@ -299,6 +310,17 @@ DemodulatorPanel.prototype.validateHash = function(params) {
return params;
};
DemodulatorPanel.prototype.validateInitialParams = function(params) {
return Object.fromEntries(
Object.entries(params).filter(function(a) {
if (a[0] == "offset_frequency") {
return Math.abs(a[1]) <= bandwidth / 2;
}
return true;
})
);
};
DemodulatorPanel.prototype.updateHash = function() {
var demod = this.getDemodulator();
if (!demod) return;
@ -322,6 +344,24 @@ DemodulatorPanel.prototype.updateSquelch = function() {
if (demod) demod.setSquelch(sliderValue);
};
DemodulatorPanel.prototype.setSquelchMargin = function(margin) {
if (typeof(margin) === 'undefined' || this.squelchMargin == margin) return;
this.squelchMargin = margin;
};
DemodulatorPanel.prototype.getSquelchMargin = function() {
return this.squelchMargin;
};
DemodulatorPanel.prototype.setMouseFrequency = function(freq) {
this.mouseFrequencyDisplay.setFrequency(freq);
};
DemodulatorPanel.prototype.setTuningPrecision = function(precision) {
this.tuneableFrequencyDisplay.setTuningPrecision(precision);
this.mouseFrequencyDisplay.setTuningPrecision(precision);
};
$.fn.demodulatorPanel = function(){
if (!this.data('panel')) {
this.data('panel', new DemodulatorPanel(this));

View File

@ -1,6 +1,14 @@
function FrequencyDisplay(element) {
this.suffixes = {
'': 0,
'k': 3,
'M': 6,
'G': 9,
'T': 12
};
this.element = $(element);
this.digits = [];
this.precision = 2;
this.setupElements();
this.setFrequency(0);
}
@ -8,13 +16,31 @@ function FrequencyDisplay(element) {
FrequencyDisplay.prototype.setupElements = function() {
this.displayContainer = $('<div>');
this.digitContainer = $('<span>');
this.displayContainer.html([this.digitContainer, $('<span> MHz</span>')]);
this.unitContainer = $('<span> Hz</span>');
this.displayContainer.html([this.digitContainer, this.unitContainer]);
this.element.html(this.displayContainer);
};
FrequencyDisplay.prototype.getSuffix = function() {
var me = this;
return Object.keys(me.suffixes).filter(function(key){
return me.suffixes[key] == me.exponent;
})[0] || "";
};
FrequencyDisplay.prototype.setFrequency = function(freq) {
this.frequency = freq;
var formatted = (freq / 1e6).toLocaleString(undefined, {maximumFractionDigits: 4, minimumFractionDigits: 4});
if (this.frequency === 0 || Number.isNaN(this.frequency)) {
this.exponent = 0
} else {
this.exponent = Math.floor(Math.log10(this.frequency) / 3) * 3;
}
var digits = Math.max(0, this.exponent - this.precision);
var formatted = (freq / 10 ** this.exponent).toLocaleString(
undefined,
{maximumFractionDigits: digits, minimumFractionDigits: digits}
);
var children = this.digitContainer.children();
for (var i = 0; i < formatted.length; i++) {
if (!this.digits[i]) {
@ -32,6 +58,13 @@ FrequencyDisplay.prototype.setFrequency = function(freq) {
while (this.digits.length > formatted.length) {
this.digits.pop().remove();
}
this.unitContainer.text(' ' + this.getSuffix() + 'Hz');
};
FrequencyDisplay.prototype.setTuningPrecision = function(precision) {
if (typeof(precision) == 'undefined') return;
this.precision = precision;
this.setFrequency(this.frequency);
};
function TuneableFrequencyDisplay(element) {
@ -43,22 +76,28 @@ TuneableFrequencyDisplay.prototype = new FrequencyDisplay();
TuneableFrequencyDisplay.prototype.setupElements = function() {
FrequencyDisplay.prototype.setupElements.call(this);
this.input = $('<input>');
this.input.hide();
this.element.append(this.input);
this.input = $('<input type="number" step="any">');
this.suffixInput = $('<select tabindex="-1">');
this.suffixInput.append($.map(this.suffixes, function(e, p) {
return $('<option value="' + e + '">' + p + 'Hz</option>');
}));
this.inputGroup = $('<div class="input-group">');
this.inputGroup.append([this.input, this.suffixInput]);
this.inputGroup.hide();
this.element.append(this.inputGroup);
};
TuneableFrequencyDisplay.prototype.setupEvents = function() {
var me = this;
me.element.on('wheel', function(e){
me.displayContainer.on('wheel', function(e){
e.preventDefault();
e.stopPropagation();
var index = me.digitContainer.find('.digit').index(e.target);
if (index < 0) return;
var delta = 10 ** (Math.floor(Math.max(6, Math.log10(me.frequency))) - index);
var delta = 10 ** (Math.floor(Math.max(me.exponent, Math.log10(me.frequency))) - index);
if (e.originalEvent.deltaY > 0) delta *= -1;
var newFrequency = me.frequency + delta;
@ -66,26 +105,64 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
});
var submit = function(){
var freq = parseInt(me.input.val());
var exponent = parseInt(me.suffixInput.val());
var freq = parseFloat(me.input.val()) * 10 ** exponent;
if (!isNaN(freq)) {
me.element.trigger('frequencychange', freq);
}
me.input.hide();
me.inputGroup.hide();
me.displayContainer.show();
};
me.input.on('blur', submit).on('keyup', function(e){
$inputs = $.merge($(), me.input);
$inputs = $.merge($inputs, me.suffixInput);
$('body').on('click', function(e) {
if (!me.input.is(':visible')) return;
if ($.contains(me.element[0], e.target)) return;
submit();
});
$inputs.on('blur', function(e){
if ($inputs.toArray().indexOf(e.relatedTarget) >= 0) {
return;
}
submit();
});
me.input.on('keydown', function(e){
if (e.keyCode == 13) return submit();
if (e.keyCode == 27) {
me.input.hide();
me.inputGroup.hide();
me.displayContainer.show();
return;
}
var c = String.fromCharCode(e.which);
Object.entries(me.suffixes).forEach(function(e) {
if (e[0].toUpperCase() == c) {
me.suffixInput.val(e[1]);
return submit();
}
})
});
me.input.on('click', function(e){
var currentExponent;
me.suffixInput.on('change', function() {
var newExponent = me.suffixInput.val();
delta = currentExponent - newExponent;
if (delta >= 0) {
me.input.val(parseFloat(me.input.val()) * 10 ** delta);
} else {
// should not be necessary to handle this separately, but floating point precision in javascript
// does not handle this well otherwise
me.input.val(parseFloat(me.input.val()) / 10 ** -delta);
}
currentExponent = newExponent;
me.input.focus();
});
$inputs.on('click', function(e){
e.stopPropagation();
});
me.element.on('click', function(){
me.input.val(me.frequency);
me.input.show();
currentExponent = me.exponent;
me.input.val(me.frequency / 10 ** me.exponent);
me.suffixInput.val(me.exponent);
me.inputGroup.show();
me.displayContainer.hide();
me.input.focus();
});

View File

@ -1,20 +1,23 @@
function Header(el) {
this.el = el;
this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () {
var $buttons = this.el.find('.openwebrx-main-buttons').find('[data-toggle-panel]').filter(function(){
// ignore buttons when the corresponding panel is not in the DOM
return $('#' + $(this).data('toggle-panel'))[0];
});
$buttons.css({display: 'block'}).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']);
this.el.find('.webrx-rx-title').html(details['receiver_name']);
this.el.find('.webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m');
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() {
@ -26,25 +29,19 @@ Header.prototype.init_rx_photo = function() {
}
});
$('#webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this));
$('.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();
this.el.find('.openwebrx-description-container').removeClass('expanded');
this.el.find(".openwebrx-rx-details-arrow").removeClass('openwebrx-rx-details-arrow--up').addClass('openwebrx-rx-details-arrow--down');
}
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();
this.el.find('.openwebrx-description-container').addClass('expanded');
this.el.find(".openwebrx-rx-details-arrow").removeClass('openwebrx-rx-details-arrow--down').addClass('openwebrx-rx-details-arrow--up');
}
Header.prototype.toggle_rx_photo = function(ev) {
@ -58,13 +55,6 @@ Header.prototype.toggle_rx_photo = function(ev) {
}
};
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));
@ -73,5 +63,5 @@ $.fn.header = function() {
};
$(function(){
$('#webrx-top-container').header();
$('.webrx-top-container').header();
});

View File

@ -100,7 +100,13 @@ Js8Thread.prototype.purgeOldMessages = function() {
return this.messages.length;
};
Js8Thread.prototype.purge = function() {
this.message = [];
this.el.remove();
};
Js8Threader = function(el){
MessagePanel.call(this, el);
this.threads = [];
this.tbody = $(el).find('tbody');
var me = this;
@ -109,6 +115,28 @@ Js8Threader = function(el){
}, 15000);
};
Js8Threader.prototype = new MessagePanel();
Js8Threader.prototype.render = function() {
$(this.el).append($(
'<table>' +
'<thead><tr>' +
'<th>UTC</th>' +
'<th class="decimal freq">Freq</th>' +
'<th class="message">Message</th>' +
'</tr></thead>' +
'<tbody></tbody>' +
'</table>'
));
};
Js8Threader.prototype.clearMessages = function() {
this.threads.forEach(function(t) {
t.purge();
});
this.threads = [];
};
Js8Threader.prototype.purgeOldMessages = function() {
this.threads = this.threads.filter(function(t) {
return t.purgeOldMessages();

247
htdocs/lib/MessagePanel.js Normal file
View File

@ -0,0 +1,247 @@
function MessagePanel(el) {
this.el = el;
this.render();
this.initClearButton();
}
MessagePanel.prototype.render = function() {
};
MessagePanel.prototype.pushMessage = function(message) {
};
// automatic clearing is not enabled by default. call this method from the constructor to enable
MessagePanel.prototype.initClearTimer = function() {
var me = this;
if (me.removalInterval) clearInterval(me.removalInterval);
me.removalInterval = setInterval(function () {
me.clearMessages(1000);
}, 15000);
};
MessagePanel.prototype.clearMessages = function(toRemain) {
var $elements = $(this.el).find('tbody tr');
// limit to 1000 entries in the list since browsers get laggy at some point
var toRemove = $elements.length - toRemain;
if (toRemove <= 0) return;
$elements.slice(0, toRemove).remove();
};
MessagePanel.prototype.initClearButton = function() {
var me = this;
me.clearButton = $(
'<div class="openwebrx-button">Clear</div>'
);
me.clearButton.css({
position: 'absolute',
top: '10px',
right: '10px'
});
me.clearButton.on('click', function() {
me.clearMessages(0);
});
$(me.el).append(me.clearButton);
};
function WsjtMessagePanel(el) {
MessagePanel.call(this, el);
this.initClearTimer();
}
WsjtMessagePanel.prototype = new MessagePanel();
WsjtMessagePanel.prototype.render = function() {
$(this.el).append($(
'<table>' +
'<thead><tr>' +
'<th>UTC</th>' +
'<th class="decimal">dB</th>' +
'<th class="decimal">DT</th>' +
'<th class="decimal freq">Freq</th>' +
'<th class="message">Message</th>' +
'</tr></thead>' +
'<tbody></tbody>' +
'</table>'
));
};
WsjtMessagePanel.prototype.pushMessage = function(msg) {
var $b = $(this.el).find('tbody');
var t = new Date(msg['timestamp']);
var pad = function (i) {
return ('' + i).padStart(2, "0");
};
var linkedmsg = msg['msg'];
var matches;
var html_escape = function(input) {
return $('<div/>').text(input).html()
};
if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65'].indexOf(msg['mode']) >= 0) {
matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/);
if (matches && matches[2] !== 'RR73') {
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
} else {
linkedmsg = html_escape(linkedmsg);
}
} else if (['WSPR', 'FST4W'].indexOf(msg['mode']) >= 0) {
matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/);
if (matches) {
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + html_escape(matches[3]);
} else {
linkedmsg = html_escape(linkedmsg);
}
}
$b.append($(
'<tr data-timestamp="' + msg['timestamp'] + '">' +
'<td>' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '</td>' +
'<td class="decimal">' + msg['db'] + '</td>' +
'<td class="decimal">' + msg['dt'] + '</td>' +
'<td class="decimal freq">' + msg['freq'] + '</td>' +
'<td class="message">' + linkedmsg + '</td>' +
'</tr>'
));
$b.scrollTop($b[0].scrollHeight);
}
$.fn.wsjtMessagePanel = function(){
if (!this.data('panel')) {
this.data('panel', new WsjtMessagePanel(this));
};
return this.data('panel');
};
function PacketMessagePanel(el) {
MessagePanel.call(this, el);
this.initClearTimer();
}
PacketMessagePanel.prototype = new MessagePanel();
PacketMessagePanel.prototype.render = function() {
$(this.el).append($(
'<table>' +
'<thead><tr>' +
'<th>UTC</th>' +
'<th class="callsign">Callsign</th>' +
'<th class="coord">Coord</th>' +
'<th class="message">Comment</th>' +
'</tr></thead>' +
'<tbody></tbody>' +
'</table>'
));
};
PacketMessagePanel.prototype.pushMessage = function(msg) {
var $b = $(this.el).find('tbody');
var pad = function (i) {
return ('' + i).padStart(2, "0");
};
if (msg.type && msg.type === 'thirdparty' && msg.data) {
msg = msg.data;
}
var source = msg.source;
if (msg.type) {
if (msg.type === 'item') {
source = msg.item;
}
if (msg.type === 'object') {
source = msg.object;
}
}
var timestamp = '';
if (msg.timestamp) {
var t = new Date(msg.timestamp);
timestamp = pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds())
}
var link = '';
var classes = [];
var styles = {};
var overlay = '';
var stylesToString = function (s) {
return $.map(s, function (value, key) {
return key + ':' + value + ';'
}).join('')
};
if (msg.symbol) {
classes.push('aprs-symbol');
classes.push('aprs-symboltable-' + (msg.symbol.table === '/' ? 'normal' : 'alternate'));
styles['background-position-x'] = -(msg.symbol.index % 16) * 15 + 'px';
styles['background-position-y'] = -Math.floor(msg.symbol.index / 16) * 15 + 'px';
if (msg.symbol.table !== '/' && msg.symbol.table !== '\\') {
var s = {};
s['background-position-x'] = -(msg.symbol.tableindex % 16) * 15 + 'px';
s['background-position-y'] = -Math.floor(msg.symbol.tableindex / 16) * 15 + 'px';
overlay = '<div class="aprs-symbol aprs-symboltable-overlay" style="' + stylesToString(s) + '"></div>';
}
} else if (msg.lat && msg.lon) {
classes.push('openwebrx-maps-pin');
}
var attrs = [
'class="' + classes.join(' ') + '"',
'style="' + stylesToString(styles) + '"'
].join(' ');
if (msg.lat && msg.lon) {
link = '<a ' + attrs + ' href="map?callsign=' + encodeURIComponent(source) + '" target="openwebrx-map">' + overlay + '</a>';
} else {
link = '<div ' + attrs + '>' + overlay + '</div>'
}
$b.append($(
'<tr>' +
'<td>' + timestamp + '</td>' +
'<td class="callsign">' + source + '</td>' +
'<td class="coord">' + link + '</td>' +
'<td class="message">' + (msg.comment || msg.message || '') + '</td>' +
'</tr>'
));
$b.scrollTop($b[0].scrollHeight);
};
$.fn.packetMessagePanel = function() {
if (!this.data('panel')) {
this.data('panel', new PacketMessagePanel(this));
};
return this.data('panel');
};
PocsagMessagePanel = function(el) {
MessagePanel.call(this, el);
this.initClearTimer();
}
PocsagMessagePanel.prototype = new MessagePanel();
PocsagMessagePanel.prototype.render = function() {
$(this.el).append($(
'<table>' +
'<thead><tr>' +
'<th class="address">Address</th>' +
'<th class="message">Message</th>' +
'</tr></thead>' +
'<tbody></tbody>' +
'</table>'
));
};
PocsagMessagePanel.prototype.pushMessage = function(msg) {
var $b = $(this.el).find('tbody');
$b.append($(
'<tr>' +
'<td class="address">' + msg.address + '</td>' +
'<td class="message">' + msg.message + '</td>' +
'</tr>'
));
$b.scrollTop($b[0].scrollHeight);
};
$.fn.pocsagMessagePanel = function() {
if (!this.data('panel')) {
this.data('panel', new PocsagMessagePanel(this));
};
return this.data('panel');
};

180
htdocs/lib/MetaPanel.js Normal file
View File

@ -0,0 +1,180 @@
function MetaPanel(el) {
this.el = el;
this.modes = [];
}
MetaPanel.prototype.update = function(data) {
};
MetaPanel.prototype.isSupported = function(data) {
return this.modes.includes(data.protocol);
};
MetaPanel.prototype.clear = function() {
this.el.find(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
};
function DmrMetaSlot(el) {
this.el = $(el);
this.clear();
}
DmrMetaSlot.prototype.update = function(data) {
this.el[data['sync'] ? "addClass" : "removeClass"]("sync");
if (data['sync'] && data['sync'] === "voice") {
this.setId(data['additional'] && data['additional']['callsign'] || data['source']);
this.setName(data['additional'] && data['additional']['fname']);
this.setMode(['group', 'direct'].includes(data['type']) ? data['type'] : undefined);
this.setTarget(data['target']);
this.el.addClass("active");
} else {
this.clear();
}
};
DmrMetaSlot.prototype.setId = function(id) {
if (this.id === id) return;
this.id = id;
this.el.find('.openwebrx-dmr-id').text(id || '');
}
DmrMetaSlot.prototype.setName = function(name) {
if (this.name === name) return;
this.name = name;
this.el.find('.openwebrx-dmr-name').text(name || '');
};
DmrMetaSlot.prototype.setMode = function(mode) {
if (this.mode === mode) return;
this.mode = mode;
var classes = ['group', 'direct'].filter(function(c){
return c !== mode;
});
this.el.removeClass(classes.join(' ')).addClass(mode);
}
DmrMetaSlot.prototype.setTarget = function(target) {
if (this.target === target) return;
this.target = target;
this.el.find('.openwebrx-dmr-target').text(target || '');
}
DmrMetaSlot.prototype.clear = function() {
this.setId();
this.setName();
this.setMode();
this.setTarget();
this.el.removeClass("active");
};
function DmrMetaPanel(el) {
MetaPanel.call(this, el);
this.modes = ['DMR'];
this.slots = this.el.find('.openwebrx-meta-slot').toArray().map(function(el){
return new DmrMetaSlot(el);
});
}
DmrMetaPanel.prototype = new MetaPanel();
DmrMetaPanel.prototype.update = function(data) {
if (!this.isSupported(data)) return;
if (data['slot']) {
var slot = this.slots[data['slot']];
slot.update(data);
} else {
this.clear();
}
}
DmrMetaPanel.prototype.clear = function() {
MetaPanel.prototype.clear.call(this);
this.el.find(".openwebrx-dmr-timeslot-panel").removeClass("muted");
this.slots.forEach(function(slot) {
slot.clear();
});
};
function YsfMetaPanel(el) {
MetaPanel.call(this, el);
this.modes = ['YSF'];
this.clear();
}
YsfMetaPanel.prototype = new MetaPanel();
YsfMetaPanel.prototype.update = function(data) {
if (!this.isSupported(data)) return;
this.setMode(data['mode']);
if (data['mode'] && data['mode'] !== "") {
this.setSource(data['source']);
this.setLocation(data['lat'], data['lon'], data['source']);
this.setUp(data['up']);
this.setDown(data['down']);
this.el.find(".openwebrx-meta-slot").addClass("active");
} else {
this.clear();
}
};
YsfMetaPanel.prototype.clear = function() {
MetaPanel.prototype.clear.call(this);
this.setMode();
this.setSource();
this.setLocation();
this.setUp();
this.setDown();
};
YsfMetaPanel.prototype.setMode = function(mode) {
if (this.mode === mode) return;
this.mode = mode;
this.el.find('.openwebrx-ysf-mode').text(mode || '');
};
YsfMetaPanel.prototype.setSource = function(source) {
if (this.source === source) return;
this.source = source;
this.el.find('.openwebrx-ysf-source .callsign').text(source || '');
};
YsfMetaPanel.prototype.setLocation = function(lat, lon, callsign) {
var hasLocation = lat && lon && callsign && callsign != '';
if (hasLocation === this.hasLocation && this.callsign === callsign) return;
this.hasLocation = hasLocation; this.callsign = callsign;
var html = '';
if (hasLocation) {
html = '<a class="openwebrx-maps-pin" href="map?callsign=' + encodeURIComponent(callsign) + '" target="_blank"></a>';
}
this.el.find('.openwebrx-ysf-source .location').html(html);
};
YsfMetaPanel.prototype.setUp = function(up) {
if (this.up === up) return;
this.up = up;
this.el.find('.openwebrx-ysf-up').text(up || '');
};
YsfMetaPanel.prototype.setDown = function(down) {
if (this.down === down) return;
this.down = down;
this.el.find('.openwebrx-ysf-down').text(down || '');
}
MetaPanel.types = {
dmr: DmrMetaPanel,
ysf: YsfMetaPanel
};
$.fn.metaPanel = function() {
return this.map(function() {
var $self = $(this);
if (!$self.data('metapanel')) {
var matches = /^openwebrx-panel-metadata-([a-z]+)$/.exec($self.prop('id'));
var constructor = matches && MetaPanel.types[matches[1]] || MetaPanel;
$self.data('metapanel', new constructor($self));
}
return $self.data('metapanel');
});
};

View File

@ -3,7 +3,6 @@ ProgressBar = function(el) {
this.$innerText = $('<span class="openwebrx-progressbar-text">' + this.getDefaultText() + '</span>');
this.$innerBar = $('<div class="openwebrx-progressbar-bar"></div>');
this.$el.empty().append(this.$innerText, this.$innerBar);
this.$innerBar.css('width', '0%');
};
ProgressBar.prototype.getDefaultText = function() {
@ -19,7 +18,7 @@ ProgressBar.prototype.set = function(val, text, over) {
ProgressBar.prototype.setValue = function(val) {
if (val < 0) val = 0;
if (val > 1) val = 1;
this.$innerBar.stop().animate({width: val * 100 + '%'}, 700);
this.$innerBar.css({transform: 'translate(' + ((val - 1) * 100) + '%) translateZ(0)'});
};
ProgressBar.prototype.setText = function(text) {
@ -27,7 +26,7 @@ ProgressBar.prototype.setText = function(text) {
};
ProgressBar.prototype.setOver = function(over) {
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
this.$el[over ? 'addClass' : 'removeClass']('openwebrx-progressbar--over');
};
AudioBufferProgressBar = function(el) {

6
htdocs/lib/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

3
htdocs/lib/jquery.nanoscroller.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
htdocs/lib/location-picker.min.js vendored Normal file
View File

@ -0,0 +1,2 @@
/* Taken from https://github.com/cyphercodes/location-picker under GPLv3 license */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.locationPicker=t()}(this,function(){"use strict";return function(e,t){void 0===t&&(t={});var n=t.insertAt;if(e&&"undefined"!=typeof document){var o=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style");i.type="text/css","top"===n&&o.firstChild?o.insertBefore(i,o.firstChild):o.appendChild(i),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(document.createTextNode(e))}}('.location-picker .centerMarker{position:absolute;background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAABWCAYAAACEsWWHAAAGLElEQVR4AdXcA2xsXReH8f3Ztm3btm2br23btm3btm2rd257b835vVnJStL0sj3Tds6TrOTkzN5r/5925qgorQRPwQewDHbHebgXDQxlNXLfeTlmmZzzlNJOpMyXsA86TJ2O7PGl6DnbQr/CncYx1tNj4JzTde+0la5V/6/x+x+Z85Ov6/jWZ0TFduyL12JMjI05E4iev5pxQXwFV0lGOx7Xf/xRulb5n45vflrH1z4xqYo5MTd6RC9JrvGVmRB6C04dLzR/yw10fP2TGbJyRa/oOVHw1Fh7uqS+jAY0e3uaPXvupOM7n48w01LRO9aItSDX/nKrpf6FYRi85AJzfvy1WHxGKtaKNZPhyNIKoadjZ0nfIfvFYrNSsXYiMz2tilg00Bweas7fZO1YYFYrMkQWKVfl7ScadS7z12jcFhVZxsn9ayoHimHI71RbVWRKhiPrZA7pjZn+TFX4zDWW6lSAU/PoFw3auiJjcuqSryjyPJWH9LauyBhZEXxlcdd+VyFOjDGxFhVZk6vCoUwEv5KXSXlFUYuKrJE5+dXCvlt3Ia7TYkKtKjIndxr/XcOXIMzzgrZWFZkje/Kl8WL7QP/xR7Z0wWs/9X5HvutNdnjdq2z6qpdFxXbsi9daulZkT/YZ/zbsQNwTtWSRuz7/YXu9+bU2ePlLFlcxJsa2ZM3InnSEU8EH5J1v3iRWqhs+9QGbv+plS5TKirExp/K6kT0ckg8ULIO4RW/Jd2q81GTkWvGdC4dkmYLdoXvHLSs3zrffVCrmVl4/HJLdC86r/vmKA8X7ImCVqnxACYfkvIL7EE+OKjWNI11FsehRKUM4JPcVNBCPxSo1jcN4RbHoUSlDOCSNgiHEM79KTeMcVVEselTKEA7JUMEA4pqr9mLhkAwUPAqNX36n9m/Fxi+/K3mk4GaY+/df1/7gEQ7JzQUnwPwN16h8XTjbh/twSE4oWBf6jjio9ifocEjWLfgeDN14be0vqcIh+V7BCzFobKw556ffqO1FcGQPh3AJpxLgROjebrPa3rZE9uTEEgT4IwzffENtbzQje/LH8WLPRQPycXatKjInjXAp48GGMHjx+bUTi8zJhmUieAX60ez8359rIxVZ0czsrygLA9uM+6zVoiJrsnVZFHg55sP8jddqe6nImMyP7GVxYA0Yffyxtn4iHNkiY7J6WRJ4Fu6G3n13a1uxyJbcHZnL0oDvQHNwsJmPDNqqIlNkg8haJgOOgKGrL287sciUHFEmC16FTpi/2bptIxVZks7IWKYC/grN+fOa+bBnVisyRBaIbKUKOBsGzjp11sUiQ3J2qQreij7oWn3ZWZOKtZO+yFRaAVaAsSceb3Z874szLhVrxtoQWUqrwFNxeeWfoVX/2dflkaW0ErwPQ2h2Lvf3GZOKtdDMtd9XpgOsByMP3q/j25+bdqlYI9ZK1ivTBZ6JW6D3kH2nXSzWSG6Jtct0gs+iaWSkOfcvv5g2qehtdLSJqM+WmQC7wND110ybWPROdikzBV6Ih2H+5uu1XCp6Jg/HWmUmwa+gOa+rOedHX22ZVPSKngh+VWYDnAP9Jx3TMrHolZxTZgu8B8PxBHbun39eWSp6RC8MR+8ym2AbGLzo3Mpi0SPZpsw2eCm6oPP/f5myVMxNuqJnaQewKgzdMPXDf8xNVi3tAp6Nh2DemstPWirmJNHj2aWdwDIwfNstkxaLOckypd3Ac/AEdP73T0stFWOTJ6JHaUewBgycecpSi8XYZI3SruBlGGwODy/VT0djTIzFYMwt7QwOhZ69d16iWIxJDi3tDr5CPh/5xqcWKRWvxRgEX6mDWP5GOF0r/muRYl0r/ltyV8wpdQCbQ98xhy1SLF5LNi91AV+A0ccfXaRYvJZ8oU5iT0cXNP7wExOlYl/SFWNLncARMH+L9U0Ui33JEaVuYGXoP/lYE8ViX7JyHcW+uahrx9iXfLOOYq+Bse75JorFvuQ1pY6gE8b/kV1sJ52lruCuib+9GtvJXXUWu2ri72fFdnJVncXOhvgXFykmtpOz6yx2MMzfdB0pJraTg+sstiwM33mbxm9/qPG7H4ntZNk6iz0Hd5hA7ntOqTN4OfbDo1mx/fIyzTwJJedUPgRWtocAAAAASUVORK5CYII=") no-repeat;background-size:100%;top:50%;left:50%;z-index:1;margin-left:-14px;margin-top:-43px;height:44px;width:28px;cursor:pointer}'),function(){function e(e,t,n){void 0===t&&(t={}),void 0===n&&(n={});var o={setCurrentPosition:!0};Object.assign(o,t);var i={center:new google.maps.LatLng(o.lat?o.lat:34.4346,o.lng?o.lng:35.8362),zoom:15};Object.assign(i,n),e instanceof HTMLElement?this.element=e:this.element=document.getElementById(e),this.map=new google.maps.Map(this.element,i);var r=document.createElement("div");r.classList.add("centerMarker"),this.element&&(this.element.classList.add("location-picker"),this.element.children[0].appendChild(r)),!o.setCurrentPosition||o.lat||o.lng||this.setCurrentPosition()}return e.prototype.getMarkerPosition=function(){var e=this.map.getCenter();return{lat:e.lat(),lng:e.lng()}},e.prototype.setLocation=function(e,t){this.map.setCenter(new google.maps.LatLng(e,t))},e.prototype.setCurrentPosition=function(){var e=this;navigator.geolocation?navigator.geolocation.getCurrentPosition(function(t){var n={lat:t.coords.latitude,lng:t.coords.longitude};e.map.setCenter(n)},function(){console.log("Could not determine your location...")}):console.log("Your browser does not support Geolocation.")},e}()});

View File

@ -30,7 +30,8 @@ var nite = {
fillOpacity: 0.1,
strokeOpacity: 0,
clickable: false,
editable: false
editable: false,
zIndex: 1
});
this.marker_twilight_nautical = new google.maps.Circle({
map: this.map,
@ -40,7 +41,8 @@ var nite = {
fillOpacity: 0.1,
strokeOpacity: 0,
clickable: false,
editable: false
editable: false,
zIndex: 1
});
this.marker_twilight_astronomical = new google.maps.Circle({
map: this.map,
@ -50,7 +52,8 @@ var nite = {
fillOpacity: 0.1,
strokeOpacity: 0,
clickable: false,
editable: false
editable: false,
zIndex: 1
});
this.marker_night = new google.maps.Circle({
map: this.map,
@ -60,7 +63,8 @@ var nite = {
fillOpacity: 0.1,
strokeOpacity: 0,
clickable: false,
editable: false
editable: false,
zIndex: 1
});
},
getShadowRadiusFromAngle: function(angle) {

View File

@ -0,0 +1,402 @@
function Editor(table) {
this.table = table;
}
Editor.prototype.getInputHtml = function() {
return '<input>';
}
Editor.prototype.render = function(el) {
this.input = $(this.getInputHtml());
el.append(this.input);
this.setupEvents();
};
Editor.prototype.setupEvents = function() {
var me = this;
this.input.on('blur', function() { me.submit(); }).on('change', function() { me.submit(); }).on('keydown', function(e){
if (e.keyCode == 13) return me.submit();
if (e.keyCode == 27) return me.cancel();
});
};
Editor.prototype.submit = function() {
if (!this.onSubmit) return;
var submit = this.onSubmit;
delete this.onSubmit;
submit();
};
Editor.prototype.cancel = function() {
if (!this.onCancel) return;
var cancel = this.onCancel;
delete this.onCancel;
cancel();
};
Editor.prototype.focus = function() {
this.input.focus();
};
Editor.prototype.disable = function(flag) {
this.input.prop('disabled', flag);
};
Editor.prototype.setValue = function(value) {
this.input.val(value);
};
Editor.prototype.getValue = function() {
return this.input.val();
};
Editor.prototype.getHtml = function() {
return this.getValue();
};
function NameEditor(table) {
Editor.call(this, table);
}
NameEditor.prototype = new Editor();
NameEditor.prototype.getInputHtml = function() {
return '<input class="form-control form-control-sm" type="text">';
}
function FrequencyEditor(table) {
Editor.call(this, table);
}
FrequencyEditor.suffixes = {
'': 0,
'K': 3,
'M': 6,
'G': 9,
'T': 12
};
FrequencyEditor.prototype = new Editor();
FrequencyEditor.prototype.getInputHtml = function() {
return '<div class="input-group input-group-sm exponential-input" name="frequency">' +
'<input class="form-control form-control-sm" type="number" step="1">' +
'<div class="input-group-append">' +
'<select class="input-group-text exponent" tabindex="-1">' +
$.map(FrequencyEditor.suffixes, function(v, k) {
// fix lowercase "kHz"
if (k === "K") k = "k";
return '<option value="' + v + '">' + k + 'Hz</option>';
}).join('') +
'</select>' +
'</div>' +
'</div>';
};
FrequencyEditor.prototype.render = function(el) {
this.input = $(this.getInputHtml());
el.append(this.input);
this.freqInput = el.find('input');
this.expInput = el.find('select');
this.setupEvents();
};
FrequencyEditor.prototype.setupEvents = function() {
var me = this;
var inputs = [this.freqInput, this.expInput].map(function(i) { return i[0]; });
inputs.forEach(function(input) {
$(input).on('blur', function(e){
if (inputs.indexOf(e.relatedTarget) >= 0) {
return;
}
me.submit();
});
});
var me = this;
this.freqInput.on('keydown', function(e){
if (e.keyCode == 13) return me.submit();
if (e.keyCode == 27) return me.cancel();
var c = String.fromCharCode(e.which);
if (c in FrequencyEditor.suffixes) {
me.expInput.val(FrequencyEditor.suffixes[c]);
}
});
}
FrequencyEditor.prototype.getValue = function() {
var frequency = parseFloat(this.freqInput.val());
var exp = parseInt(this.expInput.val());
return Math.floor(frequency * 10 ** exp);
};
FrequencyEditor.prototype.setValue = function(value) {
var value = parseFloat(value);
var exp = 0;
if (!Number.isNaN(value) && value > 0) {
exp = Math.floor(Math.log10(value) / 3) * 3;
}
this.freqInput.val(value / 10 ** exp);
this.expInput.val(exp);
};
FrequencyEditor.prototype.focus = function() {
this.freqInput.focus();
};
var renderFrequency = function(freq) {
var exp = 0;
if (!Number.isNaN(freq)) {
exp = Math.floor(Math.log10(freq) / 3) * 3;
}
var frequency = freq / 10 ** exp;
var suffix = Object.entries(FrequencyEditor.suffixes).find(function(e) {
return e[1] == exp;
});
if (!suffix) {
return freq + ' Hz';
}
// fix lowercase 'kHz'
suffix = suffix[0] == 'K' ? 'k' : suffix[0];
var expString = suffix[0] + 'Hz';
return frequency + ' ' + expString;
}
FrequencyEditor.prototype.getHtml = function() {
return renderFrequency(this.getValue());
};
function ModulationEditor(table) {
Editor.call(this, table);
this.modes = table.data('modes');
}
ModulationEditor.prototype = new Editor();
ModulationEditor.prototype.getInputHtml = function() {
return '<select class="form-control form-control-sm">' +
$.map(this.modes, function(name, modulation) {
return '<option value="' + modulation + '">' + name + '</option>';
}).join('') +
'</select>';
};
ModulationEditor.prototype.getHtml = function() {
var $option = this.input.find('option:selected')
return $option.html();
};
$.fn.bookmarktable = function() {
var editors = {
name: NameEditor,
frequency: FrequencyEditor,
modulation: ModulationEditor
};
$.each(this, function(){
var $table = $(this).find('table');
$table.on('dblclick', 'td', function(e) {
var $cell = $(e.target);
var html = $cell.html();
var $row = $cell.parents('tr');
var name = $cell.data('editor');
var EditorCls = editors[name];
if (!EditorCls) return;
var editor = new EditorCls($table);
editor.render($cell.html(''));
editor.setValue($cell.data('value'));
editor.focus();
editor.onSubmit = function() {
editor.disable(true);
$.ajax(document.location.href + "/" + $row.data('id'), {
data: JSON.stringify(Object.fromEntries([[name, editor.getValue()]])),
contentType: 'application/json',
method: 'POST'
}).done(function(){
$cell.data('value', editor.getValue());
$cell.html(editor.getHtml());
});
};
editor.onCancel = function() {
$cell.html(html);
};
});
var $modal = $('#deleteModal').modal({show:false});
$modal.on('hidden.bs.modal', function() {
var $row = $modal.data('row');
if (!$row) return;
$row.find('.bookmark-delete').prop('disabled', false);
$modal.removeData('row');
});
$modal.on('click', '.confirm', function() {
var $row = $modal.data('row');
if (!$row) return;
$.ajax(document.location.href + "/" + $row.data('id'), {
data: "{}",
contentType: 'application/json',
method: 'DELETE'
}).done(function(){
$row.remove();
$modal.modal('hide');
});
});
$table.on('click', '.bookmark-delete', function(e) {
var $button = $(e.target);
$button.prop('disabled', true);
var $row = $button.parents('tr');
$modal.data('row', $row);
$modal.modal('show');
});
$(this).on('click', '.bookmark-add', function() {
if ($table.find('tr[data-id="new"]').length) return;
$table.find('.emptytext').remove();
var row = $('<tr data-id="new">');
var inputs = Object.fromEntries(
Object.entries(editors).map(function(e) {
return [e[0], new e[1]($table)];
})
);
row.append($.map(inputs, function(editor, name){
var cell = $('<td data-editor="' + name + '" class="' + name + '">');
editor.render(cell);
return cell;
}));
row.append($(
'<td>' +
'<div class="btn-group btn-group-sm">' +
'<button type="button" class="btn btn-primary bookmark-save">Save</button>' +
'<button type="button" class="btn btn-secondary bookmark-cancel">Cancel</button>' +
'</div>' +
'</td>'
));
row.on('click', '.bookmark-cancel', function() {
row.remove();
});
row.on('click', '.bookmark-save', function() {
var data = Object.fromEntries(
$.map(inputs, function(input, name){
input.disable(true);
// double wrapped because jQuery.map() flattens the result
return [[name, input.getValue()]];
})
);
$.ajax(document.location.href, {
data: JSON.stringify([data]),
contentType: 'application/json',
method: 'POST'
}).done(function(data){
if (data.length && data.length === 1 && 'bookmark_id' in data[0]) {
row.attr('data-id', data[0]['bookmark_id']);
var tds = row.find('td');
Object.values(inputs).forEach(function(input, index) {
var td = $(tds[index]);
td.data('value', input.getValue());
td.html(input.getHtml());
});
var $cell = row.find('td').last();
var $group = $cell.find('.btn-group');
if ($group.length) {
$group.remove;
$cell.html('<div class="btn btn-sm btn-danger bookmark-delete">delete</div>');
}
}
});
});
$table.append(row);
row[0].scrollIntoView();
});
var $importModal = $('#importModal').modal({show: false});
$(this).find('.bookmark-import').on('click', function() {
var storage = new BookmarkLocalStorage();
var bookmarks = storage.getBookmarks();
if (bookmarks.length) {
var modes = $table.data('modes');
var $list = $('<table class="table table-sm">');
$list.append(bookmarks.map(function(b) {
var modulation = b.modulation;
if (modulation in modes) {
modulation = modes[modulation];
}
var row = $(
'<tr>' +
'<td><input class="form-check-input select" type="checkbox"></td>' +
'<td>' + b.name + '</td>' +
'<td class="frequency">' + renderFrequency(b.frequency) + '</td>' +
'<td>' + modulation + '</td>' +
'</tr>'
);
row.data('bookmark', b);
return row;
}));
$importModal.find('.bookmark-list').html($list);
} else {
$importModal.find('.bookmark-list').html('No personal bookmarks found in this browser');
}
$importModal.modal('show');
});
$importModal.on('click', '.confirm', function() {
var $list = $importModal.find('.bookmark-list table');
if ($list.length) {
var selected = $list.find('tr').filter(function(){
return $(this).find('.select').is(':checked');
}).map(function(){
return $(this).data('bookmark');
}).toArray();
if (selected.length) {
$.ajax(document.location.href, {
data: JSON.stringify(selected),
contentType: 'application/json',
method: 'POST'
}).done(function(data){
$table.find('.emptytext').remove();
var modes = $table.data('modes');
if (data.length && data.length == selected.length) {
$table.append(data.map(function(obj, index) {
var bookmark = selected[index];
var modulation_name = bookmark.modulation;
if (modulation_name in modes) {
modulation_name = modes[modulation_name];
}
return $(
'<tr data-id="' + obj.bookmark_id + '">' +
'<td data-editor="name" data-value="' + bookmark.name + '">' + bookmark.name + '</td>' +
'<td data-editor="frequency" data-value="' + bookmark.frequency + '" class="frequency">' + renderFrequency(bookmark.frequency) +'</td>' +
'<td data-editor="modulation" data-value="' + bookmark.modulation + '">' + modulation_name + '</td>' +
'<td>' +
'<button type="button" class="btn btn-sm btn-danger bookmark-delete">delete</button>' +
'</td>' +
'</tr>'
)
}));
}
});
}
}
$importModal.modal('hide');
});
});
};

View File

@ -0,0 +1,45 @@
$.fn.exponentialInput = function() {
var prefixes = {
'K': 3,
'M': 6,
'G': 9,
'T': 12
};
this.each(function(){
var $group = $(this);
var currentExponent = 0;
var $input = $group.find('input');
var setExponent = function() {
var newExponent = parseInt($exponent.val());
var delta = currentExponent - newExponent;
if (delta >= 0) {
$input.val(parseFloat($input.val()) * 10 ** delta);
} else {
// should not be necessary to handle this separately, but floating point precision in javascript
// does not handle this well otherwise
$input.val(parseFloat($input.val()) / 10 ** -delta);
}
currentExponent = newExponent;
};
$input.on('keydown', function(e) {
var c = String.fromCharCode(e.which);
if (c in prefixes) {
currentExponent = prefixes[c];
$exponent.val(prefixes[c]);
}
});
var $exponent = $group.find('select.exponent');
$exponent.on('change', setExponent);
// calculate initial exponent
var value = parseFloat($input.val());
if (!Number.isNaN(value)) {
$exponent.val(Math.floor(Math.log10(value) / 3) * 3);
setExponent();
}
})
};

View File

@ -0,0 +1,17 @@
$.fn.gainInput = function() {
this.each(function() {
var $container = $(this);
var update = function(value){
$container.find('.option').hide();
$container.find('.option.' + value).show();
}
var $select = $container.find('select');
$select.on('change', function(e) {
var value = $(e.target).val();
update(value);
});
update($select.val());
});
}

View File

@ -0,0 +1,87 @@
$.fn.imageUpload = function() {
$.each(this, function(){
var $this = $(this);
var $uploadButton = $this.find('button.upload');
var $restoreButton = $this.find('button.restore');
var $img = $this.find('img');
var originalUrl = $img.prop('src');
var $input = $this.find('input');
var id = $input.prop('id');
var maxSize = $this.data('max-size');
var $error;
var handleError = function(message) {
clearError();
$error = $('<div class="invalid-feedback">' + message + '</div>');
$this.after($error);
$this.addClass('is-invalid');
};
var clearError = function(message) {
if ($error) $error.remove();
$this.removeClass('is-invalid');
};
$uploadButton.click(function(){
var input = document.createElement('input');
input.type = 'file';
input.accept = 'image/jpeg, image/png, image/webp';
input.onchange = function(e) {
$uploadButton.prop('disabled', true);
var $spinner = $('<span class="spinner-border spinner-border-sm mr-1" role="status"></span>');
$uploadButton.prepend($spinner);
var reader = new FileReader()
reader.readAsArrayBuffer(e.target.files[0]);
reader.onprogress = function(e) {
if (e.loaded > maxSize) {
handleError('Maximum file size exceeded');
$uploadButton.prop('disabled', false);
$spinner.remove();
reader.abort();
}
};
reader.onload = function(e) {
if (e.loaded > maxSize) {
handleError('Maximum file size exceeded');
$uploadButton.prop('disabled', false);
$spinner.remove();
return;
}
$.ajax({
url: '../imageupload?id=' + id,
type: 'POST',
data: e.target.result,
processData: false,
contentType: 'application/octet-stream',
}).done(function(data){
$input.val(data.file);
$img.one('load', function() {
$uploadButton.prop('disabled', false);
$spinner.remove();
});
$img.prop('src', '../imageupload?file=' + data.file);
clearError();
}).fail(function(xhr, error){
try {
var res = JSON.parse(xhr.responseText);
handleError(res.error || error);
} catch (e) {
handleError(error);
}
$uploadButton.prop('disabled', false);
$spinner.remove();
});
}
};
input.click();
return false;
});
$restoreButton.click(function(){
$input.val('restore');
$img.prop('src', originalUrl + "&mapped=false");
clearError();
return false;
});
});
}

View File

@ -1,138 +0,0 @@
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,23 @@
$.fn.mapInput = function() {
this.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);
});
});
});
};

View File

@ -0,0 +1,29 @@
$.fn.optionalSection = function(){
this.each(function() {
var $section = $(this);
var $select = $section.find('.optional-select');
var $optionalInputs = $section.find('.optional-inputs');
$section.on('click', '.option-add-button', function(e){
var field = $select.val();
var group = $optionalInputs.find(".form-group[data-field='" + field + "']");
group.find('input, select').filter(function(){
// exclude template inputs
return !$(this).parents('.template').length;
}).prop('disabled', false);
$section.find('hr').before(group);
$select.find('option[value=\'' + field + '\']').remove();
return false;
});
$section.on('click', '.option-remove-button', function(e) {
var group = $(e.target).parents('.form-group')
group.find('input, select').prop('disabled', true);
$optionalInputs.append(group);
var $label = group.find('label');
var $option = $('<option value="' + group.data('field') + '">' + $label.text() + '</option>');
$select.append($option);
return false;
})
});
}

View File

@ -0,0 +1,33 @@
$.fn.schedulerInput = function() {
this.each(function() {
var $container = $(this);
var $template = $container.find('.template');
$template.find('input, select').prop('disabled', true);
var update = function(value){
$container.find('.option').hide();
$container.find('.option.' + value).show();
}
var $select = $container.find('select.mode');
$select.on('change', function(e) {
var value = $(e.target).val();
update(value);
});
update($select.val());
$container.find('.add-button').on('click', function() {
var row = $template.clone();
row.removeClass('template').show();
row.find('input, select').prop('disabled', false);
$template.before(row);
return false;
});
$container.on('click', '.remove-button', function(e) {
var row = $(e.target).parents('.scheduler-static-time-inputs');
row.remove();
});
});
}

View File

@ -1,252 +0,0 @@
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');
});
};

View File

@ -0,0 +1,11 @@
$.fn.waterfallDropdown = function(){
this.each(function(){
var $select = $(this);
var setVisibility = function() {
var show = $select.val() === 'CUSTOM';
$('#waterfall_colors').parents('.form-group')[show ? 'show' : 'hide']();
}
$select.on('change', setVisibility);
setVisibility();
})
}

View File

@ -0,0 +1,68 @@
$.fn.wsjtDecodingDepthsInput = function() {
function WsjtDecodingDepthRow(inputs, mode, value) {
this.el = $('<tr>');
this.modeInput = $(inputs.get(0)).clone();
this.modeInput.val(mode);
this.valueInput = $(inputs.get(1)).clone();
this.valueInput.val(value);
this.removeButton = $('<button type="button" class="btn btn-sm btn-danger remove">Remove</button>');
this.removeButton.data('row', this);
this.el.append([this.modeInput, this.valueInput, this.removeButton].map(function(i) {
return $('<td>').append(i);
}));
}
WsjtDecodingDepthRow.prototype.getEl = function() {
return this.el;
}
WsjtDecodingDepthRow.prototype.getValue = function() {
var value = parseInt(this.valueInput.val())
if (Number.isNaN(value)) {
return {};
}
return Object.fromEntries([[this.modeInput.val(), value]]);
}
this.each(function(){
var $input = $(this);
var $el = $input.parent();
var $inputs = $el.find('.inputs')
var inputs = $inputs.find('input, select');
$inputs.remove();
var rows = $.map(JSON.parse($input.val()), function(value, mode) {
return new WsjtDecodingDepthRow(inputs, mode, value);
});
var $table = $('<table class="table table-sm table-borderless wsjt-decoding-depths-table">');
$table.append(rows.map(function(r) {
return r.getEl();
}));
var updateValue = function(){
$input.val(JSON.stringify($.extend.apply({}, rows.map(function(r) {
return r.getValue();
}))));
};
$table.on('change', updateValue);
var $addButton = $('<button type="button" class="btn btn-sm btn-primary">Add...</button>');
$addButton.on('click', function() {
var row = new WsjtDecodingDepthRow(inputs)
rows.push(row);
$table.append(row.getEl());
return false;
});
$el.on('click', '.btn.remove', function(e){
var row = $(e.target).data('row');
var index = rows.indexOf(row);
if (index < 0) return false;
rows.splice(index, 1);
row.getEl().remove();
updateValue();
return false;
});
$input.after($table, $addButton);
});
};

View File

@ -11,17 +11,19 @@
</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 class="login-container">
<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>
</div>
</body>

View File

@ -2,6 +2,9 @@
<html>
<head>
<title>OpenWebRX Map</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#222" />
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<script src="compiled/map.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>

View File

@ -1,4 +1,4 @@
(function(){
$(function(){
var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){
var s = v.split('=');
var r = {};
@ -9,7 +9,7 @@
});
var expectedCallsign;
if (query.callsign) expectedCallsign = query.callsign;
if (query.callsign) expectedCallsign = decodeURIComponent(query.callsign);
var expectedLocator;
if (query.locator) expectedLocator = query.locator;
@ -30,6 +30,7 @@
var map;
var markers = {};
var rectangles = {};
var receiverMarker;
var updateQueue = [];
// reasonable default; will be overriden by server
@ -44,7 +45,12 @@
if (!colorKeys[id]) {
var keys = Object.keys(colorKeys);
keys.push(id);
keys.sort();
keys.sort(function(a, b) {
var pa = parseFloat(a);
var pb = parseFloat(b);
if (isNaN(pa) || isNaN(pb)) return a.localeCompare(b);
return pa - pb;
});
var colors = colorScale.colors(keys.length);
colorKeys = {};
keys.forEach(function(key, index) {
@ -81,6 +87,7 @@
$('#openwebrx-map-colormode').on('change', function(){
colorMode = $(this).val();
colorKeys = {};
filterRectangles(allRectangles);
reColor();
updateLegend();
});
@ -88,7 +95,10 @@
var updateLegend = function() {
var lis = $.map(colorKeys, function(value, key) {
return '<li class="square"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>';
// fake rectangle to test if the filter would match
var fakeRectangle = Object.fromEntries([[colorMode.slice(2), key]]);
var disabled = rectangleFilter(fakeRectangle) ? '' : ' disabled';
return '<li class="square' + disabled + '" data-selector="' + key + '"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>';
});
$(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>');
}
@ -131,14 +141,13 @@
marker.band = update.band;
marker.comment = update.location.comment;
// TODO the trim should happen on the server side
if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
if (expectedCallsign && expectedCallsign == update.callsign) {
map.panTo(pos);
showMarkerInfoWindow(update.callsign, pos);
expectedCallsign = false;
}
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign.trim()) {
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign) {
showMarkerInfoWindow(infowindow.callsign, pos);
}
break;
@ -159,11 +168,17 @@
});
rectangles[update.callsign] = rectangle;
}
rectangle.lastseen = update.lastseen;
rectangle.locator = update.location.locator;
rectangle.mode = update.mode;
rectangle.band = update.band;
rectangle.center = center;
rectangle.setOptions($.extend({
strokeColor: color,
strokeWeight: 2,
fillColor: color,
map: map,
map: rectangleFilter(rectangle) ? map : undefined,
bounds:{
north: lat,
south: lat + 1,
@ -171,11 +186,6 @@
east: lon + 2
}
}, getRectangleOpacityOptions(update.lastseen) ));
rectangle.lastseen = update.lastseen;
rectangle.locator = update.location.locator;
rectangle.mode = update.mode;
rectangle.band = update.band;
rectangle.center = center;
if (expectedLocator && expectedLocator == update.location.locator) {
map.panTo(center);
@ -195,12 +205,15 @@
var reset = function(callsign, item) { item.setMap(); };
$.each(markers, reset);
$.each(rectangles, reset);
receiverMarker.setMap();
markers = {};
rectangles = {};
};
var reconnect_timeout = false;
var config = {}
var connect = function(){
var ws = new WebSocket(ws_url);
ws.onopen = function(){
@ -214,40 +227,71 @@
return
}
if (e.data.substr(0, 16) == "CLIENT DE SERVER") {
console.log("Server acknowledged WebSocket connection.");
return
}
try {
var json = JSON.parse(e.data);
switch (json.type) {
case "config":
var config = json.value;
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
map = new google.maps.Map($('.openwebrx-map')[0], {
center: {
lat: config.receiver_gps.lat,
lng: config.receiver_gps.lon
},
zoom: 5,
});
Object.assign(config, json.value);
if ('receiver_gps' in config) {
var receiverPos = {
lat: config.receiver_gps.lat,
lng: config.receiver_gps.lon
};
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
map = new google.maps.Map($('.openwebrx-map')[0], {
center: receiverPos,
zoom: 5,
});
$.getScript("static/lib/nite-overlay.js").done(function(){
nite.init(map);
setInterval(function() { nite.refresh() }, 10000); // every 10s
$.getScript("static/lib/nite-overlay.js").done(function(){
nite.init(map);
setInterval(function() { nite.refresh() }, 10000); // every 10s
});
$.getScript('static/lib/AprsMarker.js').done(function(){
processUpdates(updateQueue);
updateQueue = [];
});
var $legend = $(".openwebrx-map-legend");
setupLegendFilters($legend);
map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($legend[0]);
if (!receiverMarker) {
receiverMarker = new google.maps.Marker();
receiverMarker.addListener('click', function() {
showReceiverInfoWindow(receiverMarker);
});
}
receiverMarker.setOptions({
map: map,
position: receiverPos,
title: config['receiver_name'],
config: config
});
}); else {
receiverMarker.setOptions({
map: map,
position: receiverPos,
config: config
});
}
}
if ('receiver_name' in config && receiverMarker) {
receiverMarker.setOptions({
title: config['receiver_name']
});
$.getScript('static/lib/AprsMarker.js').done(function(){
processUpdates(updateQueue);
updateQueue = [];
});
map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($(".openwebrx-map-legend")[0]);
});
retention_time = config.map_position_retention_time * 1000;
}
if ('map_position_retention_time' in config) {
retention_time = config.map_position_retention_time * 1000;
}
break;
case "update":
processUpdates(json.value);
break;
case 'receiver_details':
$('#webrx-top-container').header().setDetails(json['value']);
$('.webrx-top-container').header().setDetails(json['value']);
break;
default:
console.warn('received message of unknown type: ' + json['type']);
@ -291,6 +335,8 @@
delete infowindow.callsign;
});
}
delete infowindow.locator;
delete infowindow.callsign;
return infowindow;
}
@ -300,7 +346,7 @@
infowindow.locator = locator;
var inLocator = $.map(rectangles, function(r, callsign) {
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
}).filter(function(d) {
}).filter(rectangleFilter).filter(function(d) {
return d.locator == locator;
}).sort(function(a, b){
return b.lastseen - a.lastseen;
@ -339,6 +385,15 @@
infowindow.open(map, marker);
}
var showReceiverInfoWindow = function(marker) {
var infowindow = getInfoWindow()
infowindow.setContent(
'<h3>' + marker.config['receiver_name'] + '</h3>' +
'<div>Receiver location</div>'
);
infowindow.open(map, marker);
}
var getScale = function(lastseen) {
var age = new Date().getTime() - lastseen;
var scale = 1;
@ -386,4 +441,36 @@
});
}, 1000);
})();
var rectangleFilter = allRectangles = function() { return true; };
var filterRectangles = function(filter) {
rectangleFilter = filter;
$.each(rectangles, function(_, r) {
r.setMap(rectangleFilter(r) ? map : undefined);
});
};
var setupLegendFilters = function($legend) {
$content = $legend.find('.content');
$content.on('click', 'li', function() {
var $el = $(this);
$lis = $content.find('li');
if ($lis.hasClass('disabled') && !$el.hasClass('disabled')) {
$lis.removeClass('disabled');
filterRectangles(allRectangles);
} else {
$el.removeClass('disabled');
$lis.filter(function() {
return this != $el[0]
}).addClass('disabled');
var key = colorMode.slice(2);
var selector = $el.data('selector');
filterRectangles(function(r) {
return r[key] === selector;
});
}
});
}
});

View File

@ -3,7 +3,7 @@
This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@ -32,26 +32,20 @@ var fft_codec;
var waterfall_setup_done = 0;
var secondary_fft_size;
function e(what) {
return document.getElementById(what);
}
function updateVolume() {
audioEngine.setVolume(parseFloat(e("openwebrx-panel-volume").value) / 100);
audioEngine.setVolume(parseFloat($("#openwebrx-panel-volume").val()) / 100);
}
function toggleMute() {
if (mute) {
mute = false;
e("openwebrx-mute-on").id = "openwebrx-mute-off";
e("openwebrx-panel-volume").disabled = false;
e("openwebrx-panel-volume").value = volumeBeforeMute;
var $muteButton = $('.openwebrx-mute-button');
var $volumePanel = $('#openwebrx-panel-volume');
if ($muteButton.hasClass('muted')) {
$muteButton.removeClass('muted');
$volumePanel.prop('disabled', false).val(volumeBeforeMute);
} else {
mute = true;
e("openwebrx-mute-off").id = "openwebrx-mute-on";
e("openwebrx-panel-volume").disabled = true;
volumeBeforeMute = e("openwebrx-panel-volume").value;
e("openwebrx-panel-volume").value = 0;
$muteButton.addClass('muted');
volumeBeforeMute = $volumePanel.val();
$volumePanel.prop('disabled', true).val(0);
}
updateVolume();
@ -78,7 +72,8 @@ var waterfall_max_level;
var waterfall_min_level_default;
var waterfall_max_level_default;
var waterfall_colors = buildWaterfallColors(['#000', '#FFF']);
var waterfall_auto_level_margin;
var waterfall_auto_levels;
var waterfall_auto_min_range;
function buildWaterfallColors(input) {
return chroma.scale(input).colors(256, 'rgb')
@ -116,9 +111,9 @@ function waterfallColorsDefault() {
}
function waterfallColorsAuto(levels) {
var min_level = levels.min - waterfall_auto_level_margin.min;
var max_level = levels.max + waterfall_auto_level_margin.max;
max_level = Math.max(min_level + (waterfall_auto_level_margin.min_range || 0), max_level);
var min_level = levels.min - waterfall_auto_levels.min;
var max_level = levels.max + waterfall_auto_levels.max;
max_level = Math.max(min_level + (waterfall_auto_min_range || 0), max_level);
waterfall_min_level = min_level;
waterfall_max_level = max_level;
updateWaterfallSliders();
@ -151,13 +146,19 @@ function waterfallColorsContinuous(levels) {
function setSmeterRelativeValue(value) {
if (value < 0) value = 0;
if (value > 1.0) value = 1.0;
var bar = e("openwebrx-smeter-bar");
var outer = e("openwebrx-smeter-outer");
bar.style.width = (outer.offsetWidth * value).toString() + "px";
var bgRed = "linear-gradient(to top, #ff5939 , #961700)";
var bgGreen = "linear-gradient(to top, #22ff2f , #008908)";
var bgYellow = "linear-gradient(to top, #fff720 , #a49f00)";
bar.style.background = (value > 0.9) ? bgRed : ((value > 0.7) ? bgYellow : bgGreen);
var $meter = $("#openwebrx-smeter");
var $bar = $meter.find(".openwebrx-smeter-bar");
$bar.css({transform: 'translate(' + ((value - 1) * 100) + '%) translateZ(0)'});
if (value > 0.9) {
// red
$bar.css({background: 'linear-gradient(to top, #ff5939 , #961700)'});
} else if (value > 0.7) {
// yellow
$bar.css({background: 'linear-gradient(to top, #fff720 , #a49f00)'});
} else {
// red
$bar.css({background: 'linear-gradient(to top, #22ff2f , #008908)'});
}
}
function setSquelchSliderBackground(val) {
@ -185,7 +186,7 @@ function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch
var highLevel = waterfall_max_level + 20;
var percent = (logValue - lowLevel) / (highLevel - lowLevel);
setSmeterRelativeValue(percent);
e("openwebrx-smeter-db").innerHTML = logValue.toFixed(1) + " dB";
$("#openwebrx-smeter-db").html(logValue.toFixed(1) + " dB");
}
function typeInAnimation(element, timeout, what, onFinish) {
@ -238,14 +239,14 @@ var scale_ctx;
var scale_canvas;
function scale_setup() {
scale_canvas = e("openwebrx-scale-canvas");
scale_canvas = $("#openwebrx-scale-canvas")[0];
scale_ctx = scale_canvas.getContext("2d");
scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false);
scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false);
scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false);
resize_scale();
var frequency_container = e("openwebrx-frequency-container");
frequency_container.addEventListener("mousemove", frequency_container_mousemove, false);
var frequency_container = $("#openwebrx-frequency-container");
frequency_container.on("mousemove", frequency_container_mousemove, false);
}
var scale_canvas_drag_params = {
@ -292,7 +293,7 @@ function scale_canvas_mousemove(evt) {
function frequency_container_mousemove(evt) {
var frequency = center_freq + scale_offset_freq_from_px(evt.pageX);
$('.webrx-mouse-freq').frequencyDisplay().setFrequency(frequency);
$('#openwebrx-panel-receiver').demodulatorPanel().setMouseFrequency(frequency);
}
function scale_canvas_end_drag(x) {
@ -314,14 +315,16 @@ function scale_px_from_freq(f, range) {
}
function get_visible_freq_range() {
var out = {};
if (!bandwidth) return false;
var fcalc = function (x) {
var canvasWidth = waterfallWidth() * zoom_levels[zoom_level];
return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2);
};
out.start = fcalc(0);
out.center = fcalc(waterfallWidth() / 2);
out.end = fcalc(waterfallWidth());
var out = {
start: fcalc(0),
center: fcalc(waterfallWidth() / 2),
end: fcalc(waterfallWidth()),
}
out.bw = out.end - out.start;
out.hps = out.bw / waterfallWidth();
return out;
@ -426,6 +429,7 @@ var range;
function mkscale() {
//clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes):
range = get_visible_freq_range();
if (!range) return;
mkenvelopes(range); //when scale changes we will always have to redraw filter envelopes, too
scale_ctx.clearRect(0, 22, scale_ctx.canvas.width, scale_ctx.canvas.height - 22);
scale_ctx.strokeStyle = "#fff";
@ -442,9 +446,7 @@ function mkscale() {
};
var last_large;
var x;
for (; ;) {
x = scale_px_from_freq(marker_hz, range);
if (x > window.innerWidth) break;
while ((x = scale_px_from_freq(marker_hz, range)) <= window.innerWidth) {
scale_ctx.beginPath();
scale_ctx.moveTo(x, 22);
if (marker_hz % spacing.params.large_marker_per_hz === 0) { //large marker
@ -510,7 +512,7 @@ function resize_scale() {
}
function canvas_get_freq_offset(relativeX) {
var rel = (relativeX / canvases[0].clientWidth);
var rel = (relativeX / canvas_container.clientWidth);
return Math.round((bandwidth * rel) - (bandwidth / 2));
}
@ -570,7 +572,7 @@ function canvas_mousemove(evt) {
bookmarks.position();
}
} else {
$('.webrx-mouse-freq').frequencyDisplay().setFrequency(canvas_get_frequency(relativeX));
$('#openwebrx-panel-receiver').demodulatorPanel().setMouseFrequency(canvas_get_frequency(relativeX));
}
}
@ -683,7 +685,11 @@ function zoom_calc() {
}
var networkSpeedMeasurement;
var currentprofile;
var currentprofile = {
toString: function() {
return this['sdr_id'] + '|' + this['profile_id'];
}
};
var COMPRESS_FFT_PAD_N = 10; //should be the same as in csdr.c
@ -693,56 +699,96 @@ function on_ws_recv(evt) {
networkSpeedMeasurement.add(evt.data.length);
if (evt.data.substr(0, 16) === "CLIENT DE SERVER") {
divlog("Server acknowledged WebSocket connection.");
params = Object.fromEntries(
evt.data.slice(17).split(' ').map(function(param) {
var args = param.split('=');
return [args[0], args.slice(1).join('=')]
})
);
var versionInfo = 'Unknown server';
if (params.server && params.server === 'openwebrx' && params.version) {
versionInfo = 'OpenWebRX version: ' + params.version;
}
divlog('Server acknowledged WebSocket connection, ' + versionInfo);
} else {
try {
var json = JSON.parse(evt.data);
switch (json.type) {
case "config":
var config = json['value'];
waterfall_colors = buildWaterfallColors(config['waterfall_colors']);
waterfall_min_level_default = config['waterfall_min_level'];
waterfall_max_level_default = config['waterfall_max_level'];
waterfall_auto_level_margin = config['waterfall_auto_level_margin'];
if ('waterfall_colors' in config)
waterfall_colors = buildWaterfallColors(config['waterfall_colors']);
if ('waterfall_levels' in config) {
waterfall_min_level_default = config['waterfall_levels']['min'];
waterfall_max_level_default = config['waterfall_levels']['max'];
}
if ('waterfall_auto_levels' in config)
waterfall_auto_levels = config['waterfall_auto_levels'];
if ('waterfall_auto_min_range' in config)
waterfall_auto_min_range = config['waterfall_auto_min_range'];
waterfallColorsDefault();
var initial_demodulator_params = {
mod: config['start_mod'],
offset_frequency: config['start_offset_freq'],
squelch_level: Number.isInteger(config['initial_squelch_level']) ? config['initial_squelch_level'] : -150
};
var initial_demodulator_params = {};
if ('start_mod' in config)
initial_demodulator_params['mod'] = config['start_mod'];
if ('start_offset_freq' in config)
initial_demodulator_params['offset_frequency'] = config['start_offset_freq'];
if ('initial_squelch_level' in config)
initial_demodulator_params['squelch_level'] = Number.isInteger(config['initial_squelch_level']) ? config['initial_squelch_level'] : -150;
bandwidth = config['samp_rate'];
center_freq = config['center_freq'];
fft_size = config['fft_size'];
var audio_compression = config['audio_compression'];
audioEngine.setCompression(audio_compression);
divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
fft_compression = config['fft_compression'];
divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
$('#openwebrx-bar-clients').progressbar().setMaxClients(config['max_clients']);
if ('samp_rate' in config)
bandwidth = config['samp_rate'];
if ('center_freq' in config)
center_freq = config['center_freq'];
if ('fft_size' in config) {
fft_size = config['fft_size'];
waterfall_clear();
}
if ('audio_compression' in config) {
var audio_compression = config['audio_compression'];
audioEngine.setCompression(audio_compression);
divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
}
if ('fft_compression' in config) {
fft_compression = config['fft_compression'];
divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
}
if ('max_clients' in config)
$('#openwebrx-bar-clients').progressbar().setMaxClients(config['max_clients']);
waterfall_init();
var demodulatorPanel = $('#openwebrx-panel-receiver').demodulatorPanel();
demodulatorPanel.setCenterFrequency(center_freq);
demodulatorPanel.setInitialParams(initial_demodulator_params);
if ('squelch_auto_margin' in config)
demodulatorPanel.setSquelchMargin(config['squelch_auto_margin']);
bookmarks.loadLocalBookmarks();
waterfall_clear();
if ('sdr_id' in config || 'profile_id' in config) {
currentprofile['sdr_id'] = config['sdr_id'] || currentprofile['sdr_id'];
currentprofile['profile_id'] = config['profile_id'] || currentprofile['profile_id'];
$('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString());
currentprofile = config['sdr_id'] + '|' + config['profile_id'];
$('#openwebrx-sdr-profiles-listbox').val(currentprofile);
waterfall_clear();
}
if ('tuning_precision' in config)
$('#openwebrx-panel-receiver').demodulatorPanel().setTuningPrecision(config['tuning_precision']);
break;
case "secondary_config":
var s = json['value'];
window.secondary_fft_size = s['secondary_fft_size'];
window.secondary_bw = s['secondary_bw'];
window.if_samp_rate = s['if_samp_rate'];
if ('secondary_fft_size' in s)
window.secondary_fft_size = s['secondary_fft_size'];
if ('secondary_bw' in s)
window.secondary_bw = s['secondary_bw'];
if ('if_samp_rate' in s)
window.if_samp_rate = s['if_samp_rate'];
secondary_demod_init_canvases();
break;
case "receiver_details":
$('#webrx-top-container').header().setDetails(json['value']);
$('.webrx-top-container').header().setDetails(json['value']);
break;
case "smeter":
smeter_level = json['value'];
@ -755,25 +801,31 @@ function on_ws_recv(evt) {
$('#openwebrx-bar-clients').progressbar().setClients(json['value']);
break;
case "profiles":
var listbox = e("openwebrx-sdr-profiles-listbox");
listbox.innerHTML = json['value'].map(function (profile) {
var listbox = $("#openwebrx-sdr-profiles-listbox");
listbox.html(json['value'].map(function (profile) {
return '<option value="' + profile['id'] + '">' + profile['name'] + "</option>";
}).join("");
if (currentprofile) {
$('#openwebrx-sdr-profiles-listbox').val(currentprofile);
}).join(""));
$('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString());
// this is a bit hacky since it only makes sense if the error is actually "no sdr devices"
// the only other error condition for which the overlay is used right now is "too many users"
// so there shouldn't be a problem here
if (Object.keys(json['value']).length) {
$('#openwebrx-error-overlay').hide();
}
break;
case "features":
Modes.setFeatures(json['value']);
break;
case "metadata":
update_metadata(json['value']);
$('.openwebrx-meta-panel').metaPanel().each(function(){
this.update(json['value']);
});
break;
case "js8_message":
$("#openwebrx-panel-js8-message").js8().pushMessage(json['value']);
break;
case "wsjt_message":
update_wsjt_panel(json['value']);
$("#openwebrx-panel-wsjt-message").wsjtMessagePanel().pushMessage(json['value']);
break;
case "dial_frequencies":
var as_bookmarks = json['value'].map(function (d) {
@ -786,7 +838,7 @@ function on_ws_recv(evt) {
bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies');
break;
case "aprs_data":
update_packet_panel(json['value']);
$('#openwebrx-panel-packet-message').packetMessagePanel().pushMessage(json['value']);
break;
case "bookmarks":
bookmarks.replace_bookmarks(json['value'], "server");
@ -796,6 +848,7 @@ function on_ws_recv(evt) {
var $overlay = $('#openwebrx-error-overlay');
$overlay.find('.errormessage').text(json['value']);
$overlay.show();
$("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator();
break;
case 'secondary_demod':
secondary_demod_push_data(json['value']);
@ -804,7 +857,7 @@ function on_ws_recv(evt) {
divlog(json['value'], true);
break;
case 'pocsag_data':
update_pocsag_panel(json['value']);
$('#openwebrx-panel-pocsag-message').pocsagMessagePanel().pushMessage(json['value']);
break;
case 'backoff':
divlog("Server is currently busy: " + json['reason'], true);
@ -877,210 +930,6 @@ function on_ws_recv(evt) {
}
}
function update_metadata(meta) {
var el;
if (meta['protocol']) switch (meta['protocol']) {
case 'DMR':
if (meta['slot']) {
el = $("#openwebrx-panel-metadata-dmr").find(".openwebrx-dmr-timeslot-panel").get(meta['slot']);
var id = "";
var name = "";
var target = "";
var group = false;
$(el)[meta['sync'] ? "addClass" : "removeClass"]("sync");
if (meta['sync'] && meta['sync'] === "voice") {
id = (meta['additional'] && meta['additional']['callsign']) || meta['source'] || "";
name = (meta['additional'] && meta['additional']['fname']) || "";
if (meta['type'] === "group") {
target = "Talkgroup: ";
group = true;
}
if (meta['type'] === "direct") target = "Direct: ";
target += meta['target'] || "";
$(el).addClass("active");
} else {
$(el).removeClass("active");
}
$(el).find(".openwebrx-dmr-id").text(id);
$(el).find(".openwebrx-dmr-name").text(name);
$(el).find(".openwebrx-dmr-target").text(target);
$(el).find(".openwebrx-meta-user-image")[group ? "addClass" : "removeClass"]("group");
} else {
clear_metadata();
}
break;
case 'YSF':
el = $("#openwebrx-panel-metadata-ysf");
var mode = " ";
var source = "";
var up = "";
var down = "";
if (meta['mode'] && meta['mode'] !== "") {
mode = "Mode: " + meta['mode'];
source = meta['source'] || "";
if (meta['lat'] && meta['lon'] && meta['source']) {
source = "<a class=\"openwebrx-maps-pin\" href=\"map?callsign=" + meta['source'] + "\" target=\"_blank\"></a>" + source;
}
up = meta['up'] ? "Up: " + meta['up'] : "";
down = meta['down'] ? "Down: " + meta['down'] : "";
$(el).find(".openwebrx-meta-slot").addClass("active");
} else {
$(el).find(".openwebrx-meta-slot").removeClass("active");
}
$(el).find(".openwebrx-ysf-mode").text(mode);
$(el).find(".openwebrx-ysf-source").html(source);
$(el).find(".openwebrx-ysf-up").text(up);
$(el).find(".openwebrx-ysf-down").text(down);
break;
} else {
clear_metadata();
}
}
function html_escape(input) {
return $('<div/>').text(input).html()
}
function update_wsjt_panel(msg) {
var $b = $('#openwebrx-panel-wsjt-message').find('tbody');
var t = new Date(msg['timestamp']);
var pad = function (i) {
return ('' + i).padStart(2, "0");
};
var linkedmsg = msg['msg'];
var matches;
if (['FT8', 'JT65', 'JT9', 'FT4'].indexOf(msg['mode']) >= 0) {
matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/);
if (matches && matches[2] !== 'RR73') {
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
} else {
linkedmsg = html_escape(linkedmsg);
}
} else if (msg['mode'] === 'WSPR') {
matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/);
if (matches) {
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + html_escape(matches[3]);
} else {
linkedmsg = html_escape(linkedmsg);
}
}
$b.append($(
'<tr data-timestamp="' + msg['timestamp'] + '">' +
'<td>' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '</td>' +
'<td class="decimal">' + msg['db'] + '</td>' +
'<td class="decimal">' + msg['dt'] + '</td>' +
'<td class="decimal freq">' + msg['freq'] + '</td>' +
'<td class="message">' + linkedmsg + '</td>' +
'</tr>'
));
$b.scrollTop($b[0].scrollHeight);
}
var digital_removal_interval;
// remove old wsjt messages in fixed intervals
function init_digital_removal_timer() {
if (digital_removal_interval) clearInterval(digital_removal_interval);
digital_removal_interval = setInterval(function () {
['#openwebrx-panel-wsjt-message', '#openwebrx-panel-packet-message'].forEach(function (root) {
var $elements = $(root + ' tbody tr');
// limit to 1000 entries in the list since browsers get laggy at some point
var toRemove = $elements.length - 1000;
if (toRemove <= 0) return;
$elements.slice(0, toRemove).remove();
});
}, 15000);
}
function update_packet_panel(msg) {
var $b = $('#openwebrx-panel-packet-message').find('tbody');
var pad = function (i) {
return ('' + i).padStart(2, "0");
};
if (msg.type && msg.type === 'thirdparty' && msg.data) {
msg = msg.data;
}
var source = msg.source;
if (msg.type) {
if (msg.type === 'item') {
source = msg.item;
}
if (msg.type === 'object') {
source = msg.object;
}
}
var timestamp = '';
if (msg.timestamp) {
var t = new Date(msg.timestamp);
timestamp = pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds())
}
var link = '';
var classes = [];
var styles = {};
var overlay = '';
var stylesToString = function (s) {
return $.map(s, function (value, key) {
return key + ':' + value + ';'
}).join('')
};
if (msg.symbol) {
classes.push('aprs-symbol');
classes.push('aprs-symboltable-' + (msg.symbol.table === '/' ? 'normal' : 'alternate'));
styles['background-position-x'] = -(msg.symbol.index % 16) * 15 + 'px';
styles['background-position-y'] = -Math.floor(msg.symbol.index / 16) * 15 + 'px';
if (msg.symbol.table !== '/' && msg.symbol.table !== '\\') {
var s = {};
s['background-position-x'] = -(msg.symbol.tableindex % 16) * 15 + 'px';
s['background-position-y'] = -Math.floor(msg.symbol.tableindex / 16) * 15 + 'px';
overlay = '<div class="aprs-symbol aprs-symboltable-overlay" style="' + stylesToString(s) + '"></div>';
}
} else if (msg.lat && msg.lon) {
classes.push('openwebrx-maps-pin');
}
var attrs = [
'class="' + classes.join(' ') + '"',
'style="' + stylesToString(styles) + '"'
].join(' ');
if (msg.lat && msg.lon) {
link = '<a ' + attrs + ' href="map?callsign=' + source + '" target="openwebrx-map">' + overlay + '</a>';
} else {
link = '<div ' + attrs + '>' + overlay + '</div>'
}
$b.append($(
'<tr>' +
'<td>' + timestamp + '</td>' +
'<td class="callsign">' + source + '</td>' +
'<td class="coord">' + link + '</td>' +
'<td class="message">' + (msg.comment || msg.message || '') + '</td>' +
'</tr>'
));
$b.scrollTop($b[0].scrollHeight);
}
function update_pocsag_panel(msg) {
var $b = $('#openwebrx-panel-pocsag-message').find('tbody');
$b.append($(
'<tr>' +
'<td class="address">' + msg.address + '</td>' +
'<td class="message">' + msg.message + '</td>' +
'</tr>'
));
$b.scrollTop($b[0].scrollHeight);
}
function clear_metadata() {
$(".openwebrx-meta-panel .openwebrx-meta-autoclear").text("");
$(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
$(".openwebrx-dmr-timeslot-panel").removeClass("muted");
}
var waterfall_measure_minmax_now = false;
var waterfall_measure_minmax_continuous = false;
@ -1125,7 +974,7 @@ function divlog(what, is_error) {
what = "<span class=\"webrx-error\">" + what + "</span>";
toggle_panel("openwebrx-panel-log", true); //show panel if any error is present
}
e("openwebrx-debugdiv").innerHTML += what + "<br />";
$('#openwebrx-debugdiv')[0].innerHTML += what + "<br />";
var nano = $('.nano');
nano.nanoScroller();
nano.nanoScroller({scroll: 'bottom'});
@ -1157,7 +1006,9 @@ function onAudioStart(apiType){
var reconnect_timeout = false;
function on_ws_closed() {
$("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator();
var demodulatorPanel = $("#openwebrx-panel-receiver").demodulatorPanel();
demodulatorPanel.stopDemodulator();
demodulatorPanel.resetInitialParams();
if (reconnect_timeout) {
// max value: roundabout 8 and a half minutes
reconnect_timeout = Math.min(reconnect_timeout * 2, 512000);
@ -1230,15 +1081,15 @@ var canvas_context;
var canvases = [];
var canvas_default_height = 200;
var canvas_container;
var canvas_actual_line;
var canvas_actual_line = -1;
function add_canvas() {
var new_canvas = document.createElement("canvas");
new_canvas.width = fft_size;
new_canvas.height = canvas_default_height;
canvas_actual_line = canvas_default_height - 1;
new_canvas.openwebrx_top = (-canvas_default_height + 1);
new_canvas.style.top = new_canvas.openwebrx_top.toString() + "px";
canvas_actual_line = canvas_default_height;
new_canvas.openwebrx_top = -canvas_default_height;
new_canvas.style.transform = 'translate(0, ' + new_canvas.openwebrx_top.toString() + 'px)';
canvas_context = new_canvas.getContext("2d");
canvas_container.appendChild(new_canvas);
canvases.push(new_canvas);
@ -1251,22 +1102,21 @@ function add_canvas() {
function init_canvas_container() {
canvas_container = e("webrx-canvas-container");
canvas_container = $("#webrx-canvas-container")[0];
canvas_container.addEventListener("mouseleave", canvas_container_mouseleave, false);
canvas_container.addEventListener("mousemove", canvas_mousemove, false);
canvas_container.addEventListener("mouseup", canvas_mouseup, false);
canvas_container.addEventListener("mousedown", canvas_mousedown, false);
canvas_container.addEventListener("wheel", canvas_mousewheel, false);
var frequency_container = e("openwebrx-frequency-container");
frequency_container.addEventListener("wheel", canvas_mousewheel, false);
add_canvas();
var frequency_container = $("#openwebrx-frequency-container");
frequency_container.on("wheel", canvas_mousewheel, false);
}
canvas_maxshift = 0;
function shift_canvases() {
canvases.forEach(function (p) {
p.style.top = (p.openwebrx_top++).toString() + "px";
p.style.transform = 'translate(0, ' + (p.openwebrx_top++).toString() + 'px)';
});
canvas_maxshift++;
}
@ -1305,6 +1155,9 @@ function waterfall_add(data) {
waterfallColorsContinuous(level);
}
// create new canvas if the current one is full (or there isn't one)
if (canvas_actual_line <= 0) add_canvas();
//Add line to waterfall image
var oneline_image = canvas_context.createImageData(w, 1);
for (var x = 0; x < w; x++) {
@ -1314,18 +1167,17 @@ function waterfall_add(data) {
}
//Draw image
canvas_context.putImageData(oneline_image, 0, canvas_actual_line--);
canvas_context.putImageData(oneline_image, 0, --canvas_actual_line);
shift_canvases();
if (canvas_actual_line < 0) add_canvas();
}
function waterfall_clear() {
while (canvases.length) //delete all canvases
{
//delete all canvases
while (canvases.length) {
var x = canvases.shift();
x.parentNode.removeChild(x);
}
add_canvas();
canvas_actual_line = -1;
}
function openwebrx_resize() {
@ -1361,12 +1213,20 @@ var audioEngine;
function openwebrx_init() {
audioEngine = new AudioEngine(audio_buffer_maximal_length_sec, audioReporter);
$overlay = $('#openwebrx-autoplay-overlay');
$overlay.on('click', function(){
$('body').on('click', '#openwebrx-autoplay-overlay', function(){
audioEngine.resume();
});
audioEngine.onStart(onAudioStart);
if (!audioEngine.isAllowed()) {
var $overlay = $(
'<div id="openwebrx-autoplay-overlay" class="openwebrx-overlay" style="display:none;">' +
'<div class="overlay-content">' +
'<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.svg" />' +
'<div>Start OpenWebRX</div>' +
'</div>' +
'</div>'
);
$('body').append($overlay);
$overlay.show();
}
fft_codec = new ImaAdpcmCodec();
@ -1375,7 +1235,6 @@ function openwebrx_init() {
secondary_demod_init();
digimodes_init();
initPanels();
$('.webrx-mouse-freq').frequencyDisplay();
$('#openwebrx-panel-receiver').demodulatorPanel();
window.addEventListener("resize", openwebrx_resize);
bookmarks = new BookmarkBar();
@ -1414,6 +1273,8 @@ function digimodes_init() {
$(e.currentTarget).toggleClass("muted");
update_dmr_timeslot_filtering();
});
$('.openwebrx-meta-panel').metaPanel();
}
function update_dmr_timeslot_filtering() {
@ -1444,7 +1305,7 @@ var rt = function (s, n) {
// ========================================================
function panel_displayed(el){
return !(el.style && el.style.display && el.style.display === 'none')
return !(el.style && el.style.display && el.style.display === 'none') && !(el.movement && el.movement === 'collapse');
}
function toggle_panel(what, on) {
@ -1454,14 +1315,13 @@ function toggle_panel(what, on) {
if (typeof on !== "undefined" && displayed === on) {
return;
}
if (item.openwebrxDisableClick) return;
if (displayed) {
item.movement = 'collapse';
item.style.transform = "perspective(600px) rotateX(90deg)";
item.style.transitionProperty = 'transform';
} else {
item.movement = 'expand';
item.style.display = 'block';
item.style.display = null;
setTimeout(function(){
item.style.transitionProperty = 'transform';
item.style.transform = 'perspective(600px) rotateX(0deg)';
@ -1469,9 +1329,6 @@ function toggle_panel(what, on) {
}
item.style.transitionDuration = "600ms";
item.style.transitionDelay = "0ms";
item.openwebrxDisableClick = true;
}
function first_show_panel(panel) {
@ -1500,13 +1357,13 @@ function initPanels() {
el.openwebrxPanelTransparent = (!!el.dataset.panelTransparent);
el.addEventListener('transitionend', function(ev){
if (ev.target !== el) return;
el.openwebrxDisableClick = false;
el.style.transitionDuration = null;
el.style.transitionDelay = null;
el.style.transitionProperty = null;
if (el.movement && el.movement === 'collapse') {
el.style.display = 'none';
}
delete el.movement;
});
if (panel_displayed(el)) first_show_panel(el);
});
@ -1569,7 +1426,9 @@ function secondary_demod_init_canvases() {
}
function secondary_demod_canvases_update_top() {
for (var i = 0; i < 2; i++) secondary_demod_canvases[i].style.top = secondary_demod_canvases[i].openwebrx_top + "px";
for (var i = 0; i < 2; i++) {
secondary_demod_canvases[i].style.transform = 'translate(0, ' + secondary_demod_canvases[i].openwebrx_top + 'px)';
}
}
function secondary_demod_swap_canvases() {
@ -1587,7 +1446,10 @@ function secondary_demod_init() {
.mousedown(secondary_demod_canvas_container_mousedown)
.mouseenter(secondary_demod_canvas_container_mousein)
.mouseleave(secondary_demod_canvas_container_mouseleave);
init_digital_removal_timer();
$('#openwebrx-panel-wsjt-message').wsjtMessagePanel();
$('#openwebrx-panel-packet-message').packetMessagePanel();
$('#openwebrx-panel-pocsag-message').pocsagMessagePanel();
$('#openwebrx-panel-js8-message').js8();
}
function secondary_demod_push_data(x) {

32
htdocs/pwchange.html Normal file
View File

@ -0,0 +1,32 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Password change</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-container">
<div class="login">
<div class="alert alert-primary">
Your password has been automatically generated and must be changed in order to proceed.
</div>
<form method="POST">
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
</div>
<div class="form-group">
<label for="confirm">Password confirmation</label>
<input type="password" class="form-control" id="confirm" name="confirm" placeholder="Password confirmation">
</div>
<button type="submit" class="btn btn-secondary btn-login">Change password</button>
</form>
</div>
</div>
</body>

View File

@ -1,21 +0,0 @@
<!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>

View File

@ -11,17 +11,31 @@
<body>
${header}
<div class="container">
<div class="col-12">
<h1>Settings</h1>
<div class="row">
<h1 class="col-12">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 class="row settings-grid">
<div class="col-4">
<a class="btn btn-secondary" href="settings/general">General settings</a>
</div>
<div class="col-4">
<a class="btn btn-secondary" href="settings/sdr">SDR devices and profiles</a>
</div>
<div class="col-4">
<a class="btn btn-secondary" href="settings/bookmarks">Bookmark editor</a>
</div>
<div class="col-4">
<a class="btn btn-secondary" href="settings/decoding">Demodulation and decoding</a>
</div>
<div class="col-4">
<a class="btn btn-secondary" href="settings/backgrounddecoding">Background decoding</a>
</div>
<div class="col-4">
<a class="btn btn-secondary" href="settings/reporting">Spotting and reporting</a>
</div>
<div class="col-4">
<a class="btn btn-secondary" href="features">Feature report</a>
</div>
</div>
</div>
</body>

View File

@ -1,25 +1,11 @@
$(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();
$('.map-input').mapInput();
$('.imageupload').imageUpload();
$('.bookmarks').bookmarktable();
$('.wsjt-decoding-depths').wsjtDecodingDepthsInput();
$('#waterfall_scheme').waterfallDropdown();
$('#rf_gain').gainInput();
$('.optional-section').optionalSection();
$('#scheduler').schedulerInput();
$('.exponential-input').exponentialInput();
});

View File

@ -0,0 +1,69 @@
<!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">
${breadcrumb}
<div class="row">
<h1 class="col-12">Bookmarks</h1>
</div>
<div class="row">
<div class="col-12">Double-click the values in the table to edit them.</div>
</div>
<div class="row mt-3 bookmarks">
${bookmarks}
<div class="buttons container">
<button type="button" class="btn btn-info bookmark-import">Import personal bookmarks...</button>
<button type="button" class="btn btn-primary bookmark-add">Add a new bookmark</button>
</div>
</div>
${breadcrumb}
</div>
<div class="modal" id="deleteModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5>Please confirm</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Do you really want to delete this bookmark?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger confirm">Delete</button>
</div>
</div>
</div>
</div>
<div class="modal" id="importModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5>Import from personal bookmarks</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Please select the bookmarks you would like to import:</p>
<div class="bookmark-list"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary confirm">Import</button>
</div>
</div>
</div>
</div>
</body>

View File

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

10
openwebrx.conf Normal file
View File

@ -0,0 +1,10 @@
[core]
data_directory = /var/lib/openwebrx
temporary_directory = /tmp
[web]
port = 8073
[aprs]
# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols)
symbols_path = /usr/share/aprs-symbols/png

View File

@ -1,25 +1,71 @@
import logging
# the linter will complain about this, but the logging must be configured before importing all the other modules
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.core import CoreConfig
from owrx.config import Config
from owrx.config.commands import MigrateCommand
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.reporting import ReportingEngine
from owrx.version import openwebrx_version
from owrx.audio.queue import DecoderQueue
from owrx.admin import add_admin_parser, run_admin_action
import signal
import argparse
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
pass
class SignalException(Exception):
pass
def handleSignal(sig, frame):
raise SignalException("Received Signal {sig}".format(sig=sig))
def main():
parser = argparse.ArgumentParser(description="OpenWebRX - Open Source SDR Web App for Everyone!")
parser.add_argument("-v", "--version", action="store_true", help="Show the software version")
parser.add_argument("--debug", action="store_true", help="Set loglevel to DEBUG")
moduleparser = parser.add_subparsers(title="Modules", dest="module")
adminparser = moduleparser.add_parser("admin", help="Administration actions")
add_admin_parser(adminparser)
configparser = moduleparser.add_parser("config", help="Configuration actions")
configcommandparser = configparser.add_subparsers(title="Commands", dest="command")
migrateparser = configcommandparser.add_parser("migrate", help="Migrate configuration files")
migrateparser.set_defaults(cls=MigrateCommand)
args = parser.parse_args()
# set loglevel to info for CLI commands
if args.module is not None and not args.debug:
logging.getLogger().setLevel(logging.INFO)
if args.version:
print("OpenWebRX version {version}".format(version=openwebrx_version))
elif args.module == "admin":
run_admin_action(adminparser, args)
elif args.module == "config":
run_admin_action(configparser, args)
else:
start_receiver()
def start_receiver():
print(
"""
@ -35,16 +81,12 @@ Support and info: https://groups.io/g/openwebrx
logger.info("OpenWebRX version {0} starting up...".format(openwebrx_version))
pm = Config.get()
for sig in [signal.SIGINT, signal.SIGTERM]:
signal.signal(sig, handleSignal)
configErrors = Config.validateConfig()
if configErrors:
logger.error(
"your configuration contains errors. please address the following errors:"
)
for e in configErrors:
logger.error(e)
return
# config warmup
Config.validateConfig()
coreConfig = CoreConfig()
featureDetector = FeatureDetector()
if not featureDetector.is_available("core"):
@ -56,14 +98,18 @@ Support and info: https://groups.io/g/openwebrx
return
# Get error messages about unknown / unavailable features as soon as possible
SdrService.loadProps()
# start up "always-on" sources right away
SdrService.getAllSources()
Services.start()
try:
server = ThreadedHttpServer(("0.0.0.0", pm["web_port"]), RequestHandler)
server = ThreadedHttpServer(("0.0.0.0", coreConfig.get_web_port()), RequestHandler)
server.serve_forever()
except KeyboardInterrupt:
WebSocketConnection.closeAll()
Services.stop()
PskReporter.stop()
except SignalException:
pass
WebSocketConnection.closeAll()
Services.stop()
ReportingEngine.stopAll()
DecoderQueue.stopAll()

60
owrx/admin/__init__.py Normal file
View File

@ -0,0 +1,60 @@
from owrx.admin.commands import NewUser, DeleteUser, ResetPassword, ListUsers, DisableUser, EnableUser, HasUser
import sys
import traceback
def add_admin_parser(moduleparser):
subparsers = moduleparser.add_subparsers(title="Commands", dest="command")
adduser_parser = subparsers.add_parser("adduser", help="Add a new user")
adduser_parser.add_argument("user", help="Username to be added")
adduser_parser.set_defaults(cls=NewUser)
removeuser_parser = subparsers.add_parser("removeuser", help="Remove an existing user")
removeuser_parser.add_argument("user", help="Username to be remvoed")
removeuser_parser.set_defaults(cls=DeleteUser)
resetpassword_parser = subparsers.add_parser("resetpassword", help="Reset a user's password")
resetpassword_parser.add_argument("user", help="Username to be remvoed")
resetpassword_parser.set_defaults(cls=ResetPassword)
listusers_parser = subparsers.add_parser("listusers", help="List enabled users")
listusers_parser.add_argument("-a", "--all", action="store_true", help="Show all users (including disabled ones)")
listusers_parser.set_defaults(cls=ListUsers)
disableuser_parser = subparsers.add_parser("disableuser", help="Disable a user")
disableuser_parser.add_argument("user", help="Username to be disabled")
disableuser_parser.set_defaults(cls=DisableUser)
enableuser_parser = subparsers.add_parser("enableuser", help="Enable a user")
enableuser_parser.add_argument("user", help="Username to be enabled")
enableuser_parser.set_defaults(cls=EnableUser)
hasuser_parser = subparsers.add_parser("hasuser", help="Test if a user exists")
hasuser_parser.add_argument("user", help="Username to be checked")
hasuser_parser.set_defaults(cls=HasUser)
moduleparser.add_argument(
"--noninteractive", action="store_true", help="Don't ask for any user input (useful for automation)"
)
moduleparser.add_argument("--silent", action="store_true", help="Ignore errors (useful for automation)")
def run_admin_action(parser, args):
if hasattr(args, "cls"):
command = args.cls()
else:
if not hasattr(args, "silent") or not args.silent:
parser.print_help()
sys.exit(1)
sys.exit(0)
try:
command.run(args)
except Exception:
if not hasattr(args, "silent") or not args.silent:
print("Error running command:")
traceback.print_exc()
sys.exit(1)
sys.exit(0)

115
owrx/admin/commands.py Normal file
View File

@ -0,0 +1,115 @@
from abc import ABC, ABCMeta, abstractmethod
from getpass import getpass
from owrx.users import UserList, User, DefaultPasswordClass
import sys
import random
import string
import os
class Command(ABC):
@abstractmethod
def run(self, args):
pass
class UserCommand(Command, metaclass=ABCMeta):
def getPassword(self, args, username):
if args.noninteractive:
if "OWRX_PASSWORD" in os.environ:
password = os.environ["OWRX_PASSWORD"]
generated = False
else:
print("Generating password for user {username}...".format(username=username))
password = self.getRandomPassword()
generated = True
print('Password for {username} is "{password}".'.format(username=username, password=password))
print('This password is suitable for initial setup only, you will be asked to reset it on initial use.')
print('This password cannot be recovered from the system, please copy it now.')
else:
password = getpass("Please enter the new password for {username}: ".format(username=username))
confirm = getpass("Please confirm the new password: ")
if password != confirm:
print("ERROR: Password mismatch.")
sys.exit(1)
generated = False
return password, generated
def getRandomPassword(self, length=10):
printable = list(string.ascii_letters) + list(string.digits)
return ''.join(random.choices(printable, k=length))
class NewUser(UserCommand):
def run(self, args):
username = args.user
userList = UserList()
# early test to bypass the password stuff if the user already exists
if username in userList:
raise KeyError("User {username} already exists".format(username=username))
password, generated = self.getPassword(args, username)
print("Creating user {username}...".format(username=username))
user = User(name=username, enabled=True, password=DefaultPasswordClass(password), must_change_password=generated)
userList.addUser(user)
class DeleteUser(UserCommand):
def run(self, args):
username = args.user
print("Deleting user {username}...".format(username=username))
userList = UserList()
userList.deleteUser(username)
class ResetPassword(UserCommand):
def run(self, args):
username = args.user
password, generated = self.getPassword(args, username)
userList = UserList()
userList[username].setPassword(DefaultPasswordClass(password), must_change_password=generated)
# this is a change to an object in the list, not the list itself
# in this case, store() is explicit
userList.store()
class DisableUser(UserCommand):
def run(self, args):
username = args.user
userList = UserList()
userList[username].disable()
userList.store()
class EnableUser(UserCommand):
def run(self, args):
username = args.user
userList = UserList()
userList[username].enable()
userList.store()
class ListUsers(Command):
def run(self, args):
userList = UserList()
print("List of enabled users:")
for u in userList.values():
if args.all or u.enabled:
print(" {name}".format(name=u.name))
class HasUser(Command):
"""
internal command used by the debian config scripts to test if the admin user has already been created
"""
def run(self, args):
userList = UserList()
if args.user in userList:
if not args.silent:
print('User "{name}" exists.'.format(name=args.user))
else:
if not args.silent:
print('User "{name}" does not exist.'.format(name=args.user))
# in bash, a return code > 0 is interpreted as "false"
sys.exit(1)

View File

@ -1,270 +0,0 @@
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,
)
try:
for line in decoder.stdout:
self.outputWriter.send((job.freq, line))
except OSError:
decoder.stdout.flush()
# TODO uncouple parsing from the output so that decodes can still go to the map and the spotters
logger.debug("output has gone away while decoding job.")
try:
rc = decoder.wait(timeout=10)
if rc != 0:
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.outputWriter.close()
self.outputWriter = None
# drain messages left in the queue so that the queue can be successfully closed
# this is necessary since python keeps the file descriptors open otherwise
try:
while True:
self.outputReader.recv()
except EOFError:
pass
self.outputReader.close()
self.outputReader = None
self.cancelTimer()
try:
self.wavefile.close()
except Exception:
logger.exception("error closing wave file")
try:
os.unlink(self.wavefilename)
except Exception:
logger.exception("error removing undecoded file")
self.wavefile = None
self.wavefilename = None
class AudioChopper(threading.Thread, metaclass=ABCMeta):
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 = None
try:
data = self.source.read(256)
except ValueError:
pass
if data is None or (isinstance(data, bytes) and len(data) == 0):
self.doRun = False
else:
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, OSError):
return None

86
owrx/audio/__init__.py Normal file
View File

@ -0,0 +1,86 @@
from owrx.config import Config
from abc import ABC, ABCMeta, abstractmethod
from typing import List
import logging
logger = logging.getLogger(__name__)
class AudioChopperProfile(ABC):
@abstractmethod
def getInterval(self):
pass
@abstractmethod
def getFileTimestampFormat(self):
pass
@abstractmethod
def decoder_commandline(self, file):
pass
class ProfileSourceSubscriber(ABC):
@abstractmethod
def onProfilesChanged(self):
pass
class ProfileSource(ABC):
def __init__(self):
self.subscribers = []
@abstractmethod
def getProfiles(self) -> List[AudioChopperProfile]:
pass
def subscribe(self, subscriber: ProfileSourceSubscriber):
if subscriber in self.subscribers:
return
self.subscribers.append(subscriber)
def unsubscribe(self, subscriber: ProfileSourceSubscriber):
if subscriber not in self.subscribers:
return
self.subscribers.remove(subscriber)
def fireProfilesChanged(self):
for sub in self.subscribers.copy():
try:
sub.onProfilesChanged()
except Exception:
logger.exception("Error while notifying profile subscriptions")
class ConfigWiredProfileSource(ProfileSource, metaclass=ABCMeta):
def __init__(self):
super().__init__()
self.configSub = None
@abstractmethod
def getPropertiesToWire(self) -> List[str]:
pass
def subscribe(self, subscriber: ProfileSourceSubscriber):
super().subscribe(subscriber)
if self.subscribers and self.configSub is None:
self.configSub = Config.get().filter(*self.getPropertiesToWire()).wire(self.fireProfilesChanged)
def unsubscribe(self, subscriber: ProfileSourceSubscriber):
super().unsubscribe(subscriber)
if not self.subscribers and self.configSub is not None:
self.configSub.cancel()
self.configSub = None
def fireProfilesChanged(self, *args):
super().fireProfilesChanged()
class StaticProfileSource(ProfileSource):
def __init__(self, profiles: List[AudioChopperProfile]):
super().__init__()
self.profiles = profiles
def getProfiles(self) -> List[AudioChopperProfile]:
return self.profiles

90
owrx/audio/chopper.py Normal file
View File

@ -0,0 +1,90 @@
from owrx.modes import Modes, AudioChopperMode
from csdr.output import Output
from itertools import groupby
import threading
from owrx.audio import ProfileSourceSubscriber
from owrx.audio.wav import AudioWriter
from multiprocessing.connection import Pipe
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class AudioChopper(threading.Thread, Output, ProfileSourceSubscriber):
def __init__(self, active_dsp, mode_str: str):
self.read_fn = None
self.doRun = True
self.dsp = active_dsp
self.writers = []
mode = Modes.findByModulation(mode_str)
if mode is None or not isinstance(mode, AudioChopperMode):
raise ValueError("Mode {} is not an audio chopper mode".format(mode_str))
self.profile_source = mode.get_profile_source()
(self.outputReader, self.outputWriter) = Pipe()
super().__init__()
def stop_writers(self):
while self.writers:
self.writers.pop().stop()
def setup_writers(self):
self.stop_writers()
sorted_profiles = sorted(self.profile_source.getProfiles(), key=lambda p: p.getInterval())
groups = {interval: list(group) for interval, group in groupby(sorted_profiles, key=lambda p: p.getInterval())}
writers = [
AudioWriter(self.dsp, self.outputWriter, interval, profiles) for interval, profiles in groups.items()
]
for w in writers:
w.start()
self.writers = writers
def supports_type(self, t):
return t == "audio"
def receive_output(self, t, read_fn):
self.read_fn = read_fn
self.start()
def run(self) -> None:
logger.debug("Audio chopper starting up")
self.setup_writers()
self.profile_source.subscribe(self)
while self.doRun:
data = None
try:
data = self.read_fn(256)
except ValueError:
pass
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")
self.profile_source.unsubscribe(self)
self.stop_writers()
self.outputWriter.close()
self.outputWriter = None
# drain messages left in the queue so that the queue can be successfully closed
# this is necessary since python keeps the file descriptors open otherwise
try:
while True:
self.outputReader.recv()
except EOFError:
pass
self.outputReader.close()
self.outputReader = None
def onProfilesChanged(self):
logger.debug("profile change received, resetting writers...")
self.setup_writers()
def read(self):
try:
return self.outputReader.recv()
except (EOFError, OSError):
return None

172
owrx/audio/queue.py Normal file
View File

@ -0,0 +1,172 @@
from owrx.config import Config
from owrx.config.core import CoreConfig
from owrx.metrics import Metrics, CounterMetric, DirectMetric
from queue import Queue, Full, Empty
import subprocess
import os
import threading
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class QueueJob(object):
def __init__(self, profile, writer, file, freq):
self.profile = profile
self.writer = writer
self.file = file
self.freq = freq
def run(self):
logger.debug("processing file %s", self.file)
tmp_dir = CoreConfig().get_temporary_directory()
decoder = subprocess.Popen(
["nice", "-n", "10"] + self.profile.decoder_commandline(self.file),
stdout=subprocess.PIPE,
cwd=tmp_dir,
close_fds=True,
)
try:
for line in decoder.stdout:
self.writer.send((self.profile, self.freq, line))
except (OSError, AttributeError):
decoder.stdout.flush()
# TODO uncouple parsing from the output so that decodes can still go to the map and the spotters
logger.debug("output has gone away while decoding job.")
try:
rc = decoder.wait(timeout=10)
if rc != 0:
raise RuntimeError("decoder return code: {0}".format(rc))
except subprocess.TimeoutExpired:
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
decoder.kill()
raise
def unlink(self):
try:
os.unlink(self.file)
except FileNotFoundError:
pass
PoisonPill = object()
class QueueWorker(threading.Thread):
def __init__(self, queue):
self.queue = queue
self.doRun = True
super().__init__()
def run(self) -> None:
while self.doRun:
job = self.queue.get()
if job is PoisonPill:
self.stop()
else:
try:
job.run()
except Exception:
logger.exception("failed to decode job")
self.queue.onError()
finally:
job.unlink()
self.queue.task_done()
def stop(self):
self.doRun = False
class DecoderQueue(Queue):
sharedInstance = None
creationLock = threading.Lock()
@staticmethod
def getSharedInstance():
with DecoderQueue.creationLock:
if DecoderQueue.sharedInstance is None:
DecoderQueue.sharedInstance = DecoderQueue()
return DecoderQueue.sharedInstance
@staticmethod
def stopAll():
with DecoderQueue.creationLock:
if DecoderQueue.sharedInstance is not None:
DecoderQueue.sharedInstance.stop()
DecoderQueue.sharedInstance = None
def __init__(self):
pm = Config.get()
super().__init__(pm["decoding_queue_length"])
self.workers = []
self._setWorkers(pm["decoding_queue_workers"])
self.subscriptions = [
pm.wireProperty("decoding_queue_length", self._setMaxSize),
pm.wireProperty("decoding_queue_workers", self._setWorkers),
]
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)
def _setMaxSize(self, size):
if self.maxsize == size:
return
self.maxsize = size
def _setWorkers(self, workers):
while len(self.workers) > workers:
logger.debug("stopping one worker")
self.workers.pop().stop()
while len(self.workers) < workers:
logger.debug("starting one worker")
self.workers.append(self.newWorker())
def stop(self):
logger.debug("shutting down the queue")
while self.subscriptions:
self.subscriptions.pop().cancel()
try:
# purge all remaining jobs
while not self.empty():
job = self.get()
job.unlink()
self.task_done()
except Empty:
pass
# put() a PoisonPill for all active workers to shut them down
for w in self.workers:
if w.is_alive():
self.put(PoisonPill)
self.join()
def put(self, item, **kwargs):
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()

139
owrx/audio/wav.py Normal file
View File

@ -0,0 +1,139 @@
from owrx.config.core import CoreConfig
from owrx.audio import AudioChopperProfile
from owrx.audio.queue import QueueJob, DecoderQueue
import threading
import wave
import os
from datetime import datetime, timedelta
from queue import Full
from typing import List
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class WaveFile(object):
def __init__(self, writer_id):
self.timestamp = datetime.utcnow()
self.writer_id = writer_id
tmp_dir = CoreConfig().get_temporary_directory()
self.filename = "{tmp_dir}/openwebrx-audiochopper-master-{id}-{timestamp}.wav".format(
tmp_dir=tmp_dir,
id=self.writer_id,
timestamp=self.timestamp.strftime("%y%m%d_%H%M%S"),
)
self.waveFile = wave.open(self.filename, "wb")
self.waveFile.setnchannels(1)
self.waveFile.setsampwidth(2)
self.waveFile.setframerate(12000)
def close(self):
self.waveFile.close()
def getFileName(self):
return self.filename
def getTimestamp(self):
return self.timestamp
def writeframes(self, data):
return self.waveFile.writeframes(data)
def unlink(self):
os.unlink(self.filename)
self.waveFile = None
class AudioWriter(object):
def __init__(self, active_dsp, outputWriter, interval, profiles: List[AudioChopperProfile]):
self.dsp = active_dsp
self.outputWriter = outputWriter
self.interval = interval
self.profiles = profiles
self.wavefile = None
self.switchingLock = threading.Lock()
self.timer = None
def getWaveFile(self):
return WaveFile(id(self))
def getNextDecodingTime(self):
# add one second to have the intervals tick over one second earlier
# this avoids filename collisions, but also avoids decoding wave files with less than one second of audio
t = datetime.utcnow() + timedelta(seconds=1)
zeroed = t.replace(minute=0, second=0, microsecond=0)
delta = t - zeroed
seconds = (int(delta.total_seconds() / self.interval) + 1) * self.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):
with self.switchingLock:
file = self.wavefile
self.wavefile = self.getWaveFile()
file.close()
tmp_dir = CoreConfig().get_temporary_directory()
for profile in self.profiles:
# create hardlinks for the individual profiles
filename = "{tmp_dir}/openwebrx-audiochopper-{pid}-{timestamp}.wav".format(
tmp_dir=tmp_dir,
pid=id(profile),
timestamp=file.getTimestamp().strftime(profile.getFileTimestampFormat()),
)
try:
os.link(file.getFileName(), filename)
except OSError:
logger.exception("Error while linking job files")
continue
job = QueueJob(profile, self.outputWriter, filename, self.dsp.get_operating_freq())
try:
DecoderQueue.getSharedInstance().put(job)
except Full:
logger.warning("decoding queue overflow; dropping one file")
job.unlink()
try:
# our master can be deleted now, the profiles will delete their hardlinked copies after processing
file.unlink()
except OSError:
logger.exception("Error while unlinking job files")
self._scheduleNextSwitch()
def start(self):
self.wavefile = self.getWaveFile()
self._scheduleNextSwitch()
def write(self, data):
with self.switchingLock:
self.wavefile.writeframes(data)
def stop(self):
self.cancelTimer()
try:
self.wavefile.close()
except Exception:
logger.exception("error closing wave file")
try:
with self.switchingLock:
self.wavefile.unlink()
except Exception:
logger.exception("error removing undecoded file")
self.wavefile = None

View File

@ -1,4 +1,7 @@
from owrx.modes import Modes
from datetime import datetime, timezone
import json
import os
import logging
@ -12,7 +15,15 @@ class Band(object):
self.upper_bound = dict["upper_bound"]
self.frequencies = []
if "frequencies" in dict:
availableModes = [mode.modulation for mode in Modes.getAvailableModes()]
for (mode, freqs) in dict["frequencies"].items():
if mode not in availableModes:
logger.info(
'Modulation "{mode}" is not available, bandplan bookmark will not be displayed'.format(
mode=mode
)
)
continue
if not isinstance(freqs, list):
freqs = [freqs]
for f in freqs:
@ -22,8 +33,8 @@ class Band(object):
mode=mode, frequency=f, band=self.name
)
)
else:
self.frequencies.append({"mode": mode, "frequency": f})
continue
self.frequencies.append({"mode": mode, "frequency": f})
def inBand(self, freq):
return self.lower_bound <= freq <= self.upper_bound
@ -46,10 +57,29 @@ class Bandplan(object):
return Bandplan.sharedInstance
def __init__(self):
self.bands = self.loadBands()
self.bands = []
self.file_modified = None
self.fileList = ["/etc/openwebrx/bands.json", "bands.json"]
def loadBands(self):
for file in ["/etc/openwebrx/bands.json", "bands.json"]:
def _refresh(self):
modified = self._getFileModifiedTimestamp()
if self.file_modified is None or modified > self.file_modified:
logger.debug("reloading bands from disk due to file modification")
self.bands = self._loadBands()
self.file_modified = modified
def _getFileModifiedTimestamp(self):
timestamp = 0
for file in self.fileList:
try:
timestamp = os.path.getmtime(file)
break
except FileNotFoundError:
pass
return datetime.fromtimestamp(timestamp, timezone.utc)
def _loadBands(self):
for file in self.fileList:
try:
f = open(file, "r")
bands_json = json.load(f)
@ -66,6 +96,7 @@ class Bandplan(object):
return []
def findBands(self, freq):
self._refresh()
return [band for band in self.bands if band.inBand(freq)]
def findBand(self, freq):
@ -76,4 +107,5 @@ class Bandplan(object):
return None
def collectDialFrequencies(self, range):
self._refresh()
return [e for b in self.bands for e in b.getDialFrequencies(range)]

View File

@ -1,4 +1,7 @@
from datetime import datetime, timezone
from owrx.config.core import CoreConfig
import json
import os
import logging
@ -28,6 +31,23 @@ class Bookmark(object):
}
class BookmakrSubscription(object):
def __init__(self, subscriptee, range, subscriber: callable):
self.subscriptee = subscriptee
self.range = range
self.subscriber = subscriber
def inRange(self, bookmark: Bookmark):
low, high = self.range
return low <= bookmark.getFrequency() <= high
def call(self, *args, **kwargs):
self.subscriber(*args, **kwargs)
def cancel(self):
self.subscriptee.unsubscribe(self)
class Bookmarks(object):
sharedInstance = None
@ -38,15 +58,36 @@ class Bookmarks(object):
return Bookmarks.sharedInstance
def __init__(self):
self.bookmarks = self.loadBookmarks()
self.file_modified = None
self.bookmarks = []
self.subscriptions = []
self.fileList = [Bookmarks._getBookmarksFile(), "/etc/openwebrx/bookmarks.json", "bookmarks.json"]
def loadBookmarks(self):
for file in ["/etc/openwebrx/bookmarks.json", "bookmarks.json"]:
def _refresh(self):
modified = self._getFileModifiedTimestamp()
if self.file_modified is None or modified > self.file_modified:
logger.debug("reloading bookmarks from disk due to file modification")
self.bookmarks = self._loadBookmarks()
self.file_modified = modified
def _getFileModifiedTimestamp(self):
timestamp = 0
for file in self.fileList:
try:
f = open(file, "r")
bookmarks_json = json.load(f)
f.close()
return [Bookmark(d) for d in bookmarks_json]
timestamp = os.path.getmtime(file)
break
except FileNotFoundError:
pass
return datetime.fromtimestamp(timestamp, timezone.utc)
def _loadBookmarks(self):
for file in self.fileList:
try:
with open(file, "r") as f:
content = f.read()
if content:
bookmarks_json = json.loads(content)
return [Bookmark(d) for d in bookmarks_json]
except FileNotFoundError:
pass
except json.JSONDecodeError:
@ -57,6 +98,48 @@ class Bookmarks(object):
return []
return []
def getBookmarks(self, range):
(lo, hi) = range
return [b for b in self.bookmarks if lo <= b.getFrequency() <= hi]
def getBookmarks(self, range=None):
self._refresh()
if range is None:
return self.bookmarks
else:
(lo, hi) = range
return [b for b in self.bookmarks if lo <= b.getFrequency() <= hi]
@staticmethod
def _getBookmarksFile():
coreConfig = CoreConfig()
return "{data_directory}/bookmarks.json".format(data_directory=coreConfig.get_data_directory())
def store(self):
# don't write directly to file to avoid corruption on exceptions
jsonContent = json.dumps([b.__dict__() for b in self.bookmarks], indent=4)
with open(Bookmarks._getBookmarksFile(), "w") as file:
file.write(jsonContent)
self.file_modified = self._getFileModifiedTimestamp()
def addBookmark(self, bookmark: Bookmark):
self.bookmarks.append(bookmark)
self.notifySubscriptions(bookmark)
def removeBookmark(self, bookmark: Bookmark):
if bookmark not in self.bookmarks:
return
self.bookmarks.remove(bookmark)
self.notifySubscriptions(bookmark)
def notifySubscriptions(self, bookmark: Bookmark):
for sub in self.subscriptions:
if sub.inRange(bookmark):
try:
sub.call()
except Exception:
logger.exception("Error while calling bookmark subscriptions")
def subscribe(self, range, callback):
self.subscriptions.append(BookmakrSubscription(self, range, callback))
def unsubscribe(self, subscriptions: BookmakrSubscription):
if subscriptions not in self.subscriptions:
return
self.subscriptions.remove(subscriptions)

44
owrx/breadcrumb.py Normal file
View File

@ -0,0 +1,44 @@
from typing import List
from abc import ABC, abstractmethod
class BreadcrumbItem(object):
def __init__(self, title, href):
self.title = title
self.href = href
def render(self, documentRoot, active=False):
return '<li class="breadcrumb-item {active}"><a href="{documentRoot}{href}">{title}</a></li>'.format(
documentRoot=documentRoot, href=self.href, title=self.title, active="active" if active else ""
)
class Breadcrumb(object):
def __init__(self, breadcrumbs: List[BreadcrumbItem]):
self.items = breadcrumbs
def render(self, documentRoot):
return """
<ol class="breadcrumb">
{crumbs}
{last_crumb}
</ol>
""".format(
crumbs="".join(item.render(documentRoot) for item in self.items[:-1]),
last_crumb="".join(item.render(documentRoot, True) for item in self.items[-1:]),
)
def append(self, crumb: BreadcrumbItem):
self.items.append(crumb)
return self
class BreadcrumbMixin(ABC):
def template_variables(self):
variables = super().template_variables()
variables["breadcrumb"] = self.get_breadcrumb().render(self.get_document_root())
return variables
@abstractmethod
def get_breadcrumb(self) -> Breadcrumb:
pass

View File

@ -23,6 +23,7 @@ class ClientRegistry(object):
def __init__(self):
self.clients = []
Config.get().wireProperty("max_clients", self._checkClientCount)
super().__init__()
def broadcast(self):
@ -46,3 +47,8 @@ class ClientRegistry(object):
except ValueError:
pass
self.broadcast()
def _checkClientCount(self, new_count):
for client in self.clients[new_count:]:
logger.debug("closing one connection...")
client.close()

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