476 Commits

Author SHA1 Message Date
774b71f8f0 update latest image 2019-09-29 20:42:31 +02:00
5903ae1603 prevent the meta panel from disappearing 2019-09-29 17:16:08 +02:00
1c72e9ac50 switch rf_gain to 0 for sdrplay (4 is not supported on RSP1) 2019-09-29 16:21:42 +02:00
b662c547f3 update readme 2019-09-29 16:02:37 +02:00
fac19e09cd scale background (it's stretching now, doesn't look too bad though) 2019-09-29 15:48:24 +02:00
5a3e2a2575 auto-focus; submit on enter; 2019-09-29 15:38:50 +02:00
eed520daac implement edit and delete 2019-09-29 15:29:53 +02:00
4a7b42202e add edit and delete button 2019-09-29 14:48:36 +02:00
f292ba55c1 use actual, not visible, frequency 2019-09-28 20:52:37 +02:00
fef6f3bbd1 fix bookmark frequencies 2019-09-28 20:42:17 +02:00
af9fcbc38d complete storage and display 2019-09-28 20:28:25 +02:00
bd9cdc1cba buttons and storage action 2019-09-28 20:15:47 +02:00
be21d4c9ac show dialog and load values into it 2019-09-28 19:20:21 +02:00
b29d3c575d even moar bookmarks 2019-09-28 19:19:55 +02:00
39a4366eab locator wrappers aren't even needed 2019-09-28 16:54:24 +02:00
4c2979d242 add z-index to prevent other content shining through 2019-09-28 16:51:34 +02:00
4407146962 add bookmark button 2019-09-28 16:50:21 +02:00
c3bcb17312 Merge branch 'develop' into bookmarks 2019-09-28 07:36:54 +02:00
1b95807ac6 beautiful 2x scale for retina displays 2019-09-28 07:36:28 +02:00
240074bdc5 Merge branch 'develop' into bookmarks 2019-09-28 03:08:29 +02:00
46162dadbe protect pskreporter upload loop 2019-09-28 03:08:10 +02:00
455001a759 protect pskreporter upload loop 2019-09-28 03:06:34 +02:00
31881ce472 standard font size 2019-09-28 03:03:41 +02:00
9669b4e365 moar bookmarks 2019-09-28 02:35:12 +02:00
d0c0ee2981 prevent line-wraps (not enough space) 2019-09-28 02:34:58 +02:00
12a341e607 click handling and tuning 2019-09-28 02:21:29 +02:00
020445743c add bookmarks display 2019-09-28 01:57:34 +02:00
cc98c94b2b send bookmarks to client 2019-09-28 00:53:58 +02:00
00febdf255 implement all methods for consistency 2019-09-28 00:27:42 +02:00
cbc7b73b1d hand over message handling after initial handshake instead of delegating 2019-09-28 00:25:36 +02:00
42c59a3aa0 fft needs the multiprocessing send, too 2019-09-27 23:29:22 +02:00
5f703a043b fix ping race condition 2019-09-27 23:28:43 +02:00
76fe11741a add ping / pong to keep the websockets running 2019-09-26 22:57:10 +02:00
2c4add6aad update with latest sd card image 2019-09-26 03:08:52 +02:00
6cb7e65231 differentiate between None and empty return 2019-09-26 00:24:55 +01:00
2d1bcf221c add aprs images to the docker build 2019-09-25 23:40:17 +02:00
a761559fd3 latest news for everybody to see 2019-09-25 23:25:49 +02:00
b27eb4a173 code formatting 2019-09-25 23:12:30 +02:00
01fabd0342 use the 60m frequency for europe for now (seems to be controversial) 2019-09-25 23:05:27 +02:00
6911ca407e code format 2019-09-25 00:47:34 +02:00
68fbc436f2 fix length problem 2019-09-25 00:36:40 +02:00
ecb754ab29 disable reporting if not set in config 2019-09-25 00:36:22 +02:00
41bd018191 determine locator from gps coordinates 2019-09-25 00:35:57 +02:00
bfcbd0265a update config 2019-09-24 21:44:14 +02:00
45479b9f65 Merge branch 'develop' into pskreporter 2019-09-24 21:43:00 +02:00
a68ba01320 handle socket timeouts 2019-09-24 21:42:41 +02:00
ba03243527 fix date 2019-09-24 21:42:00 +02:00
22f4504629 set random to be at least 5 minutes 2019-09-24 21:41:31 +02:00
bf59ed34cf no more conditional 2019-09-23 23:53:22 +02:00
d8bc2cab2e actual upload 2019-09-23 23:47:12 +02:00
f8dcff788b build valid packets (hopefully) 2019-09-23 22:45:55 +02:00
4be34e4dc1 integrate pskreporter scheduling (no upload yet) 2019-09-23 18:33:52 +02:00
b1742dafc2 incomplete implementation to extend a callsign location 2019-09-23 16:51:38 +02:00
e24de8334f silence direwolf 2019-09-23 16:51:24 +02:00
ae87185ad0 run the formatter once more 2019-09-23 03:15:24 +02:00
72f92a1c2b use events instead of simple sleep for clean shutdown 2019-09-23 03:06:51 +02:00
8b9121a5c1 tone down http logging 2019-09-22 20:51:33 +02:00
cfb4208db2 improved api 2019-09-22 13:16:24 +02:00
52afe3fb02 tone down wsjt logging 2019-09-22 12:57:59 +02:00
57975b6f96 move connection tracking to all websockets 2019-09-22 12:57:13 +02:00
b4ffc6e2f0 replace os pipe with multiprocessing (seems to work better) 2019-09-22 12:56:35 +02:00
1ed69de5b0 un-couple messaging between connections; use non-blocking io 2019-09-21 22:10:16 +02:00
6ec85aa349 don't start up unnecesserily 2019-09-21 15:24:06 +02:00
671509df3b fix variable name 2019-09-21 15:19:10 +02:00
2edeffb761 close websocket connections in an improved way 2019-09-21 13:49:37 +02:00
428a9ca509 await the right condition 2019-09-21 13:41:04 +02:00
cf273021ab re-draw on update and apply opacity 2019-09-19 16:24:04 +02:00
ecbae5af2d implement icon rotation 2019-09-19 02:25:32 +02:00
15c28b130d use custom marker class to solve overlay problem (and enable rotation at
a later point)
2019-09-19 01:35:58 +02:00
996422ff4b show aprs symbols in decoding list, too 2019-09-19 00:18:51 +02:00
e231c07c80 2x resolution for retina displays 2019-09-18 19:41:37 +02:00
3e8e0c9224 first work on custom aprs icons 2019-09-18 18:50:48 +02:00
c6c4012a36 add aprs symbols to http server 2019-09-18 17:22:35 +02:00
30512e347a fix more threading issues; add users metric 2019-09-18 15:40:23 +02:00
6f983ccb6b synchronize scheduler access 2019-09-18 01:46:31 +02:00
3814767e28 count errors 2019-09-18 01:46:09 +02:00
243e73064a add band information to ysf locations 2019-09-17 18:44:37 +02:00
8df4f9ce52 add the ability to schedule profiles to be used when sources are idle 2019-09-16 00:31:35 +02:00
b0b2df5422 no need for shared instances here 2019-09-15 21:10:30 +02:00
5b6edd110d wsjt decoding depth configuration 2019-09-15 16:37:12 +02:00
392c226cbe overflow metrics 2019-09-15 12:23:35 +02:00
7689d1a2e2 narrow bandpass specifically for wspr 2019-09-15 12:23:11 +02:00
711bd18d06 update readme with latest features 2019-09-13 23:14:44 +02:00
98f1545fca code format 2019-09-13 23:03:05 +02:00
8d47259f78 show decoded aprs messages in the frontend 2019-09-13 22:29:04 +02:00
311f22f6ba flag services (avoid connecting to aprs network twice) 2019-09-13 22:28:17 +02:00
5bcad1ef2f hide output text for packet 2019-09-13 21:04:00 +02:00
be05b54053 jt65 seems very prone to false decodes 2019-09-13 20:58:37 +02:00
6ff55e1279 queue in / out stats 2019-09-13 00:16:36 +02:00
338a19373c count aprs decodes, too 2019-09-12 23:23:50 +02:00
bc5b16b5e3 rewire the metrics; make queue length metric available 2019-09-12 22:50:29 +02:00
a11875145b make wsjt queue configurable 2019-09-12 15:32:54 +02:00
25a1d06dcb Merge branch 'develop' into packet 2019-09-11 01:03:12 +02:00
d87e5da75c attempt to reduce cpu usage by pre-selecting parts of the spectrum with
resamplers
2019-09-11 00:30:14 +02:00
6d44aa3f58 don't decimate at factor 1 2019-09-11 00:27:49 +02:00
08cf8977f7 fix ft4 frequency on 80m 2019-09-09 23:07:38 +02:00
942ee637b0 fix alternate spaces 2019-09-03 23:38:27 +02:00
aac618bfee fix for python 3.5 2019-09-02 16:20:49 +01:00
2dcdad3a49 fix message parsing range 2019-08-28 22:09:52 +02:00
db8d4cd3fe display items and objects on the map 2019-08-28 22:01:01 +02:00
de22169ea8 implement item and object parsing 2019-08-28 21:56:50 +02:00
b24e56803c avoid overriding weather dict keys 2019-08-27 23:52:51 +02:00
5530c96f8e fix message offsets 2019-08-27 23:32:21 +02:00
1d8fea891a additional types; parse messages 2019-08-27 23:13:26 +02:00
707fcdb1ab convert fahrenheit to celsius 2019-08-27 11:42:48 +02:00
1a2f6b4970 improve weather decoding 2019-08-27 11:32:50 +02:00
4409a369fa implement weather report parsing 2019-08-26 23:43:08 +02:00
272c305ec2 handle exceptions that may occur when parsing strings to numbers 2019-08-26 13:24:23 +02:00
a81c5f44a2 improve thirtparty header parsing 2019-08-26 11:41:22 +02:00
2a09462f6f first work on the thirdparty header 2019-08-26 00:10:43 +02:00
fdd74e2e09 remove patch (included in git now) 2019-08-25 16:30:01 +02:00
5cc67aba15 handle execptions during decode to avoid worker drain 2019-08-23 22:32:46 +02:00
62e9a39557 add direwolf to docker build 2019-08-23 22:21:30 +02:00
fadcb9b43f handle a full queue 2019-08-22 21:24:36 +02:00
24d134ad6c try to avoid stressing out the cpu by using a proper queue 2019-08-22 21:16:43 +02:00
faaef9d9f8 let's be nice 2019-08-22 20:51:36 +02:00
c5cc364918 filters don't seem to work 2019-08-22 20:51:09 +02:00
ed9057e780 Merge branch 'develop' into packet 2019-08-18 22:04:55 +00:00
9bdeda7814 Merge branch 'develop' of github.com:jketterl/openwebrx into develop 2019-08-18 22:04:23 +00:00
e4ef364aa8 looks like we have some additional dependencies now 2019-08-18 22:03:41 +00:00
379251d29d filter smallest possible to avoid traffic from the network 2019-08-18 21:41:26 +02:00
cf8b84925e Merge branch 'develop' into packet 2019-08-18 21:41:07 +02:00
f07bc9e6de update wsjt-x version in docker build 2019-08-18 21:40:46 +02:00
94533e277c improve config 2019-08-18 01:39:23 +02:00
73102053dc code formatting 2019-08-18 00:16:08 +02:00
5fab3e3d36 add igate functionality 2019-08-18 00:15:07 +02:00
54bcba195d delete configs after use 2019-08-17 22:38:09 +02:00
7e757c005c implement aprs data extensions 2019-08-17 22:04:45 +02:00
82eaff5da6 get altitude from comment 2019-08-17 20:35:32 +02:00
1eb28d6aee optimize 2019-08-17 20:20:28 +02:00
bdbe45e322 recognize third party data (don't think we can parse them) 2019-08-17 20:01:12 +02:00
34a8311647 remove annoying debugging line 2019-08-17 20:00:57 +02:00
cf45caa762 fix piping stuff for packet 2019-08-17 19:59:58 +02:00
5b72728aa2 timestamps, status updates, replace faulty characters 2019-08-17 13:39:02 +02:00
67f3dc7430 fix conversion errors 2019-08-16 16:43:16 +02:00
b40af9bbdc back to utf-8 2019-08-16 07:29:31 +02:00
cc66ffd6f3 use generated port numbers for direwolf, allowing multiple instances 2019-08-16 01:27:03 +02:00
5a7ef65c56 reduce debugging output 2019-08-15 23:33:02 +02:00
46ac0ecc77 convert speed to metric 2019-08-15 22:10:58 +02:00
cc6561bdda get course and speed and extended info from mic-e frames 2019-08-15 21:46:08 +02:00
3022406f63 get the extra information out of compressed messages 2019-08-15 21:00:01 +02:00
66382eb50f add symbol information 2019-08-15 20:28:24 +02:00
21591ad6b8 format 2019-08-15 19:56:59 +02:00
88bbb76752 make sure there is actually enough data to parse 2019-08-15 19:50:47 +02:00
765f075576 add some type information; fix string offsets 2019-08-15 18:21:35 +02:00
6b93973d9b decode mic-e device and altitude data 2019-08-15 18:08:20 +02:00
439da266a9 prevent empty frames 2019-08-15 15:53:55 +02:00
0207374592 restructure the code to have the parser sit where all the parsers sit 2019-08-15 15:45:15 +02:00
7beb773a37 Merge branch 'develop' into packet 2019-08-12 11:44:20 +02:00
4b3a68f4cd fix the dial button (not enough space on some browsers) 2019-08-12 11:05:32 +02:00
3dbc6ffb2b make aprs available as service 2019-08-12 00:02:39 +02:00
bf5e2bcc84 compressed locations; other TODOS 2019-08-11 22:58:04 +02:00
b80e85638a implement the horrifying mic-e protocol 2019-08-11 22:08:32 +02:00
12c92928fa pass through comments for display on the map 2019-08-11 18:42:41 +02:00
e5dffc3d9f better decoding 2019-08-11 18:13:12 +02:00
fe84a39097 add aprs frequency 2019-08-11 18:12:50 +02:00
55c8ce7cf0 send decodes to map 2019-08-11 17:39:41 +02:00
cbb65e8d79 decode basic aprs frames 2019-08-11 17:18:02 +02:00
2053e5f521 get raw packet data from KISS socket and start decoding 2019-08-11 16:37:30 +02:00
f53b51a208 fix sample rates 2019-08-11 16:36:53 +02:00
e63569e3e9 packet decoding as secondary demodulator, finally displayin something on
the webpage
2019-08-11 13:52:19 +02:00
2fed83659f these should not be in here 2019-08-11 13:09:34 +02:00
ef90e3e048 disable colors 2019-08-11 13:05:36 +02:00
5fbbd897b5 Merge branch 'develop' into packet 2019-08-11 11:53:29 +02:00
b0056a4677 disable services by default 2019-08-11 11:39:35 +02:00
d467d79bdf code format with black 2019-08-11 11:37:45 +02:00
92321a3b4e simple metrics api to interface with collectd and grafana 2019-08-04 18:36:03 +02:00
766300bdff use latest improvementes for fft, too 2019-08-04 17:31:50 +02:00
8214fdb24d looks configurable to me, at least for now 2019-08-04 15:17:03 +02:00
42aae4c03a save some cpu cycles by only running necessary stuff for services 2019-08-04 14:55:56 +02:00
441738e569 additional ft4 frequency on 80m 2019-08-04 00:21:53 +02:00
5337ddba8d add 2m frequencies from wsjt-x 2019-08-03 23:58:08 +02:00
d1eaab7711 delay startup of background services to increase user interface response 2019-08-03 23:44:56 +02:00
8f7f34c190 better colors (?) 2019-07-28 22:13:55 +02:00
e40b400f6f try to improve "moving" callsigns 2019-07-28 16:36:12 +02:00
3b5883dd55 improved legend with opacity 2019-07-28 16:33:19 +02:00
785d439605 play with the colors 2019-07-28 16:26:03 +02:00
ff98b172c4 add option to select coloring by mode, too 2019-07-28 16:17:23 +02:00
30d8b1327b give it some space 2019-07-28 15:59:54 +02:00
74dddcb8ad add simple legend with colors 2019-07-28 15:57:33 +02:00
6e7d99376d color by band 2019-07-28 15:28:39 +02:00
98c5e9e15b allow service configuration 2019-07-28 13:29:45 +02:00
fa08009c50 more logging improvements 2019-07-28 12:11:22 +02:00
ce662796e3 Merge branch 'develop' into services 2019-07-28 11:45:55 +02:00
accf2a34ff fix exception when outside of band 2019-07-28 11:45:28 +02:00
a15e625692 de-duplicate; better logging 2019-07-28 11:40:58 +02:00
7689e31640 increase timeout 2019-07-23 20:28:51 +01:00
8c2cefe304 pass the nmux port on (defaults are bad...) 2019-07-23 16:43:46 +01:00
eb9bc5f8dc add ft4 frequencies, if available 2019-07-22 23:24:46 +02:00
9c927d9001 first iteration of background services 2019-07-21 23:39:11 +02:00
2d6b0f1877 try to catch a failing sdr device 2019-07-21 22:13:20 +02:00
6c2488f052 fix shadowing warning 2019-07-21 22:12:41 +02:00
479c49b02e Merge pull request #1 from D0han/black_reformat
Use official python formatter for better code readability
2019-07-21 21:24:28 +02:00
c0a0a642f9 Merge pull request #2 from D0han/file_permissions
Allow openwebrx.py to be run as normal executable
2019-07-21 21:06:53 +02:00
35f8daee29 Allow openwebrx.py to be run as normal executable 2019-07-21 20:19:33 +02:00
e15dc1ce11 Reformatted with black -l 120 -t py35 . 2019-07-21 19:40:28 +02:00
79062ff3d6 fix wording 2019-07-21 18:40:00 +02:00
fc5abd38cc add information about wsjt-x 2019-07-21 18:38:54 +02:00
6900810f5d modify so that it runs with python 3.5, too 2019-07-21 13:07:38 +01:00
2fae8ffa70 remove some pointless stuff 2019-07-20 20:45:13 +02:00
ea9feeefd2 complete dial frequency feature frontend 2019-07-20 19:53:42 +02:00
f09f730bff ft4 frequency for 20m (at least to my knowledge) 2019-07-20 19:52:46 +02:00
25b0e86f09 add FT4 because why not 2019-07-20 13:38:25 +02:00
18b65f769f better timestamping and overhaul 2019-07-20 12:47:10 +02:00
abd5cf0795 collect dial frequencies and send to client 2019-07-19 23:55:52 +02:00
6e08a428d6 import frequencies; fix band errors 2019-07-19 23:15:10 +02:00
a1856482ff add dial frequencies 2019-07-19 22:41:51 +02:00
a7a032dc8f this goes in there 2019-07-19 21:16:16 +02:00
4493f369dd enable 64-bit frames for large amounts of data 2019-07-19 17:01:50 +02:00
f1098801e2 let's try to avoid browser problems 2019-07-15 21:35:39 +02:00
a15341fdcf detect and pass band information to the map 2019-07-14 19:32:48 +02:00
c94331bf24 hide modes if not available 2019-07-14 18:22:02 +02:00
7dcfead843 let's try to implement jt65 and jt9 as well 2019-07-14 17:09:34 +02:00
0bb8b5349d Merge branch 'wspr' into develop 2019-07-14 16:48:35 +02:00
30b46c4cdd allocate more space to the freq column 2019-07-14 14:43:44 +02:00
69c3a63794 link the map in wpsr messages, too 2019-07-14 14:33:30 +02:00
dd1def149c Merge branch 'develop' of github.com:jketterl/openwebrx into develop 2019-07-13 21:51:49 +00:00
a6f294f361 lib64 hack only if lib64 exists 2019-07-13 21:51:30 +00:00
6d5c8491e4 implement wspr 2019-07-13 23:16:25 +02:00
420b0c60d7 exponential backoff, part 2 2019-07-13 21:44:48 +02:00
9f2b715d9f exponential backoff 2019-07-13 21:40:48 +02:00
f490fbc2c9 update dependencies
add wsjt-x to build for ft8 capabilities
2019-07-13 21:35:57 +02:00
95c117973f update readme with new image 2019-07-13 18:59:06 +02:00
9a25c68d9a wording change 2019-07-13 17:20:03 +02:00
935e79c9c2 use a temporary directory to avoid permission problems 2019-07-13 17:16:38 +02:00
efc5b936f8 clean up after use 2019-07-12 19:34:04 +02:00
c19337d65c fix ft8/usb switchover 2019-07-12 19:28:40 +02:00
2470c2bfa6 pass through the mode on the map 2019-07-11 23:40:09 +02:00
acbf2939c9 infowindow for ysf markers 2019-07-11 21:21:01 +02:00
8edc7c1374 sort by lastseen 2019-07-11 20:53:59 +02:00
d606c85443 separate decoder files 2019-07-11 20:48:02 +02:00
5ada234f64 remove javascript from the header 2019-07-11 19:37:00 +02:00
fdd2dd1b40 use flexbox since the header breaks the map height 2019-07-11 17:38:53 +02:00
d2f524bf90 fix scrolling on feature report 2019-07-11 16:49:06 +02:00
5887522dce header for feature report 2019-07-11 16:44:33 +02:00
688bd769dd move css 2019-07-11 13:44:41 +02:00
649450a24c move css 2019-07-11 13:44:04 +02:00
2bf2fcd685 implement header on map page (not fully functional yet) 2019-07-11 13:40:12 +02:00
d57f9de21e automatic map reconnection 2019-07-10 23:13:03 +02:00
596c868b9d improved map logo 2019-07-10 22:56:32 +02:00
8a8768ed1d fix ft8 audio sample rate issues with sox 2019-07-10 22:31:06 +02:00
32c76beaa2 improved fullscreen layout 2019-07-10 22:18:16 +02:00
cb0b950d34 protect the wave file switchover with a lock, since race conditions have
occured
2019-07-10 22:09:31 +02:00
2536d9f747 more javascript issues 2019-07-09 17:34:24 +02:00
438efa655f fix javascript issues 2019-07-09 17:32:49 +02:00
ad9855a791 pretty logo 2019-07-09 17:28:41 +02:00
58e819606a use moment.js to display a pretty time since last activity 2019-07-08 21:01:30 +02:00
bab8ec1eaa even prettier 2019-07-08 20:47:50 +02:00
c6aa5c3a3c make the interface pretty 2019-07-08 20:45:09 +02:00
c7503f87d7 show ft8 panel only when ft8 is active 2019-07-08 20:31:34 +02:00
561ff95436 make wsjt feature available (not used yet) 2019-07-08 20:16:29 +02:00
2201daaa20 click-through to selected locator on the map 2019-07-07 22:36:34 +02:00
94afa94428 add a link to the map 2019-07-07 21:44:42 +02:00
83273636f6 add a quick infowindow to show who's in a grid square 2019-07-07 21:24:56 +02:00
30b56c553e strip one more character; seen weird stuff at the end. 2019-07-07 20:46:31 +02:00
8b5dc8b3ad fade out markers on the map over time 2019-07-07 20:46:12 +02:00
d1f46c8f55 server-side removal of map positions 2019-07-07 15:52:24 +02:00
d0cecbdfd7 implement removal of old messages in the gui 2019-07-07 14:31:12 +02:00
1a257064f7 add missing parser integration 2019-07-07 14:10:03 +02:00
182a8af57f deliver better timestamps 2019-07-07 14:09:24 +02:00
af315e1671 let's zoom out a little, seems appropriate for now 2019-07-07 01:22:45 +02:00
ceea2475a1 get rid of the extra flags at the end 2019-07-07 00:52:28 +02:00
c22d10d0de add day/night overlay 2019-07-07 00:52:11 +02:00
849337c55d fix locator calculation 2019-07-06 23:15:33 +02:00
25bc788595 parse and show locators on the map 2019-07-06 22:43:36 +02:00
48baea3304 parse locators and send to map 2019-07-06 22:21:47 +02:00
a6d7209a45 explicit timezone information 2019-07-06 21:29:49 +02:00
eb1b1ba22f fix utc timestamps 2019-07-06 21:26:35 +02:00
d8a7dfbdbd ft8 messages panel 2019-07-06 21:04:18 +02:00
fa2d82ac13 ft8 message parsing 2019-07-06 20:03:17 +02:00
284646ee6c first stab at ft8 decoding: chop up audio, call jt9 binary to decode 2019-07-06 18:21:43 +02:00
3f05565b7b show selected callsign on the map 2019-07-06 15:04:39 +02:00
089964a5eb query parameter support for the http module 2019-07-06 13:03:49 +02:00
31b8dd4fd5 send ysf pins to the map 2019-07-06 12:53:11 +02:00
892c92eb1d add a link for the map in the top bar 2019-07-06 12:41:30 +02:00
d0d5dffe79 add some styling 2019-07-05 22:46:43 +02:00
823a4a35f0 implement feature and requirement details 2019-07-05 22:31:46 +02:00
e61c0dcc12 add some basic framework for the featurereport 2019-07-05 19:30:24 +02:00
f5f23e6fbc remove debugging 2019-07-01 21:21:26 +02:00
3b2b51f07c display locations parsed from ysf on map 2019-07-01 21:20:53 +02:00
272caa7100 rename title 2019-07-01 19:51:31 +02:00
2324a2c837 add google maps 2019-07-01 19:49:58 +02:00
893f69ad18 chain as list as a first step to better flexibility 2019-07-01 18:41:12 +02:00
a4a306374d add some map basics 2019-07-01 16:49:39 +02:00
f283a1ad68 prepare for different types of connections 2019-07-01 11:47:07 +02:00
0e205ec1d9 remove unused html files 2019-07-01 11:16:05 +02:00
c3411b8856 update readme with recent stuff 2019-06-30 15:57:32 +02:00
7e0591f0a6 disable squelch for packet, too 2019-06-22 18:31:23 +02:00
1f6f755d7f Merge branch 'develop' into packet 2019-06-22 18:20:01 +02:00
08edcd44ef add an airspy image 2019-06-20 15:37:21 +02:00
84ddcbb74d add a full build for multi-sdr support 2019-06-20 14:56:52 +02:00
f16a5f92e6 hackrf does not depend on soapy the way it's implemented now 2019-06-20 14:47:03 +02:00
a66b540254 remove rtl-sdr as default (new full package coming up) 2019-06-20 14:46:22 +02:00
a8b2e21a5a update to python 3 2019-06-20 14:46:04 +02:00
72bf698d95 Merge branch 'develop' into docker 2019-06-20 13:55:56 +02:00
7a54cf25d1 Merge branch 'master' into develop 2019-06-20 13:54:59 +02:00
96468f9258 add a basic clickable pin that opens google maps for now 2019-06-19 23:16:57 +02:00
231e4e72d9 add missing property binding 2019-06-15 21:47:28 +02:00
3b04465106 pointer on the overlay, too 2019-06-15 19:50:09 +02:00
4e9ef89276 use the old api for python < 3.6 2019-06-15 19:26:59 +02:00
8af8f93434 implement dmr timeslot muting 2019-06-15 19:10:33 +02:00
7362e48cf3 style more like openwebrx 2019-06-15 14:48:57 +02:00
efa0c060fe implement digiham version check 2019-06-15 13:29:59 +02:00
adf62bc2ca sync indicator 2019-06-15 12:30:04 +02:00
3a89f52028 better sync on the client side 2019-06-10 21:30:46 +02:00
c7d969c96e polishing up the imaging 2019-06-09 22:27:35 +02:00
2053a6b16b more clean-up stuff 2019-06-09 19:12:37 +02:00
e1d54bdf1d fix typo 2019-06-09 17:49:14 +02:00
761ca1132d nicer user display panel for YSF, too 2019-06-09 17:39:15 +02:00
2010a38411 add new nicer dmr status display 2019-06-09 15:15:27 +02:00
94516ef341 implement https detection (thanks Denys Vitali) 2019-06-08 23:36:16 +02:00
cde3ff703a gfsk decoder now supports floating point input, so we can stop
converting
2019-06-08 18:47:17 +02:00
b852fcc167 sox can accept float input, no need to convert 2019-06-08 18:17:04 +02:00
f9c14addcc apply audio filtering and agc to dsd too 2019-06-08 09:23:39 +02:00
a9d5fcf82a use fixed buf sizes to avoid cut-off audio 2019-06-07 20:23:58 +02:00
b6e59e9b11 allow avatar to be downloaded on its old url 2019-06-07 20:23:31 +02:00
e8a1a40dc0 try to handle overflowing connections 2019-06-07 20:10:03 +02:00
4b2100b593 Merge branch 'server_rework' into server_rework_dsd 2019-06-07 15:55:15 +02:00
a38872b2d0 Merge branch 'server_rework' of github.com:jketterl/openwebrx into server_rework 2019-06-07 15:49:43 +02:00
e422ca4d9b add airspy support (untested for now) 2019-06-07 15:44:11 +02:00
f49086a527 add first integration of direwolf for aprs 2019-06-07 15:11:04 +02:00
aa7212c642 handle OSErrors, too 2019-06-07 01:14:09 +02:00
0c59caa230 try to handle clipping problems with agc 2019-06-05 00:17:06 +02:00
4934e91e74 increase timeout (it's asynchronous, so we can wait) 2019-06-05 00:13:54 +02:00
546249e950 detect presence of nc 2019-06-05 00:08:56 +02:00
b7fc6a9c87 connection handling fix 2019-06-04 00:39:22 +02:00
2121739925 make the cache global 2019-05-30 18:54:45 +02:00
908e3036e0 digital pipeline tweaks (not sure if it's better that way) 2019-05-30 18:35:58 +02:00
f565b4dbcd download dmr ids asynchronously 2019-05-30 18:32:08 +02:00
7100d43d9e show callsigns for ham radio dmr ids 2019-05-30 17:19:46 +02:00
14f932eea8 parse metadata on the server side 2019-05-30 16:12:13 +02:00
05f6fff8f6 feed rrc filter with floats; add digitalvoice_filter 2019-05-25 01:46:16 +02:00
725615fbe5 display the mode from the metadata for ysf 2019-05-25 01:45:05 +02:00
1846605184 use dc blocker and limiter to improve signal decoding 2019-05-24 18:48:08 +02:00
224c895718 Merge branch 'server_rework' into server_rework_dsd 2019-05-19 22:25:37 +02:00
8a7aeca6b9 if_gain is optional, default is agc 2019-05-19 22:23:35 +02:00
7893216cce 30m fix 2019-05-19 22:12:17 +02:00
a36eb55680 Merge branch 'server_rework' into server_rework_dsd 2019-05-19 22:10:39 +02:00
8091831b1f make both gains available for sdrplay 2019-05-19 22:10:11 +02:00
3a669294d7 check for gfsk_demodulator, too 2019-05-19 17:56:41 +02:00
e79c830db5 Merge branch 'server_rework' into server_rework_dsd 2019-05-19 13:36:49 +02:00
92abef7172 pass antenna parameter only if set 2019-05-19 13:36:05 +02:00
eb758685a1 add antenna switching support for sdrplay 2019-05-19 13:17:36 +02:00
bb6b00a998 fix meta pipe crashes caused by unknown unicode characters (looks ugly now at times, but at least works continuously) 2019-05-18 22:27:19 +02:00
edadc383ff make unvoiced quality actually work 2019-05-18 22:26:52 +02:00
0629e6c777 make the ambe unvoiced quality configurable 2019-05-18 22:10:43 +02:00
e6150e4aca introduce subscription concept to simplify unsubscribing from events 2019-05-18 21:38:15 +02:00
ff8f03c983 slow down the smeter refresh rate a bit 2019-05-17 20:57:55 +02:00
0ab14f63cb add new logo 2019-05-16 23:45:24 +02:00
8e195a0de9 under construction on top looks nicer 2019-05-16 23:14:23 +02:00
7d4111fec8 hide metadata panel if no metadata is available 2019-05-16 23:09:57 +02:00
bd27d91529 resolve todo 2019-05-16 22:39:50 +02:00
9e0c2580d2 more chain magic; no squelch on digital modes; remove experimental buffer configs 2019-05-16 22:36:37 +02:00
35757168d4 add 30m 2019-05-16 21:44:05 +02:00
3f7ba343a2 remove stray character 2019-05-16 21:34:08 +02:00
a6c845de16 demodulator chain optimizations 2019-05-16 21:26:31 +02:00
b1596cbb60 clean up chains 2019-05-15 23:08:55 +02:00
4496fcc8b0 report client numbers on change only 2019-05-15 19:51:50 +02:00
cffb65e37d cpu usage fix 2019-05-15 19:43:52 +02:00
117d0483f7 streamline sdr and dsp integration 2019-05-15 11:44:03 +02:00
03049b79dd narrower bandwidth actually improves decoding 2019-05-15 11:33:23 +02:00
5e67f036b4 fix demodulator buttons 2019-05-14 23:36:37 +02:00
9812d38eee refactor dsp outputs
add digimode metadata
2019-05-14 23:30:03 +02:00
5733a5be9f separate dsd and digiham modes 2019-05-13 22:45:19 +02:00
2ddfa4d4f6 add sox feature dependency 2019-05-13 19:27:25 +02:00
2408d77f15 feature detection for digital voice; display modulator buttons only when
available
2019-05-13 19:19:15 +02:00
823995d4ba Merge branch 'server_rework' into server_rework_dsd 2019-05-13 17:46:02 +02:00
a85a6c694c improve shutdown handling 2019-05-12 18:10:24 +02:00
17a362fe7a no longer a template, no need for special file extension 2019-05-12 17:23:03 +02:00
85be2e97a1 this is now obsolete, as well 2019-05-12 17:20:44 +02:00
ddf9123e8b fix auto-sqelch 2019-05-12 16:02:49 +02:00
da37d03104 refactor into more reasonable namespaces 2019-05-12 15:56:18 +02:00
210fe5352f refactor the sdr.hu updater into the new server, too 2019-05-12 14:35:25 +02:00
697e177f00 remove obsolete global variables block 2019-05-12 13:21:08 +02:00
dd6c7bb2ea 3d waterfall color fix 2019-05-12 13:20:49 +02:00
3c5aa89469 fix the mathbox / 3d spectrum 2019-05-11 17:55:32 +02:00
fbe43a1715 fix logging 2019-05-11 14:33:13 +02:00
b34c1138b9 new version location + version increment 2019-05-11 14:18:43 +02:00
de84dc71e8 trim the config 2019-05-11 13:25:48 +02:00
d5f17d66d9 replace central entry 2019-05-11 12:58:09 +02:00
8617997e23 fix dsp unavailability problems 2019-05-11 00:38:46 +02:00
c7e4d6b976 fix root logger usage 2019-05-11 00:38:22 +02:00
b9d2654669 add 49m broadcast 2019-05-11 00:38:03 +02:00
dc44c9ed61 code style 2019-05-10 23:47:49 +02:00
1c4543b7bf re-implement the status page 2019-05-10 23:00:18 +02:00
dac35ae526 re-establish client reporting 2019-05-10 22:47:40 +02:00
0a22978660 let's see if the logging works this way 2019-05-10 22:47:07 +02:00
abb5b65217 let's get rid of deprecations straight away 2019-05-10 22:17:53 +02:00
b91d24f8d2 more protection 2019-05-10 22:08:18 +02:00
475631a06f log exceptions correctly 2019-05-10 22:08:00 +02:00
981ca755c6 use logging in the dsp module, too 2019-05-10 22:07:26 +02:00
e15359a106 use pythons logging infrastructure 2019-05-10 21:50:58 +02:00
6243a297c0 let's fix some of the code style issues 2019-05-10 21:29:05 +02:00
859e3931c6 link spectrum closer to the sdr source, since the other solution is unstable 2019-05-10 20:59:06 +02:00
52098cf9f9 introduce protected client writes, to avoid hanging connections 2019-05-10 20:08:22 +02:00
1108cd9a96 fix some issues in multi-user operation 2019-05-10 19:40:31 +02:00
dd3a970497 various changes to stabilize sdr switchovers 2019-05-10 18:30:53 +02:00
b17364e701 prevend weird asm.js error by reusing things 2019-05-10 18:29:54 +02:00
7427fa3608 sdr profile selection frontend 2019-05-10 16:14:16 +02:00
1cf4a879f7 might as well show this for now :D 2019-05-10 15:04:30 +02:00
08e0a0af19 start and shutdown dsps in a more controlled manner 2019-05-10 14:58:25 +02:00
b3d5f924c3 rewrite urls to work again 2019-05-10 14:28:29 +02:00
bbd6412e3d test sdrs and their availability early on
use polymorphism to load sdrs in
2019-05-10 14:23:54 +02:00
56ef86aab6 multi-sdr capabilities! 2019-05-09 22:44:29 +02:00
bd627d77b7 misc 2019-05-09 20:11:21 +02:00
6eb37b989f handle property changes on the fft thread 2019-05-09 16:52:42 +02:00
7550a6294e monitor rtl shutdown and allow a sdr-specific sleep parameter 2019-05-09 16:12:32 +02:00
80d387743a add some caching for static assets 2019-05-09 16:12:05 +02:00
56dcd00e82 fix audio on reconnect 2019-05-09 16:11:14 +02:00
425517d576 fix favicon 2019-05-09 16:10:58 +02:00
bd7cd01359 stabilize dsp operation with a lock 2019-05-08 16:31:52 +02:00
f5d9306c37 fix network usage 2019-05-07 20:20:12 +02:00
4cd23cf445 more work to allow seamless config switching 2019-05-07 20:06:06 +02:00
35930f79f1 send a new config message when config properties haven been changed 2019-05-07 18:47:03 +02:00
9fc77c2804 some quick nudges to allow reconfiguration of the rtl_sdr command on the fly 2019-05-07 18:19:53 +02:00
fa05249a9d first steps towards a reconfigurable sdr source 2019-05-07 17:30:30 +02:00
7eaada4726 make sdrs configurable by type; move format_conversion forward 2019-05-07 17:09:29 +02:00
cb187fd3c2 improved property system 2019-05-07 16:32:53 +02:00
df9646aaf9 extended feature detection 2019-05-07 15:50:20 +02:00
e937f2bca3 implement client reconnect; remove some old code 2019-05-07 15:21:16 +02:00
efb6e9c6cd how did that get there? 2019-05-05 22:15:27 +02:00
f44ff3715f secondary demod now at least displaying something (and other small fixes) 2019-05-05 22:09:48 +02:00
7732b3f685 create maps the python way 2019-05-05 21:09:49 +02:00
1c2810ccb8 remove debugging 2019-05-05 21:09:01 +02:00
a4313c3340 add secondary demod (not working with my csdr atm, unable to test.) 2019-05-05 20:36:50 +02:00
628731cba4 require handshake 2019-05-05 20:12:36 +02:00
30f8244abf add feature detection 2019-05-05 19:59:03 +02:00
142a4c87bd proper shutdown of dsp thread 2019-05-05 19:46:13 +02:00
cb0d59de61 make receiver details dynamic 2019-05-05 17:52:26 +02:00
0da62dad82 add cpu usage 2019-05-05 17:34:40 +02:00
854ac6d5f1 (hopefully) improve the header markup 2019-05-05 17:10:49 +02:00
0f86796e75 get the s-meter back 2019-05-05 16:17:55 +02:00
7481399908 use the web_port as configured 2019-05-05 15:53:35 +02:00
716542107f use some of those properties 2019-05-05 15:51:33 +02:00
6c82c36915 get the squelch, too 2019-05-04 23:14:31 +02:00
f05afc4b0a get the audio going as well 2019-05-04 23:11:13 +02:00
6ec21e6716 send missing parameters for audio client startup 2019-05-04 20:40:13 +02:00
1f909080db we got fft 2019-05-04 20:26:11 +02:00
89690d214d first work on the websocket connection 2019-05-04 16:56:23 +02:00
bd8e665198 add new webserver infrastructure 2019-05-03 22:59:24 +02:00
6294797466 add hackrf support 2019-01-24 17:24:15 +01:00
7bec9eaa87 don't build/push the latest tag, that's a manifest now 2019-01-24 17:07:12 +01:00
8c0a818549 split soapysdr from the sdrplay build 2019-01-24 16:46:16 +01:00
d5b5fc3798 fix the arch command 2019-01-22 18:27:25 +00:00
a2766bcc2e separate patch for raspberry 2019-01-22 17:44:58 +01:00
9953c7d1e1 fix the sdrplay driver installation 2019-01-22 17:25:46 +01:00
8d10fc573f move stuff to alpine to reduce image size (sdrplay not woking yet) 2019-01-22 14:52:53 +00:00
dea09d8eaa multi-platform build 2019-01-22 12:52:03 +01:00
74930ba253 some compatibility 2019-01-22 11:35:48 +01:00
28f84c5188 pushes need to be separate 2019-01-21 22:53:59 +00:00
b2b04dc65f fix typo 2019-01-21 17:47:05 +00:00
a712d5ca3e split into separate docker builds 2019-01-21 17:44:35 +00:00
a60521420b prepare separate images based on the used sdr device 2019-01-21 17:02:58 +00:00
896fd0c178 add docker build and push scripts 2019-01-21 16:40:36 +00:00
075fee46b7 use the dsd version with stdout support 2019-01-21 16:38:46 +00:00
4f6a9249e8 add sdrplay support 2019-01-13 21:04:29 +00:00
51b9d1289a reduce size 2019-01-13 15:54:36 +00:00
27571bd63a add docker packaging 2019-01-13 14:12:09 +00:00
3e2c20b204 make /status return a valid http response (acceptaple for varnish) 2018-12-06 06:08:41 +00:00
8ab42ce944 Merge branch 'master' into dsd_integration 2018-10-15 17:36:48 +02:00
d1ce737886 use new non-blocking strategy (affects all reads) 2018-09-25 21:15:23 +00:00
7e08c8f28e fix digital metadata 2018-09-25 19:03:12 +00:00
aa03def329 fix indents 2018-09-25 15:32:30 +02:00
7f90c0a67a more overlooked changes 2018-09-25 15:28:53 +02:00
bf4c70dfef merge recent openwebrx changes into our work 2018-09-25 14:56:47 +02:00
ac6e001fd6 metadata for ysf 2017-01-20 12:26:09 +00:00
89740b1a93 add ysf to the receiver 2016-11-27 01:29:17 +00:00
aa959cdc93 strip newlines from metadata 2016-11-15 19:20:18 +00:00
6d5a7ffefc fix javascript errors 2016-11-12 19:55:43 +00:00
95acf40eb6 more effort displaying meta information 2016-11-11 21:42:45 +00:00
7700214e5f add metadata pipe to allow digital protocol information to be displayed in the website 2016-11-11 20:56:17 +00:00
49e3bd3b80 remove old canvases from the dom to reduce memory footprint 2016-10-29 19:43:18 +00:00
fba07c521a refactor dsd parametrization 2016-10-22 21:52:22 +00:00
9569fbd72e narrower filter for dstar & nxdn 2016-10-22 21:51:51 +00:00
0a389256eb mute audio when buffer is empty 2016-10-16 19:40:03 +00:00
ddac30db63 add integrations for dmr, d-star and nxdn via dsd 2016-10-15 14:30:30 +00:00
79 changed files with 8214 additions and 2262 deletions

141
README.md
View File

@ -1,64 +1,72 @@
# OpenWebRX OpenWebRX
=========
OpenWebRX is a multi-user SDR receiver software with a web interface.
----
### ⚠️ From 2019-12-29 OpenWebRX development is discontinued. ⚠️
I'm would like to say a big thanks to everyone who supported me during this project, including those who contributed either code or donations. It has been a very fruitful 6 years, but now it's time to move on to other projects. See also my [blog](https://blog.sdr.hu) about that.
(@simonyiszk, please keep this GitHub repo for historic purposes.)
Know limitations of the last version:
- Python 2.7, a main dependency of the project, will be not be officially maintained from 1 January 2020. By time, probably it will not be secure to use this version on public servers, unless someone still provides security patches for Python 2.
- Some specific parts of the DSP code could be improved for better SNR.
Even though these limitations are probably acceptable in an amateur radio project, I would not build critical infrastructure on it.
For commercial inquiries (e.g. if someone wants me to develop an improved version without these limitations), I'm still open, [drop me an e-mail](mailto:randras@sdr.hu).
----
[:floppy_disk: Setup guide for Ubuntu](http://blog.sdr.hu/2015/06/30/quick-setup-openwebrx.html) | [:blue_book: Knowledge base on the Wiki](https://github.com/simonyiszk/openwebrx/wiki/) | [:earth_americas: Receivers on SDR.hu](http://sdr.hu/) [:floppy_disk: Setup guide for Ubuntu](http://blog.sdr.hu/2015/06/30/quick-setup-openwebrx.html) | [:blue_book: Knowledge base on the Wiki](https://github.com/simonyiszk/openwebrx/wiki/) | [:earth_americas: Receivers on SDR.hu](http://sdr.hu/)
OpenWebRX is a multi-user SDR receiver software with a web interface.
![OpenWebRX](http://blog.sdr.hu/images/openwebrx/screenshot.png) ![OpenWebRX](http://blog.sdr.hu/images/openwebrx/screenshot.png)
It has the following features: It has the following features:
- <a href="https://github.com/simonyiszk/csdr">csdr</a> based demodulators (AM/FM/SSB/CW/BPSK31), - [csdr](https://github.com/simonyiszk/csdr) based demodulators (AM/FM/SSB/CW/BPSK31),
- filter passband can be set from GUI, - filter passband can be set from GUI,
- waterfall display can be shifted back in time, - it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas
- it extensively uses HTML5 features like WebSocket, Web Audio API, and &lt;canvas&gt;, - it works in Google Chrome, Chromium and Mozilla Firefox
- it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28), - currently supports RTL-SDR, HackRF, SDRplay, AirSpy
- currently supports RTL-SDR, HackRF, SDRplay, AirSpy and many other devices, see the <a href="https://github.com/simonyiszk/openwebrx/wiki/">OpenWebRX Wiki</a>, - Multiple SDR devices can be used simultaneously
- it has a 3D waterfall display: - [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF)
- [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)
![OpenWebRX 3D waterfall](http://blog.sdr.hu/images/openwebrx/screenshot-3d.gif) **News (2019-09-29 by DD5FJK)**
- One of the most-requested features is finally coming to OpenWebRX: Bookmarks (sometimes also referred to as labels). There's two kinds of bookmarks available:
- Serverside bookmarks that are set up by the receiver administrator. Check the file `bookmarks.json` for examples!
- Clientside bookmarks which every user can store for themselves. They are stored in the browser's localStorage.
- Some more bugs in the websocket handling have been fixed.
**News (2015-08-18)** **News (2019-09-25 by DD5JFK)**
- My BSc. thesis written on OpenWebRX is <a href="https://sdr.hu/static/bsc-thesis.pdf">available here.</a> - Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is now possible. Please have a look at the configuration on how to set it up.
- Several bugs were fixed to improve reliability and stability. - Websocket communication has been overhauled in large parts. It should now be more reliable, and failing connections should now have no impact on other users.
- OpenWebRX now supports compression of audio and waterfall stream, so the required network uplink bandwidth has been decreased from 2 Mbit/s to about 200 kbit/s per client! (Measured with the default settings. It is also dependent on `fft_size`.) - Profile scheduling allows to set up band-hopping if you are running background services.
- OpenWebRX now uses <a href="https://github.com/simonyiszk/csdr#sdrjs">sdr.js</a> (*libcsdr* compiled to JavaScript) for some client-side DSP tasks. - APRS now has the ability to show symbols on the map, if a corresponding symbol set has been installed. Check the config!
- Receivers can now be listed on <a href="http://sdr.hu/">SDR.hu</a>. - Debug logging has been disabled in a handful of modules, expect vastly reduced output on the shell.
- License for OpenWebRX is now Affero GPL v3.
**News (2016-02-14)** **News (2019-09-13 by DD5JFK)**
- The DDC in *csdr* has been manually optimized for ARM NEON, so it runs around 3 times faster on the Raspberry Pi 2 than before. - New set of APRS-related features
- Also we use *ncat* instead of *rtl_mus*, and it is 3 times faster in some cases. - Decode Packet transmissions using [direwolf](https://github.com/wb2osz/direwolf) (1k2 only for now)
- OpenWebRX now supports URLs like: `http://localhost:8073/#freq=145555000,mod=usb` - APRS packets are mostly decoded and shown both in a new panel and on the map
- UI improvements were made, thanks to John Seamons and Gnoxter. - APRS is also available as a background service
- direwolfs I-gate functionality can be enabled, which allows your receiver to work as a receive-only I-gate for the APRS network in the background
- Demodulation for background services has been optimized to use less total bandwidth, saving CPU
- More metrics have been added; they can be used together with collectd and its curl_json plugin for now, with some limitations.
**News (2017-04-04)** **News (2019-07-21 by DD5JFK)**
- *ncat* has been replaced with a custom implementation called *nmux* due to a bug that caused regular crashes on some machines. The *nmux* tool is part of the *csdr* package. - Latest Features:
- Most consumer SDR devices are supported via <a href="https://github.com/rxseger/rx_tools">rx_tools</a>, see the <a href="https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX">OpenWebRX Wiki</a> on that. - More WSJT-X modes have been added, including the new FT4 mode
- I started adding a bandplan feature, the first thing visible is the "dial" indicator that brings you right to the dial frequency for digital modes
- fixed some bugs in the websocket communication which broke the map
**News (2017-07-12)** **News (2019-07-13 by DD5JFK)**
- OpenWebRX now has a BPSK31 demodulator and a 3D waterfall display. - Latest Features:
- FT8 Integration (using wsjt-x demodulators)
- New Map Feature that shows both decoded grid squares from FT8 and Locations decoded from YSF digital voice
- New Feature report that will show what functionality is available
- There's a new Raspbian SD Card image available (see below)
> When upgrading OpenWebRX, please make sure that you also upgrade *csdr*! **News (2019-06-30 by DD5JFK)**
- I have done some major rework on the openwebrx core, and I am planning to continue adding more features in the near future. Please check this place for updates.
- My work has not been accepted into the upstream repository, so you will need to chose between my fork and the official version.
- I have enabled the issue tracker on this project, so feel free to file bugs or suggest enhancements there!
- This version sports the following new and amazing features:
- Support of multiple SDR devices simultaneously
- Support for multiple profiles per SDR that allow the user to listen to different frequencies
- Support for digital voice decoding
- Feature detection that will disable functionality when dependencies are not available (if you're missing the digital buttons, this is probably why)
- Raspbian SD Card Images and Docker builds available (see below)
- I am currently working on the feature set for a stable release, but you are more than welcome to test development versions!
> When upgrading OpenWebRX, please make sure that you also upgrade *csdr* and *digiham*!
## OpenWebRX servers on SDR.hu ## OpenWebRX servers on SDR.hu
@ -68,22 +76,49 @@ It has the following features:
## Setup ## Setup
OpenWebRX currently requires Linux and python 2.7 to run. ### Raspberry Pi SD Card Images
Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-09-29-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+.
This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply.
Please note: I have not updated this to include the Raspberry Pi 4 yet. (It seems to be impossible to build Rasbpian Buster images on x86 hardware right now. Stay tuned!)
Once you have booted a Raspberry with the SD Card, it will appear in your network with the hostname "openwebrx", which should make it available as http://openwebrx:8073/ on most networks. This may vary depending on your specific setup.
For Digital voice, the minimum requirement right now seems to be a Rasbperry Pi 3B+. I would like to work on optimizing this for lower specs, but at this point I am not sure how much can be done.
### Docker Images
For those familiar with docker, I am providing [recent builds and Releases for both x86 and arm processors on the Docker hub](https://hub.docker.com/r/jketterl/openwebrx). You can find a short introduction there.
### Manual Installation
OpenWebRX currently requires Linux and python 3 to run.
First you will need to install the dependencies: First you will need to install the dependencies:
- <a href="https://github.com/simonyiszk/csdr">libcsdr</a> - [csdr](https://github.com/simonyiszk/csdr)
- <a href="http://sdr.osmocom.org/trac/wiki/rtl-sdr">rtl-sdr</a> - [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr)
Optional Dependencies if you want to be able to listen do digital voice:
- [digiham](https://github.com/jketterl/digiham)
- [dsd](https://github.com/f4exb/dsdcc)
Optional Dependency if you want to decode WSJT-X modes:
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html)
After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server:
python openwebrx.py ./openwebrx.py
You can now open the GUI at <a href="http://localhost:8073">http://localhost:8073</a>. You can now open the GUI at <a href="http://localhost:8073">http://localhost:8073</a>.
Please note that the server is also listening on the following ports (on localhost only): Please note that the server is also listening on the following ports (on localhost only):
- port 4951 for the multi-user I/Q server. - ports 4950 to 4960 for the multi-user I/Q servers.
Now the next step is to customize the parameters of your server in `config_webrx.py`. Now the next step is to customize the parameters of your server in `config_webrx.py`.
@ -104,8 +139,6 @@ If you have any problems installing OpenWebRX, you should check out the <a href=
Sometimes the actual error message is not at the end of the terminal output, you may have to look at the whole output to find it. Sometimes the actual error message is not at the end of the terminal output, you may have to look at the whole output to find it.
If you want to run OpenWebRX on a remote server instead of *localhost*, do not forget to set *server_hostname* in `config_webrx.py`.
## Licensing ## Licensing
OpenWebRX is available under Affero GPL v3 license (<a href="https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)">summary</a>). OpenWebRX is available under Affero GPL v3 license (<a href="https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)">summary</a>).

190
bands.json Normal file
View File

@ -0,0 +1,190 @@
[
{
"name": "160m",
"lower_bound": 1810000,
"upper_bound": 2000000,
"frequencies": {
"psk31": 1838000,
"ft8": 1840000,
"wspr": 1836600,
"jt65": 1838000,
"jt9": 1839000
}
},
{
"name": "80m",
"lower_bound": 3500000,
"upper_bound": 3800000,
"frequencies": {
"psk31": 3580000,
"ft8": 3573000,
"wspr": 3592600,
"jt65": 3570000,
"jt9": 3572000,
"ft4": [3568000, 3575000]
}
},
{
"name": "60m",
"lower_bound": 5351500,
"upper_bound": 5366500,
"frequencies": {
"ft8": 5357000,
"wspr": 5364700
}
},
{
"name": "40m",
"lower_bound": 7000000,
"upper_bound": 7200000,
"frequencies": {
"psk31": 7040000,
"ft8": 7074000,
"wspr": 7038600,
"jt65": 7076000,
"jt9": 7078000,
"ft4": 7047500
}
},
{
"name": "30m",
"lower_bound": 10100000,
"upper_bound": 10150000,
"frequencies": {
"psk31": 10141000,
"ft8": 10136000,
"wspr": 10138700,
"jt65": 10138000,
"jt9": 10140000,
"ft4": 10140000
}
},
{
"name": "20m",
"lower_bound": 14000000,
"upper_bound": 14350000,
"frequencies": {
"psk31": 14070000,
"ft8": 14074000,
"wspr": 14095600,
"jt65": 14076000,
"jt9": 14078000,
"ft4": 14080000
}
},
{
"name": "17m",
"lower_bound": 18068000,
"upper_bound": 18168000,
"frequencies": {
"psk31": 18098000,
"ft8": 18100000,
"wspr": 18104600,
"jt65": 18102000,
"jt9": 18104000,
"ft4": 18104000
}
},
{
"name": "15m",
"lower_bound": 21000000,
"upper_bound": 21450000,
"frequencies": {
"psk31": 21070000,
"ft8": 21074000,
"wspr": 21094600,
"jt65": 21076000,
"jt9": 21078000,
"ft4": 21140000
}
},
{
"name": "12m",
"lower_bound": 24890000,
"upper_bound": 24990000,
"frequencies": {
"psk31": 24920000,
"ft8": 24915000,
"wspr": 24924600,
"jt65": 24917000,
"jt9": 24919000,
"ft4": 24919000
}
},
{
"name": "10m",
"lower_bound": 28000000,
"upper_bound": 29700000,
"frequencies": {
"psk31": [28070000, 28120000],
"ft8": 28074000,
"wspr": 28124600,
"jt65": 28076000,
"jt9": 28078000,
"ft4": 28180000
}
},
{
"name": "6m",
"lower_bound": 50030000,
"upper_bound": 51000000,
"frequencies": {
"psk31": 50305000,
"ft8": 50313000,
"wspr": 50293000,
"jt65": 50310000,
"jt9": 50312000,
"ft4": 50318000
}
},
{
"name": "4m",
"lower_bound": 70150000,
"upper_bound": 70200000,
"frequencies": {
"wspr": 70091000
}
},
{
"name": "2m",
"lower_bound": 144000000,
"upper_bound": 146000000,
"frequencies": {
"wspr": 144489000,
"ft8": 144174000,
"ft4": 144170000,
"jt65": 144120000,
"packet": 144800000
}
},
{
"name": "70cm",
"lower_bound": 430000000,
"upper_bound": 440000000
},
{
"name": "23cm",
"lower_bound": 1240000000,
"upper_bound": 1300000000
},
{
"name": "13cm",
"lower_bound": 2320000000,
"upper_bound": 2450000000
},
{
"name": "9cm",
"lower_bound": 3400000000,
"upper_bound": 3475000000
},
{
"name": "6cm",
"lower_bound": 5650000000,
"upper_bound": 5850000000
},
{
"name": "3cm",
"lower_bound": 10000000000,
"upper_bound": 10500000000
}
]

65
bookmarks.json Normal file
View File

@ -0,0 +1,65 @@
[{
"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"
}]

22
build.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
set -euxo pipefail
ARCH=$(uname -m)
case $ARCH in
x86_64)
BASE_IMAGE=alpine
;;
armv*)
BASE_IMAGE=arm32v6/alpine
esac
TAGS=$ARCH
docker build --build-arg BASE_IMAGE=$BASE_IMAGE -t openwebrx-base:$ARCH -f docker/Dockerfiles/Dockerfile-base .
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-rtlsdr:$ARCH -f docker/Dockerfiles/Dockerfile-rtlsdr .
docker build --build-arg ARCH=$ARCH -t openwebrx-soapysdr-base:$ARCH -f docker/Dockerfiles/Dockerfile-soapysdr .
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-sdrplay:$ARCH -f docker/Dockerfiles/Dockerfile-sdrplay .
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-hackrf:$ARCH -f docker/Dockerfiles/Dockerfile-hackrf .
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-airspy:$ARCH -f docker/Dockerfiles/Dockerfile-airspy .
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-full:$ARCH -t jketterl/openwebrx:$ARCH -f docker/Dockerfiles/Dockerfile-full .

View File

@ -36,7 +36,6 @@ config_webrx: configuration options for OpenWebRX
# ==== Server settings ==== # ==== Server settings ====
web_port = 8073 web_port = 8073
server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket)
max_clients = 20 max_clients = 20
# ==== Web GUI configuration ==== # ==== Web GUI configuration ====
@ -65,17 +64,14 @@ Website: <a href="http://localhost" target="_blank">http://localhost</a>
sdrhu_key = "" sdrhu_key = ""
# 3. Set this setting to True to enable listing: # 3. Set this setting to True to enable listing:
sdrhu_public_listing = False sdrhu_public_listing = False
server_hostname = "localhost"
# ==== DSP/RX settings ==== # ==== DSP/RX settings ====
fft_fps = 9 fft_fps = 9
fft_size = 4096 # Should be power of 2 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_voverlap_factor = (
0.3
# samp_rate = 250000 ) # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
samp_rate = 2400000
center_freq = 144250000
rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode.
ppm = 0
audio_compression = "adpcm" # valid values: "adpcm", "none" audio_compression = "adpcm" # valid values: "adpcm", "none"
fft_compression = "adpcm" # valid values: "adpcm", "none" fft_compression = "adpcm" # valid values: "adpcm", "none"
@ -83,7 +79,11 @@ fft_compression="adpcm" #valid values: "adpcm", "none"
digimodes_enable = True # Decoding digimodes come with higher CPU usage. digimodes_enable = True # Decoding digimodes come with higher CPU usage.
digimodes_fft_size = 1024 digimodes_fft_size = 1024
start_rtl_thread=True # determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes
# if you're running on a Raspi (up to 3B+) you'll want to leave this on 1
digital_voice_unvoiced_quality = 1
# enables lookup of DMR ids using the radioid api
digital_voice_dmr_id_lookup = True
""" """
Note: if you experience audio underruns while CPU usage is 100%, you can: Note: if you experience audio underruns while CPU usage is 100%, you can:
@ -101,81 +101,108 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support # # Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support #
################################################################################################# #################################################################################################
# You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR): # Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf", "airspy"
# >> RTL-SDR via rtl_sdr sdrs = {
start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) "rtlsdr": {
format_conversion="csdr convert_u8_f" "name": "RTL-SDR USB Stick",
"type": "rtl_sdr",
#lna_gain=8 "ppm": 0,
#rf_amp=1 # you can change this if you use an upconverter. formula is:
#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain) # shown_center_freq = center_freq + lfo_offset
#format_conversion="csdr convert_s8_f" # "lfo_offset": 0,
""" "profiles": {
To use a HackRF, compile the HackRF host tools from its "stdout" branch: "70cm": {
git clone https://github.com/mossmann/hackrf/ "name": "70cm Relais",
cd hackrf "center_freq": 438800000,
git fetch "rf_gain": 30,
git checkout origin/stdout "samp_rate": 2400000,
cd host "start_freq": 439275000,
mkdir build "start_mod": "nfm",
cd build },
cmake .. -DINSTALL_UDEV_RULES=ON "2m": {
make "name": "2m komplett",
sudo make install "center_freq": 145000000,
""" "rf_gain": 30,
"samp_rate": 2400000,
# >> Sound card SDR (needs ALSA) "start_freq": 145725000,
# I did not have the chance to properly test it. "start_mod": "nfm",
#samp_rate = 96000 },
#start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate) },
#format_conversion="csdr convert_s16_f | csdr gain_ff 30" },
"sdrplay": {
# >> /dev/urandom test signal source "name": "SDRPlay RSP2",
# samp_rate = 2400000 "type": "sdrplay",
# start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) "ppm": 0,
# format_conversion="csdr convert_u8_f" "profiles": {
"20m": {
# >> Pre-recorded raw I/Q file as signal source "name": "20m",
# You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file. "center_freq": 14150000,
#start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05) "rf_gain": 0,
#format_conversion="csdr convert_u8_f" "samp_rate": 500000,
"start_freq": 14070000,
#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc. "start_mod": "usb",
# It will auto-detect your SDR hardware if the following tools are installed: "antenna": "Antenna A",
# * the vendor provided driver and library, },
# * the vendor-specific SoapySDR wrapper library, "30m": {
# * and SoapySDR itself. "name": "30m",
# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/ "center_freq": 10125000,
#start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) "rf_gain": 0,
#format_conversion="" "samp_rate": 250000,
"start_freq": 10142000,
# >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source) "start_mod": "usb",
#start_rtl_command="cat /tmp/osmocom_fifo" },
#format_conversion="" "40m": {
"name": "40m",
"center_freq": 7100000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 7070000,
"start_mod": "usb",
"antenna": "Antenna A",
},
"80m": {
"name": "80m",
"center_freq": 3650000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 3570000,
"start_mod": "usb",
"antenna": "Antenna A",
},
"49m": {
"name": "49m Broadcast",
"center_freq": 6000000,
"rf_gain": 0,
"samp_rate": 500000,
"start_freq": 6070000,
"start_mod": "am",
"antenna": "Antenna A",
},
},
},
# this one is just here to test feature detection
"test": {"type": "test"},
}
# ==== Misc settings ==== # ==== Misc settings ====
shown_center_freq = center_freq #you can change this if you use an upconverter
client_audio_buffer_size = 5 client_audio_buffer_size = 5
# increasing client_audio_buffer_size will: # increasing client_audio_buffer_size will:
# - also increase the latency # - also increase the latency
# - decrease the chance of audio underruns # - decrease the chance of audio underruns
start_freq = center_freq iq_port_range = [
start_mod = "nfm" #nfm, am, lsb, usb, cw 4950,
4960,
iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. ] # TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
#access_log = "~/openwebrx_access.log"
# ==== Color themes ==== # ==== Color themes ====
# A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels # A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
### default theme by teejez: ### default theme by teejez:
waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]" waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF]
waterfall_min_level = -88 # in dB waterfall_min_level = -88 # in dB
waterfall_max_level = -20 waterfall_max_level = -20
waterfall_auto_level_margin = (5, 40) waterfall_auto_level_margin = (5, 40)
@ -197,7 +224,16 @@ waterfall_auto_level_margin = (5, 40)
# 3D view settings # 3D view settings
mathbox_waterfall_frequency_resolution = 128 # bins mathbox_waterfall_frequency_resolution = 128 # bins
mathbox_waterfall_history_length = 10 # seconds mathbox_waterfall_history_length = 10 # seconds
mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" mathbox_waterfall_colors = [
0x000000FF,
0x2E6893FF,
0x69A5D0FF,
0x214B69FF,
0x9DC4E0FF,
0xFFF775FF,
0xFF8A8AFF,
0xB20000FF,
]
# === Experimental settings === # === Experimental settings ===
# Warning! The settings below are very experimental. # Warning! The settings below are very experimental.
@ -207,10 +243,48 @@ csdr_through = False # Setting this True will print out how much data is going i
nmux_memory = 50 # in megabytes. This sets the approximate size of the circular buffer used by nmux. nmux_memory = 50 # in megabytes. This sets the approximate size of the circular buffer used by nmux.
#Look up external IP address automatically from icanhazip.com, and use it as [server_hostname] google_maps_api_key = ""
"""
print "[openwebrx-config] Detecting external IP address..." # how long should positions be visible on the map?
import urllib2 # they will start fading out after half of that
server_hostname=urllib2.urlopen("http://icanhazip.com").read()[:-1] # in seconds; default: 2 hours
print "[openwebrx-config] External IP address detected:", server_hostname map_position_retention_time = 2 * 60 * 60
"""
# wsjt decoder queue configuration
# due to the nature of the wsjt operating modes (ft8, ft8, jt9, jt65 and wspr), the data is recorded for a given amount
# of time (6.5 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)
wsjt_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 wsjt services running at the same time
wsjt_queue_length = 10
# wsjt decoding depth will allow more results, but will also consume more cpu
wsjt_decoding_depth = 3
# can also be set for each mode separately
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
wsjt_decoding_depths = {"jt65": 1}
temporary_directory = "/tmp"
services_enabled = False
services_decoders = ["ft8", "ft4", "wspr", "packet"]
# === aprs igate settings ===
# if you want to share your APRS decodes with the aprs network, configure these settings accordingly
aprs_callsign = "N0CALL"
aprs_igate_enabled = False
aprs_igate_server = "euro.aprs2.net"
aprs_igate_password = ""
# beacon uses the receiver_gps setting, so if you enable this, make sure the location is correct there
aprs_igate_beacon = False
# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols)
aprs_symbols_path = "/opt/aprs-symbols/png"
# === PSK Reporter setting ===
# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator
pskreporter_enabled = False
pskreporter_callsign = "N0CALL"

676
csdr.py Executable file → Normal file
View File

@ -21,17 +21,51 @@ OpenWebRX csdr plugin: do the signal processing with csdr
""" """
import subprocess import subprocess
import time
import os import os
import code
import signal import signal
import fcntl import threading
from functools import partial
class dsp: from owrx.kiss import KissClient, DirewolfConfig
from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper
def __init__(self): 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)).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 = read()
if data is None or (isinstance(data, bytes) and len(data) == 0):
run = False
else:
write(data)
return copy
def supports_type(self, t):
return True
class dsp(object):
def __init__(self, output):
self.samp_rate = 250000 self.samp_rate = 250000
self.output_rate = 11025 #this is default, and cannot be set at the moment self.output_rate = 11025
self.fft_size = 1024 self.fft_size = 1024
self.fft_fps = 5 self.fft_fps = 5
self.offset_freq = 0 self.offset_freq = 0
@ -45,9 +79,8 @@ class dsp:
self.fft_compression = "none" self.fft_compression = "none"
self.demodulator = "nfm" self.demodulator = "nfm"
self.name = "csdr" self.name = "csdr"
self.format_conversion = "csdr convert_u8_f"
self.base_bufsize = 512 self.base_bufsize = 512
self.nc_port = 4951 self.nc_port = None
self.csdr_dynamic_bufsize = False self.csdr_dynamic_bufsize = False
self.csdr_print_bufsizes = False self.csdr_print_bufsizes = False
self.csdr_through = False self.csdr_through = False
@ -59,63 +92,180 @@ class dsp:
self.secondary_fft_size = 1024 self.secondary_fft_size = 1024
self.secondary_process_fft = None self.secondary_process_fft = None
self.secondary_process_demod = None self.secondary_process_demod = None
self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "iqtee_pipe", "iqtee2_pipe"] self.pipe_names = [
"bpf_pipe",
"shift_pipe",
"squelch_pipe",
"smeter_pipe",
"meta_pipe",
"iqtee_pipe",
"iqtee2_pipe",
"dmr_control_pipe",
]
self.secondary_pipe_names = ["secondary_shift_pipe"] self.secondary_pipe_names = ["secondary_shift_pipe"]
self.secondary_offset_freq = 1000 self.secondary_offset_freq = 1000
self.unvoiced_quality = 1
self.modification_lock = threading.Lock()
self.output = output
self.temporary_directory = "/tmp"
self.is_service = False
self.direwolf_config = None
self.direwolf_port = None
def set_service(self, flag=True):
self.is_service = flag
def set_temporary_directory(self, what):
self.temporary_directory = what
def chain(self, which): def chain(self, which):
any_chain_base="nc -v 127.0.0.1 {nc_port} | " chain = ["nc -v 127.0.0.1 {nc_port}"]
if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | " if self.csdr_dynamic_bufsize:
if self.csdr_through: any_chain_base+="csdr through | " chain += ["csdr setbuf {start_bufsize}"]
any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | " if self.csdr_through:
chain += ["csdr through"]
if which == "fft": if which == "fft":
fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \ chain += [
("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \ "csdr fft_cc {fft_size} {fft_block_size}",
"csdr fft_exchange_sides_ff {fft_size}" "csdr logpower_cf -70"
if self.fft_averages == 0
else "csdr logaveragepower_cf -70 {fft_size} {fft_averages}",
"csdr fft_exchange_sides_ff {fft_size}",
]
if self.fft_compression == "adpcm": if self.fft_compression == "adpcm":
return fft_chain_base+" | csdr compress_fft_adpcm_f_u8 {fft_size}" chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"]
else: return chain
return fft_chain_base chain += ["csdr shift_addition_cc --fifo {shift_pipe}"]
chain_begin=any_chain_base+"csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | " if self.decimation > 1:
chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"]
chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"]
if self.output.supports_type("smeter"):
chain += [
"csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}"
]
if self.secondary_demodulator: if self.secondary_demodulator:
chain_begin+="csdr tee {iqtee_pipe} | " if self.output.supports_type("secondary_fft"):
chain_begin+="csdr tee {iqtee2_pipe} | " chain += ["csdr tee {iqtee_pipe}"]
chain_end = "" chain += ["csdr tee {iqtee2_pipe}"]
# early exit if we don't want audio
if not self.output.supports_type("audio"):
return chain
# safe some cpu cycles... no need to decimate if decimation factor is 1
last_decimation_block = (
["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else []
)
if which == "nfm":
chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"]
chain += last_decimation_block
chain += ["csdr deemphasis_nfm_ff {audio_rate}"]
if self.get_audio_rate() != self.get_output_rate():
chain += [
"sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
]
else:
chain += ["csdr convert_f_s16"]
elif self.isDigitalVoice(which):
chain += ["csdr fmdemod_quadri_cf", "dc_block "]
chain += last_decimation_block
# dsd modes
if which in ["dstar", "nxdn"]:
chain += ["csdr limit_ff", "csdr convert_f_s16"]
if which == "dstar":
chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "]
elif which == "nxdn":
chain += ["dsd -fi -i - -o - -u {unvoiced_quality} -g -1 "]
chain += ["CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f"]
max_gain = 5
# digiham modes
else:
chain += ["rrc_filter", "gfsk_demodulator"]
if which == "dmr":
chain += [
"dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}",
"mbe_synthesizer -f -u {unvoiced_quality}",
]
elif which == "ysf":
chain += ["ysf_decoder --fifo {meta_pipe}", "mbe_synthesizer -y -f -u {unvoiced_quality}"]
max_gain = 0.0005
chain += [
"digitalvoice_filter -f",
"CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain),
"sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
]
elif which == "am":
chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"]
chain += last_decimation_block
chain += ["csdr agc_ff", "csdr limit_ff", "csdr convert_f_s16"]
elif which == "ssb":
chain += ["csdr realpart_cf"]
chain += last_decimation_block
chain += ["csdr agc_ff", "csdr limit_ff"]
# fixed sample rate necessary for the wsjt-x tools. fix with sox...
if self.get_audio_rate() != self.get_output_rate():
chain += [
"sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
]
else:
chain += ["csdr convert_f_s16"]
if self.audio_compression == "adpcm": if self.audio_compression == "adpcm":
chain_end = " | csdr encode_ima_adpcm_i16_u8" chain += ["csdr encode_ima_adpcm_i16_u8"]
if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr fastagc_ff 1024 | csdr convert_f_s16"+chain_end return chain
elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
def secondary_chain(self, which): def secondary_chain(self, which):
secondary_chain_base = "cat {input_pipe} | " secondary_chain_base = "cat {input_pipe} | "
if which == "fft": if which == "fft":
return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "") return (
secondary_chain_base
+ "csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 "
+ (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression == "adpcm" else "")
)
elif which == "bpsk31": elif which == "bpsk31":
return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \ return (
"csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \ secondary_chain_base
"csdr simple_agc_cc 0.001 0.5 | " + \ + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | "
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ + "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | "
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ + "csdr simple_agc_cc 0.001 0.5 | "
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" + "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | "
+ "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | "
+ "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"
)
elif self.isWsjtMode(which):
chain = secondary_chain_base + "csdr realpart_cf | "
if self.last_decimation != 1.0:
chain += "csdr fractional_decimator_ff {last_decimation} | "
chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
return chain
elif which == "packet":
chain = secondary_chain_base + "csdr fmdemod_quadri_cf | "
if self.last_decimation != 1.0:
chain += "csdr fractional_decimator_ff {last_decimation} | "
chain += "csdr convert_f_s16 | direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h - 1>&2"
return chain
def set_secondary_demodulator(self, what): def set_secondary_demodulator(self, what):
if self.get_secondary_demodulator() == what:
return
self.secondary_demodulator = what self.secondary_demodulator = what
self.calculate_decimation()
self.restart()
def secondary_fft_block_size(self): def secondary_fft_block_size(self):
return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here return (self.samp_rate / self.decimation) / (
self.fft_fps * 2
) # *2 is there because we do FFT on real signal here
def secondary_decimation(self): def secondary_decimation(self):
return 1 # currently unused return 1 # currently unused
def secondary_bpf_cutoff(self): def secondary_bpf_cutoff(self):
if self.secondary_demodulator == "bpsk31": if self.secondary_demodulator == "bpsk31":
return (31.25/2) / self.if_samp_rate() return 31.25 / self.if_samp_rate()
return 0 return 0
def secondary_bpf_transition_bw(self): def secondary_bpf_transition_bw(self):
if self.secondary_demodulator == "bpsk31": if self.secondary_demodulator == "bpsk31":
return (31.25/2) / self.if_samp_rate() return 31.25 / self.if_samp_rate()
return 0 return 0
def secondary_samples_per_bits(self): def secondary_samples_per_bits(self):
@ -128,71 +278,109 @@ class dsp:
return 31.25 return 31.25
def start_secondary_demodulator(self): def start_secondary_demodulator(self):
if(not self.secondary_demodulator): return if not self.secondary_demodulator:
print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate() return
secondary_command_fft=self.secondary_chain("fft") logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate())
secondary_command_demod = self.secondary_chain(self.secondary_demodulator) secondary_command_demod = self.secondary_chain(self.secondary_demodulator)
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod)
self.try_create_configs(secondary_command_demod)
secondary_command_fft=secondary_command_fft.format( \ secondary_command_demod = secondary_command_demod.format(
input_pipe=self.iqtee_pipe, \ input_pipe=self.iqtee2_pipe,
secondary_fft_input_size=self.secondary_fft_size, \ secondary_shift_pipe=self.secondary_shift_pipe,
secondary_fft_size=self.secondary_fft_size, \ secondary_decimation=self.secondary_decimation(),
secondary_fft_block_size=self.secondary_fft_block_size(), \ secondary_samples_per_bits=self.secondary_samples_per_bits(),
) secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
secondary_command_demod=secondary_command_demod.format( \ secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
input_pipe=self.iqtee2_pipe, \ if_samp_rate=self.if_samp_rate(),
secondary_shift_pipe=self.secondary_shift_pipe, \ last_decimation=self.last_decimation,
secondary_decimation=self.secondary_decimation(), \ audio_rate=self.get_audio_rate(),
secondary_samples_per_bits=self.secondary_samples_per_bits(), \ direwolf_config=self.direwolf_config,
secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \
if_samp_rate=self.if_samp_rate()
) )
print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft logger.debug("secondary command (demod) = %s", secondary_command_demod)
print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod
#code.interact(local=locals())
my_env = os.environ.copy() my_env = os.environ.copy()
# if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; # 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.csdr_print_bufsizes:
self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) my_env["CSDR_PRINT_BUFSIZES"] = "1"
print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)" if self.output.supports_type("secondary_fft"):
self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes secondary_command_fft = self.secondary_chain("fft")
print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes secondary_command_fft = secondary_command_fft.format(
input_pipe=self.iqtee_pipe,
secondary_fft_input_size=self.secondary_fft_size,
secondary_fft_size=self.secondary_fft_size,
secondary_fft_block_size=self.secondary_fft_block_size(),
)
logger.debug("secondary command (fft) = %s", secondary_command_fft)
self.secondary_process_fft = subprocess.Popen(
secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env
)
self.output.send_output(
"secondary_fft",
partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())),
)
# direwolf does not provide any meaningful data on stdout
# more specifically, it doesn't provide any data. if however, for any strange reason, it would start to do so,
# it would block if not read. by piping it to devnull, we avoid a potential pitfall here.
secondary_output = subprocess.DEVNULL if self.isPacket() else subprocess.PIPE
self.secondary_process_demod = subprocess.Popen(
secondary_command_demod, stdout=secondary_output, shell=True, preexec_fn=os.setpgrp, env=my_env
)
self.secondary_processes_running = True self.secondary_processes_running = True
#open control pipes for csdr and send initialization data if self.isWsjtMode():
# print "==========> 1" smd = self.get_secondary_demodulator()
if self.secondary_shift_pipe != None: #TODO digimodes if smd == "ft8":
# print "==========> 2", self.secondary_shift_pipe chopper = Ft8Chopper(self.secondary_process_demod.stdout)
self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes elif smd == "wspr":
# print "==========> 3" chopper = WsprChopper(self.secondary_process_demod.stdout)
self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes elif smd == "jt65":
# print "==========> 4" chopper = Jt65Chopper(self.secondary_process_demod.stdout)
elif smd == "jt9":
chopper = Jt9Chopper(self.secondary_process_demod.stdout)
elif smd == "ft4":
chopper = Ft4Chopper(self.secondary_process_demod.stdout)
chopper.start()
self.output.send_output("wsjt_demod", chopper.read)
elif self.isPacket():
# we best get the ax25 packets from the kiss socket
kiss = KissClient(self.direwolf_port)
self.output.send_output("packet_demod", kiss.read)
else:
self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
self.set_pipe_nonblocking(self.secondary_process_demod.stdout) # open control pipes for csdr and send initialization data
self.set_pipe_nonblocking(self.secondary_process_fft.stdout) if self.secondary_shift_pipe != None: # TODO digimodes
self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w") # TODO digimodes
self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes
def set_secondary_offset_freq(self, value): def set_secondary_offset_freq(self, value):
self.secondary_offset_freq = value self.secondary_offset_freq = value
if self.secondary_processes_running: if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"):
self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())) self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
self.secondary_shift_pipe_file.flush() self.secondary_shift_pipe_file.flush()
def stop_secondary_demodulator(self): def stop_secondary_demodulator(self):
if self.secondary_processes_running == False: return if self.secondary_processes_running == False:
return
self.try_delete_pipes(self.secondary_pipe_names) self.try_delete_pipes(self.secondary_pipe_names)
if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM) self.try_delete_configs()
if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM) if self.secondary_process_fft:
try:
os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
except ProcessLookupError:
# been killed by something else, ignore
pass
if self.secondary_process_demod:
try:
os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
except ProcessLookupError:
# been killed by something else, ignore
pass
self.secondary_processes_running = False self.secondary_processes_running = False
def read_secondary_demod(self, size):
return self.secondary_process_demod.stdout.read(size)
def read_secondary_fft(self, size):
return self.secondary_process_fft.stdout.read(size)
def get_secondary_demodulator(self): def get_secondary_demodulator(self):
return self.secondary_demodulator return self.secondary_demodulator
@ -207,20 +395,33 @@ class dsp:
self.fft_compression = what self.fft_compression = what
def get_fft_bytes_to_read(self): def get_fft_bytes_to_read(self):
if self.fft_compression=="none": return self.fft_size*4 if self.fft_compression == "none":
if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2) return self.fft_size * 4
if self.fft_compression == "adpcm":
return (self.fft_size / 2) + (10 / 2)
def get_secondary_fft_bytes_to_read(self): def get_secondary_fft_bytes_to_read(self):
if self.fft_compression=="none": return self.secondary_fft_size*4 if self.fft_compression == "none":
if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2) return self.secondary_fft_size * 4
if self.fft_compression == "adpcm":
return (self.secondary_fft_size / 2) + (10 / 2)
def set_samp_rate(self, samp_rate): def set_samp_rate(self, samp_rate):
#to change this, restart is required
self.samp_rate = samp_rate self.samp_rate = samp_rate
self.decimation=1 self.calculate_decimation()
while self.samp_rate/(self.decimation+1)>self.output_rate: if self.running:
self.decimation+=1 self.restart()
self.last_decimation=float(self.if_samp_rate())/self.output_rate
def calculate_decimation(self):
(self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate())
def get_decimation(self, input_rate, output_rate):
decimation = 1
while input_rate / (decimation + 1) >= output_rate:
decimation += 1
fraction = float(input_rate / decimation) / output_rate
intermediate_rate = input_rate / decimation
return (decimation, fraction, intermediate_rate)
def if_samp_rate(self): def if_samp_rate(self):
return self.samp_rate / self.decimation return self.samp_rate / self.decimation
@ -231,62 +432,103 @@ class dsp:
def get_output_rate(self): def get_output_rate(self):
return self.output_rate return self.output_rate
def get_audio_rate(self):
if self.isDigitalVoice() or self.isPacket():
return 48000
elif self.isWsjtMode():
return 12000
return self.get_output_rate()
def isDigitalVoice(self, demodulator=None):
if demodulator is None:
demodulator = self.get_demodulator()
return demodulator in ["dmr", "dstar", "nxdn", "ysf"]
def isWsjtMode(self, demodulator=None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
def isPacket(self, demodulator=None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator == "packet"
def set_output_rate(self, output_rate): def set_output_rate(self, output_rate):
self.output_rate = output_rate self.output_rate = output_rate
self.set_samp_rate(self.samp_rate) #as it depends on output_rate self.calculate_decimation()
def set_demodulator(self, demodulator): def set_demodulator(self, demodulator):
#to change this, restart is required if self.demodulator == demodulator:
return
self.demodulator = demodulator self.demodulator = demodulator
self.calculate_decimation()
self.restart()
def get_demodulator(self): def get_demodulator(self):
return self.demodulator return self.demodulator
def set_fft_size(self, fft_size): def set_fft_size(self, fft_size):
#to change this, restart is required
self.fft_size = fft_size self.fft_size = fft_size
self.restart()
def set_fft_fps(self, fft_fps): def set_fft_fps(self, fft_fps):
#to change this, restart is required
self.fft_fps = fft_fps self.fft_fps = fft_fps
self.restart()
def set_fft_averages(self, fft_averages): def set_fft_averages(self, fft_averages):
#to change this, restart is required
self.fft_averages = fft_averages self.fft_averages = fft_averages
self.restart()
def fft_block_size(self): def fft_block_size(self):
if self.fft_averages == 0: return self.samp_rate/self.fft_fps if self.fft_averages == 0:
else: return self.samp_rate/self.fft_fps/self.fft_averages return self.samp_rate / self.fft_fps
else:
def set_format_conversion(self,format_conversion): return self.samp_rate / self.fft_fps / self.fft_averages
self.format_conversion=format_conversion
def set_offset_freq(self, offset_freq): def set_offset_freq(self, offset_freq):
self.offset_freq = offset_freq self.offset_freq = offset_freq
if self.running: if self.running:
self.modification_lock.acquire()
self.shift_pipe_file.write("%g\n" % (-float(self.offset_freq) / self.samp_rate)) self.shift_pipe_file.write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
self.shift_pipe_file.flush() self.shift_pipe_file.flush()
self.modification_lock.release()
def set_bpf(self, low_cut, high_cut): def set_bpf(self, low_cut, high_cut):
self.low_cut = low_cut self.low_cut = low_cut
self.high_cut = high_cut self.high_cut = high_cut
if self.running: if self.running:
self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) ) self.modification_lock.acquire()
self.bpf_pipe_file.write(
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
)
self.bpf_pipe_file.flush() self.bpf_pipe_file.flush()
self.modification_lock.release()
def get_bpf(self): def get_bpf(self):
return [self.low_cut, self.high_cut] return [self.low_cut, self.high_cut]
def set_squelch_level(self, squelch_level): def set_squelch_level(self, squelch_level):
self.squelch_level = squelch_level self.squelch_level = squelch_level
# no squelch required on digital voice modes
actual_squelch = 0 if self.isDigitalVoice() or self.isPacket() else self.squelch_level
if self.running: if self.running:
self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) ) self.modification_lock.acquire()
self.squelch_pipe_file.write("%g\n" % (float(actual_squelch)))
self.squelch_pipe_file.flush() self.squelch_pipe_file.flush()
self.modification_lock.release()
def get_smeter_level(self): def set_unvoiced_quality(self, q):
if self.running: self.unvoiced_quality = q
line=self.smeter_pipe_file.readline() self.restart()
return float(line[:-1])
def get_unvoiced_quality(self):
return self.unvoiced_quality
def set_dmr_filter(self, filter):
if self.dmr_control_pipe_file:
self.dmr_control_pipe_file.write("{0}\n".format(filter))
self.dmr_control_pipe_file.flush()
def mkfifo(self, path): def mkfifo(self, path):
try: try:
@ -299,9 +541,7 @@ class dsp:
return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate)) return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate))
def try_create_pipes(self, pipe_names, command_base): def try_create_pipes(self, pipe_names, command_base):
# print "try_create_pipes"
for pipe_name in pipe_names: for pipe_name in pipe_names:
# print "\t"+pipe_name
if "{" + pipe_name + "}" in command_base: if "{" + pipe_name + "}" in command_base:
setattr(self, pipe_name, self.pipe_base_path + pipe_name) setattr(self, pipe_name, self.pipe_base_path + pipe_name)
self.mkfifo(getattr(self, pipe_name)) self.mkfifo(getattr(self, pipe_name))
@ -312,113 +552,171 @@ class dsp:
for pipe_name in pipe_names: for pipe_name in pipe_names:
pipe_path = getattr(self, pipe_name, None) pipe_path = getattr(self, pipe_name, None)
if pipe_path: if pipe_path:
try: os.unlink(pipe_path) try:
except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e os.unlink(pipe_path)
except FileNotFoundError:
# it seems like we keep calling this twice. no idea why, but we don't need the resulting error.
pass
except Exception:
logger.exception("try_delete_pipes()")
def set_pipe_nonblocking(self, pipe): def try_create_configs(self, command):
flags = fcntl.fcntl(pipe, fcntl.F_GETFL) if "{direwolf_config}" in command:
fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK) self.direwolf_config = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
tmp_dir=self.temporary_directory, myid=id(self)
)
self.direwolf_port = KissClient.getFreePort()
file = open(self.direwolf_config, "w")
file.write(DirewolfConfig().getConfig(self.direwolf_port, self.is_service))
file.close()
else:
self.direwolf_config = None
self.direwolf_port = None
def try_delete_configs(self):
if self.direwolf_config:
try:
os.unlink(self.direwolf_config)
except FileNotFoundError:
# result suits our expectations. fine :)
pass
except Exception:
logger.exception("try_delete_configs()")
self.direwolf_config = None
def start(self): def start(self):
command_base=self.chain(self.demodulator) self.modification_lock.acquire()
if self.running:
self.modification_lock.release()
return
self.running = True
command_base = " | ".join(self.chain(self.demodulator))
# create control pipes for csdr # create control pipes for csdr
self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self))
# self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None
self.try_create_pipes(self.pipe_names, command_base) self.try_create_pipes(self.pipe_names, command_base)
# if "{bpf_pipe}" in command_base:
# self.bpf_pipe=pipe_base_path+"bpf"
# self.mkfifo(self.bpf_pipe)
# if "{shift_pipe}" in command_base:
# self.shift_pipe=pipe_base_path+"shift"
# self.mkfifo(self.shift_pipe)
# if "{squelch_pipe}" in command_base:
# self.squelch_pipe=pipe_base_path+"squelch"
# self.mkfifo(self.squelch_pipe)
# if "{smeter_pipe}" in command_base:
# self.smeter_pipe=pipe_base_path+"smeter"
# self.mkfifo(self.smeter_pipe)
# if "{iqtee_pipe}" in command_base:
# self.iqtee_pipe=pipe_base_path+"iqtee"
# self.mkfifo(self.iqtee_pipe)
# if "{iqtee2_pipe}" in command_base:
# self.iqtee2_pipe=pipe_base_path+"iqtee2"
# self.mkfifo(self.iqtee2_pipe)
# run the command # run the command
command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \ command = command_base.format(
last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, \ bpf_pipe=self.bpf_pipe,
bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \ shift_pipe=self.shift_pipe,
flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \ decimation=self.decimation,
squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe ) last_decimation=self.last_decimation,
fft_size=self.fft_size,
fft_block_size=self.fft_block_size(),
fft_averages=self.fft_averages,
bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(),
ddc_transition_bw=self.ddc_transition_bw(),
flowcontrol=int(self.samp_rate * 2),
start_bufsize=self.base_bufsize * self.decimation,
nc_port=self.nc_port,
squelch_pipe=self.squelch_pipe,
smeter_pipe=self.smeter_pipe,
meta_pipe=self.meta_pipe,
iqtee_pipe=self.iqtee_pipe,
iqtee2_pipe=self.iqtee2_pipe,
output_rate=self.get_output_rate(),
smeter_report_every=int(self.if_samp_rate() / 6000),
unvoiced_quality=self.get_unvoiced_quality(),
dmr_control_pipe=self.dmr_control_pipe,
audio_rate=self.get_audio_rate(),
)
print "[openwebrx-dsp-plugin:csdr] Command =",command logger.debug("Command = %s", command)
#code.interact(local=locals())
my_env = os.environ.copy() my_env = os.environ.copy()
if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; if self.csdr_dynamic_bufsize:
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) if self.csdr_print_bufsizes:
self.running = True my_env["CSDR_PRINT_BUFSIZES"] = "1"
#open control pipes for csdr and send initialization data out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
if self.bpf_pipe != None: self.process = subprocess.Popen(command, stdout=out, shell=True, preexec_fn=os.setpgrp, env=my_env)
def watch_thread():
rc = self.process.wait()
logger.debug("dsp thread ended with rc=%d", rc)
if rc == 0 and self.running and not self.modification_lock.locked():
logger.debug("restarting since rc = 0, self.running = true, and no modification")
self.restart()
threading.Thread(target=watch_thread).start()
if self.output.supports_type("audio"):
self.output.send_output(
"audio",
partial(
self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256
),
)
# open control pipes for csdr
if self.bpf_pipe:
self.bpf_pipe_file = open(self.bpf_pipe, "w") self.bpf_pipe_file = open(self.bpf_pipe, "w")
self.set_bpf(self.low_cut,self.high_cut) if self.shift_pipe:
if self.shift_pipe != None:
self.shift_pipe_file = open(self.shift_pipe, "w") self.shift_pipe_file = open(self.shift_pipe, "w")
self.set_offset_freq(self.offset_freq) if self.squelch_pipe:
if self.squelch_pipe != None:
self.squelch_pipe_file = open(self.squelch_pipe, "w") self.squelch_pipe_file = open(self.squelch_pipe, "w")
self.set_squelch_level(self.squelch_level)
if self.smeter_pipe != None:
self.smeter_pipe_file=open(self.smeter_pipe,"r")
self.set_pipe_nonblocking(self.smeter_pipe_file)
self.start_secondary_demodulator() self.start_secondary_demodulator()
def read(self,size): self.modification_lock.release()
return self.process.stdout.read(size)
# send initial config through the pipes
if self.squelch_pipe:
self.set_squelch_level(self.squelch_level)
if self.shift_pipe:
self.set_offset_freq(self.offset_freq)
if self.bpf_pipe:
self.set_bpf(self.low_cut, self.high_cut)
if self.smeter_pipe:
self.smeter_pipe_file = open(self.smeter_pipe, "r")
def read_smeter():
raw = self.smeter_pipe_file.readline()
if len(raw) == 0:
return None
else:
return float(raw.rstrip("\n"))
self.output.send_output("smeter", read_smeter)
if self.meta_pipe != None:
# TODO make digiham output unicode and then change this here
self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437")
def read_meta():
raw = self.meta_pipe_file.readline()
if len(raw) == 0:
return None
else:
return raw.rstrip("\n")
self.output.send_output("meta", read_meta)
if self.dmr_control_pipe:
self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w")
def stop(self): def stop(self):
self.modification_lock.acquire()
self.running = False
if hasattr(self, "process"):
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
except ProcessLookupError:
# been killed by something else, ignore
pass
self.stop_secondary_demodulator() self.stop_secondary_demodulator()
#if(self.process.poll()!=None):return # returns None while subprocess is running
#while(self.process.poll()==None):
# #self.process.kill()
# print "killproc",os.getpgid(self.process.pid),self.process.pid
# os.killpg(self.process.pid, signal.SIGTERM)
#
# time.sleep(0.1)
self.try_delete_pipes(self.pipe_names) self.try_delete_pipes(self.pipe_names)
# if self.bpf_pipe: self.modification_lock.release()
# try: os.unlink(self.bpf_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe
# if self.shift_pipe:
# try: os.unlink(self.shift_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.shift_pipe
# if self.squelch_pipe:
# try: os.unlink(self.squelch_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.squelch_pipe
# if self.smeter_pipe:
# try: os.unlink(self.smeter_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe
# if self.iqtee_pipe:
# try: os.unlink(self.iqtee_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee_pipe
# if self.iqtee2_pipe:
# try: os.unlink(self.iqtee2_pipe)
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee2_pipe
self.running = False
def restart(self): def restart(self):
if not self.running:
return
self.stop() self.stop()
self.start() self.start()
def __del__(self): def __del__(self):
self.stop() self.stop()
del(self.process) del self.process

View File

@ -0,0 +1,6 @@
ARG ARCH
FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-airspy.sh /
RUN /install-dependencies-airspy.sh

View File

@ -0,0 +1,17 @@
ARG BASE_IMAGE
FROM $BASE_IMAGE
RUN apk add --no-cache bash
ADD docker/scripts/direwolf-1.5.patch /
ADD docker/scripts/install-dependencies.sh /
RUN /install-dependencies.sh
ADD . /openwebrx
WORKDIR /openwebrx
VOLUME /config
ENTRYPOINT [ "/openwebrx/docker/scripts/run.sh" ]
EXPOSE 8073

View File

@ -0,0 +1,11 @@
ARG ARCH
FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-*.sh /
ADD docker/scripts/install-lib.*.patch /
RUN /install-dependencies-rtlsdr.sh
RUN /install-dependencies-hackrf.sh
RUN /install-dependencies-soapysdr.sh
RUN /install-dependencies-sdrplay.sh
RUN /install-dependencies-airspy.sh

View File

@ -0,0 +1,6 @@
ARG ARCH
FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-hackrf.sh /
RUN /install-dependencies-hackrf.sh

View File

@ -0,0 +1,6 @@
ARG ARCH
FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-rtlsdr.sh /
RUN /install-dependencies-rtlsdr.sh

View File

@ -0,0 +1,7 @@
ARG ARCH
FROM openwebrx-soapysdr-base:$ARCH
ADD docker/scripts/install-dependencies-sdrplay.sh /
ADD docker/scripts/install-lib.*.patch /
RUN /install-dependencies-sdrplay.sh

View File

@ -0,0 +1,6 @@
ARG ARCH
FROM openwebrx-base:$ARCH
ADD docker/scripts/install-dependencies-soapysdr.sh /
RUN /install-dependencies-soapysdr.sh

View File

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

View File

@ -0,0 +1,26 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/airspy/airspyone_host.git
cmakebuild airspyone_host
apk del .build-deps

View File

@ -0,0 +1,29 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb fftw udev"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/mossmann/hackrf.git
cd hackrf
cmakebuild host
cd ..
rm -rf hackrf
apk del .build-deps

View File

@ -0,0 +1,26 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/osmocom/rtl-sdr.git
cmakebuild rtl-sdr
apk del .build-deps

View File

@ -0,0 +1,47 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb udev"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
ARCH=$(uname -m)
case $ARCH in
x86_64)
BINARY=SDRplay_RSP_API-Linux-2.13.1.run
;;
armv*)
BINARY=SDRplay_RSP_API-RPi-2.13.1.run
;;
esac
wget http://www.sdrplay.com/software/$BINARY
sh $BINARY --noexec --target sdrplay
patch --verbose -Np0 < /install-lib.$ARCH.patch
cd sdrplay
./install_lib.sh
cd ..
rm -rf sdrplay
rm $BINARY
git clone https://github.com/pothosware/SoapySDRPlay.git
cmakebuild SoapySDRPlay
apk del .build-deps

View File

@ -0,0 +1,29 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="udev"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/pothosware/SoapySDR
cmakebuild SoapySDR
git clone https://github.com/rxseger/rx_tools
cmakebuild rx_tools
apk del .build-deps

View File

@ -0,0 +1,63 @@
#!/bin/bash
set -euxo pipefail
function cmakebuild() {
cd $1
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools alsa-lib"
BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://git.code.sf.net/p/itpp/git itpp
cmakebuild itpp
git clone https://github.com/jketterl/csdr.git -b 48khz_filter
cd csdr
make
make install
cd ..
rm -rf csdr
git clone https://github.com/szechyjs/mbelib.git
cmakebuild mbelib
if [ -d "/usr/local/lib64" ]; then
# no idea why it's put into there now. alpine does not handle it correctly, so move it.
mv /usr/local/lib64/libmbe* /usr/local/lib
fi
git clone https://github.com/jketterl/digiham.git
cmakebuild digiham
git clone https://github.com/f4exb/dsd.git
cmakebuild dsd
WSJT_DIR=wsjtx-2.1.0
WSJT_TGZ=${WSJT_DIR}.tgz
wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ
tar xvfz $WSJT_TGZ
cmakebuild $WSJT_DIR
git clone https://github.com/wb2osz/direwolf.git
cd direwolf
git checkout 1.5
patch -Np1 < /direwolf-1.5.patch
make
make install
cd ..
rm -rf direwolf
git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols
apk del .build-deps

View File

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

View File

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

23
docker/scripts/run.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
set -euo pipefail
if [[ ! -f /config/config_webrx.py ]] ; then
cp config_webrx.py /config
fi
rm config_webrx.py
ln -s /config/config_webrx.py .
_term() {
echo "Caught signal!"
kill -TERM "$child" 2>/dev/null
}
trap _term SIGTERM SIGINT
python3 openwebrx.py $@ &
child=$!
wait "$child"

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

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

57
htdocs/css/map.css Normal file
View File

@ -0,0 +1,57 @@
@import url("openwebrx-header.css");
@import url("openwebrx-globals.css");
/* expandable photo not implemented on map page */
#webrx-top-photo-clip {
max-height: 67px;
}
body {
display: flex;
flex-direction: column;
}
#webrx-top-container {
flex: none;
}
.openwebrx-map {
flex: 1 1 auto;
}
h3 {
margin: 10px 0;
text-align: center;
}
ul {
margin-block-start: 5px;
margin-block-end: 5px;
padding-inline-start: 25px;
}
.openwebrx-map-legend {
background-color: #fff;
padding: 10px;
margin: 10px;
}
.openwebrx-map-legend ul {
list-style-type: none;
padding: 0;
}
.openwebrx-map-legend li.square .illustration {
display: inline-block;
width: 30px;
height: 20px;
margin-right: 10px;
border-width: 2px;
border-style: solid;
}
.openwebrx-map-legend select {
background-color: #FFF;
border-color: #DDD;
padding: 5px;
}

View File

@ -0,0 +1,8 @@
html, body
{
margin: 0;
padding: 0;
height: 100%;
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
}

View File

@ -0,0 +1,203 @@
#webrx-top-container
{
position: relative;
z-index:1000;
}
#webrx-top-photo
{
width: 100%;
display: block;
}
#webrx-top-photo-clip
{
min-height: 67px;
max-height: 350px;
overflow: hidden;
position: relative;
}
.webrx-top-bar-parts
{
height:67px;
}
#webrx-top-bar
{
background: rgba(128, 128, 128, 0.15);
margin:0;
padding:0;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
right: 0;
}
#webrx-top-logo
{
padding: 12px;
float: left;
}
#webrx-ha5kfu-top-logo
{
float: right;
padding: 15px;
}
#webrx-rx-avatar-background
{
cursor:pointer;
background-image: url(../gfx/openwebrx-avatar-background.png);
background-origin: content-box;
background-repeat: no-repeat;
float: left;
width: 54px;
height: 54px;
padding: 7px;
box-sizing: content-box;
}
#webrx-rx-avatar
{
cursor:pointer;
width: 46px;
height: 46px;
padding: 4px;
border-radius: 8px;
box-sizing: content-box;
}
#webrx-rx-texts {
float: left;
padding: 10px;
}
#webrx-rx-texts div {
padding: 3px;
}
#webrx-rx-title
{
white-space:nowrap;
overflow: hidden;
cursor:pointer;
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
color: #909090;
font-size: 11pt;
font-weight: bold;
}
#webrx-rx-desc
{
white-space:nowrap;
overflow: hidden;
cursor:pointer;
font-size: 10pt;
color: #909090;
}
#webrx-rx-desc a
{
color: #909090;
}
#openwebrx-rx-details-arrow
{
cursor:pointer;
position: absolute;
left: 470px;
top: 51px;
}
#openwebrx-rx-details-arrow a
{
margin: 0;
padding: 0;
}
#openwebrx-rx-details-arrow-down
{
display:none;
}
#openwebrx-main-buttons ul
{
display: table;
margin:0;
}
#openwebrx-main-buttons ul li
{
display: table-cell;
padding-left: 5px;
padding-right: 5px;
cursor:pointer;
}
#openwebrx-main-buttons a {
color: inherit;
text-decoration: inherit;
}
#openwebrx-main-buttons li:hover
{
background-color: rgba(255, 255, 255, 0.3);
}
#openwebrx-main-buttons li:active
{
background-color: rgba(255, 255, 255, 0.55);
}
#openwebrx-main-buttons
{
float: right;
margin:0;
color: white;
text-shadow: 0px 0px 4px #000000;
text-align: center;
font-size: 9pt;
font-weight: bold;
}
#webrx-rx-photo-title
{
position: absolute;
left: 15px;
top: 78px;
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;
font-size: 10pt;
font-weight: bold;
text-shadow: 0px 0px 6px #444;
opacity: 1;
line-height: 1.5em;
}
#webrx-rx-photo-desc a
{
color: #5ca8ff;
text-shadow: none;
}

View File

@ -18,13 +18,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
@import url("openwebrx-header.css");
@import url("openwebrx-globals.css");
html, body html, body {
{
margin: 0;
padding: 0;
height: 100%;
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
overflow: hidden; overflow: hidden;
} }
@ -147,247 +144,115 @@ input[type=range]:focus::-ms-fill-upper
background: #B6B6B6; background: #B6B6B6;
} }
#webrx-top-container
{
position: relative;
z-index:1000;
}
.webrx-top-bar-parts
{
position: absolute;
top: 0px;
left: 0px;
width:100%;
height:67px;
}
#webrx-top-bar-background
{
background-color: #808080;
opacity: 0.15;
filter:alpha(opacity=15);
}
#webrx-top-bar
{
margin:0;
padding:0;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
#webrx-top-logo
{
position: absolute;
top: 12px;
left: 15px;
}
#webrx-ha5kfu-top-logo
{
position: absolute;
top: 15px;
right: 15px;
}
#webrx-top-photo
{
width: 100%;
display: block;
}
#webrx-rx-avatar-background
{
cursor:pointer;
position: absolute;
left: 285px;
top: 6px;
}
#webrx-rx-avatar
{
cursor:pointer;
position: absolute;
left: 289px;
top: 10px;
width: 46px;
height: 46px;
}
#webrx-top-photo-clip
{
max-height: 350px;
overflow: hidden;
position: relative;
}
/*#webrx-bottom-bar
{
position: absolute;
bottom: 0px;
width: 100%;
height: 117px;
background-image:url(gfx/webrx-bottom-bar.png);
}*/
#webrx-page-container #webrx-page-container
{ {
min-height:100%; min-height:100%;
position:relative; position:relative;
} }
/*#webrx-photo-gradient-left
{
position: absolute;
bottom: 0px;
left: 0px;
background-image:url(gfx/webrx-photo-gradient-corner.png);
width: 59px;
height: 92px;
}
#webrx-photo-gradient-middle
{
position: absolute;
bottom: 0px;
left: 59px;
right: 59px;
height: 92px;
background-image:url(gfx/webrx-photo-gradient-middle.png);
}
#webrx-photo-gradient-right
{
position: absolute;
bottom: 0px;
right: 0px;
background-image:url(gfx/webrx-photo-gradient-corner.png);
width: 59px;
height: 92px;
-webkit-transform:scaleX(-1);
-moz-transform:scaleX(-1);
-ms-transform:scaleX(-1);
-o-transform:scaleX(-1);
transform:scaleX(-1);
}*/
#webrx-rx-photo-title
{
position: absolute;
left: 15px;
top: 78px;
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;
font-size: 10pt;
font-weight: bold;
text-shadow: 0px 0px 6px #444;
opacity: 1;
line-height: 1.5em;
}
#webrx-rx-photo-desc a
{
/*color: #007df1;*/
color: #5ca8ff;
text-shadow: none;
/*text-shadow: 0px 0px 7px #fff;*/
}
#webrx-rx-title
{
white-space:nowrap;
overflow: hidden;
cursor:pointer;
position: absolute;
left: 350px;
top: 13px;
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
color: #909090;
font-size: 11pt;
font-weight: bold;
}
#webrx-rx-desc
{
white-space:nowrap;
overflow: hidden;
cursor:pointer;
font-size: 10pt;
color: #909090;
position: absolute;
left: 350px;
top: 34px;
}
#webrx-rx-desc a
{
color: #909090;
/*text-decoration: none;*/
}
#openwebrx-rx-details-arrow
{
cursor:pointer;
position: absolute;
left: 470px;
top: 51px;
}
#openwebrx-rx-details-arrow a
{
margin: 0;
padding: 0;
}
#openwebrx-rx-details-arrow-down
{
display:none;
}
/*canvas#waterfall-canvas
{
border-style: none;
border-width: 1px;
height: 150px;
width: 100%;
}*/
#openwebrx-scale-container #openwebrx-scale-container
{ {
height: 47px; height: 47px;
background-image: url("gfx/openwebrx-scale-background.png");
background-repeat: repeat-x;
overflow: hidden; overflow: hidden;
z-index:1000; z-index:1000;
position: relative; position: relative;
} }
#openwebrx-frequency-container {
background-image: url("../gfx/openwebrx-scale-background.png");
background-repeat: repeat-x;
background-size: cover;
background-color: #444;
}
#openwebrx-bookmarks-container
{
height: 25px;
position: relative;
z-index: 1000;
}
#openwebrx-bookmarks-container .bookmark {
font-size: 12px;
background-color: #FFFF00;
border: 1px solid #000;
border-radius: 5px;
padding: 2px 5px;
cursor: pointer;
white-space: nowrap;
max-height: 14px;
max-width: 50px;
position: absolute;
bottom: 5px;
transform: translate(-50%, 0);
}
#openwebrx-bookmarks-container .bookmark .bookmark-content {
overflow: hidden;
text-overflow: ellipsis;
}
#openwebrx-bookmarks-container .bookmark .bookmark-actions {
display: none;
text-align: right;
}
.bookmark-actions .action {
line-height: 0;
}
.bookmark-actions .action img {
width: 14px;
}
#openwebrx-bookmarks-container .bookmark.selected {
z-index: 1010;
}
#openwebrx-bookmarks-container .bookmark:hover {
z-index: 1011;
max-height: none;
max-width: none;
}
#openwebrx-bookmarks-container .bookmark[editable]:hover .bookmark-actions {
display: block;
margin-bottom: 5px;
}
#openwebrx-bookmarks-container .bookmark:after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 0;
border: 5px solid transparent;
border-top-color: #FFFF00;
border-bottom: 0;
margin-left: -5px;
margin-bottom: -5px;
}
#openwebrx-bookmarks-container .bookmark[data-source=local] {
background-color: #0FF;
}
#openwebrx-bookmarks-container .bookmark[data-source=local]:after {
border-top-color: #0FF;
}
#webrx-canvas-container #webrx-canvas-container
{ {
/*background-image:url('gfx/openwebrx-blank-background-1.jpg');*/ /*background-image:url('../gfx/openwebrx-blank-background-1.jpg');*/
position: relative; position: relative;
height: 2000px; height: 2000px;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
/*background-color: #646464;*/ /*background-color: #646464;*/
/*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/ /*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/
background-image: url('gfx/openwebrx-background-cool-blue.png'); background-image: url('../gfx/openwebrx-background-cool-blue.png');
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: #1e5f7f; background-color: #1e5f7f;
cursor: crosshair; cursor: crosshair;
@ -477,15 +342,15 @@ input[type=range]:focus::-ms-fill-upper
/* removed non-free fonts like that: */ /* removed non-free fonts like that: */
/*@font-face { /*@font-face {
font-family: 'unibody_8_pro_regregular'; font-family: 'unibody_8_pro_regregular';
src: url('gfx/unibody8pro-regular-webfont.eot'); src: url('../gfx/unibody8pro-regular-webfont.eot');
src: url('gfx/unibody8pro-regular-webfont.ttf'); src: url('../gfx/unibody8pro-regular-webfont.ttf');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
}*/ }*/
@font-face { @font-face {
font-family: 'expletus-sans-medium'; font-family: 'expletus-sans-medium';
src: url('gfx/font-expletus-sans/ExpletusSans-Medium.ttf'); src: url('../gfx/font-expletus-sans/ExpletusSans-Medium.ttf');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -582,6 +447,20 @@ input[type=range]:focus::-ms-fill-upper
text-align: center; text-align: center;
} }
.openwebrx-dial-button svg {
width: 19px;
height: 19px;
vertical-align: bottom;
}
.openwebrx-dial-button #ph_dial {
fill: #888;
}
.openwebrx-dial-button.available #ph_dial {
fill: #FFF;
}
.openwebrx-square-button img .openwebrx-square-button img
{ {
height: 27px; height: 27px;
@ -686,54 +565,27 @@ img.openwebrx-mirror-img
height: 20px; height: 20px;
} }
#openwebrx-main-buttons img
{
}
#openwebrx-main-buttons ul
{
display: table;
margin:0;
}
#openwebrx-main-buttons ul li
{
display: table-cell;
padding-left: 5px;
padding-right: 5px;
cursor:pointer;
}
#openwebrx-main-buttons li:hover
{
background-color: rgba(255, 255, 255, 0.3);
}
#openwebrx-main-buttons li:active
{
background-color: rgba(255, 255, 255, 0.55);
}
#openwebrx-main-buttons
{
position: absolute;
right: 133px;
top: 3px;
margin:0;
color: white;
text-shadow: 0px 0px 4px #000000;
text-align: center;
font-size: 9pt;
font-weight: bold;
}
#openwebrx-panel-receiver #openwebrx-panel-receiver
{ {
width:110px; width:110px;
} }
#openwebrx-panel-receiver .frequencies-container {
display: flex;
flex-direction: row;
}
#openwebrx-panel-receiver .frequencies {
flex-grow: 1;
}
#openwebrx-panel-receiver .openwebrx-bookmark-button {
width: 27px;
height: 27px;
text-align: center;
}
#openwebrx-mute-on #openwebrx-mute-on
{ {
color: lime; color: lime;
@ -763,6 +615,10 @@ img.openwebrx-mirror-img
padding-top: 5px; padding-top: 5px;
} }
.openwebrx-panel-line:first-child {
padding-top: 0;
}
#openwebrx-smeter-outer #openwebrx-smeter-outer
{ {
border-color: #888; border-color: #888;
@ -841,10 +697,9 @@ img.openwebrx-mirror-img
transition: width 500ms, left 500ms; transition: width 500ms, left 500ms;
} }
#openwebrx-secondary-demod-listbox .openwebrx-panel select,
{ .openwebrx-dialog select,
width: 201px; .openwebrx-dialog input {
height: 27px;
border-radius: 5px; border-radius: 5px;
background-color: #373737; background-color: #373737;
color: White; color: White;
@ -856,16 +711,28 @@ img.openwebrx-mirror-img
border-color: transparent; border-color: transparent;
border-width: 0px; border-width: 0px;
-moz-appearance: none; -moz-appearance: none;
padding-left:3px;
} }
#openwebrx-secondary-demod-listbox option .openwebrx-panel select option,
{ .openwebrx-dialog select option {
border-width: 0px; border-width: 0px;
background-color: #373737; background-color: #373737;
color: White; color: White;
} }
#openwebrx-secondary-demod-listbox
{
width: 173px;
height: 27px;
padding-left:3px;
}
#openwebrx-sdr-profiles-listbox {
width: 100%;
font-size: 10pt;
height: 27px;
}
#openwebrx-cursor-blink #openwebrx-cursor-blink
{ {
animation: cursor-blink 1s infinite; animation: cursor-blink 1s infinite;
@ -937,11 +804,6 @@ img.openwebrx-mirror-img
transform-origin: 0% 50%; transform-origin: 0% 50%;
} }
#openwebrx-digimode-content .part .subpart
{
}
@keyframes new-digimode-data @keyframes new-digimode-data
{ {
0%{ opacity: 0; } 0%{ opacity: 0; }
@ -971,3 +833,246 @@ img.openwebrx-mirror-img
border-color: Red; border-color: Red;
} }
.openwebrx-meta-slot {
width: 145px;
height: 196px;
float: left;
margin-right: 10px;
background-color: #676767;
padding: 2px 0;
color: #333;
text-align: center;
position: relative;
}
.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before {
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.openwebrx-meta-slot.muted:before {
display: block;
content: "";
background-image: url("../gfx/openwebrx-mute.png");
width:100%;
height:133px;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: rgba(0,0,0,.3);
}
.openwebrx-meta-slot.active {
background-color: #95bbdf;
}
.openwebrx-meta-slot.sync .openwebrx-dmr-slot:before {
content:"";
display: inline-block;
margin: 0 5px;
width: 12px;
height: 12px;
background-color: #ABFF00;
border-radius: 50%;
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;
background-position: center;
background-repeat: no-repeat;
}
.openwebrx-meta-slot.active .openwebrx-meta-user-image {
background-image: url("../gfx/openwebrx-directcall.png");
}
.openwebrx-meta-slot.active .openwebrx-meta-user-image.group {
background-image: url("../gfx/openwebrx-groupcall.png");
}
.openwebrx-dmr-timeslot-panel * {
cursor: pointer;
}
.openwebrx-maps-pin {
background-image: url("../gfx/google_maps_pin.svg");
background-position: center;
background-repeat: no-repeat;
width: 15px;
height: 15px;
background-size: contain;
display: inline-block;
}
#openwebrx-panel-wsjt-message,
#openwebrx-panel-packet-message
{
height: 180px;
}
#openwebrx-panel-wsjt-message tbody,
#openwebrx-panel-packet-message tbody
{
display: block;
overflow: auto;
height: 150px;
width: 100%;
}
#openwebrx-panel-wsjt-message thead tr,
#openwebrx-panel-packet-message thead tr
{
display: block;
}
#openwebrx-panel-wsjt-message th,
#openwebrx-panel-wsjt-message td,
#openwebrx-panel-packet-message th,
#openwebrx-panel-packet-message td
{
width: 50px;
text-align: left;
padding: 1px 3px;
}
#openwebrx-panel-wsjt-message .message {
width: 380px;
}
#openwebrx-panel-wsjt-message .decimal {
text-align: right;
width: 35px;
}
#openwebrx-panel-wsjt-message .decimal.freq {
width: 70px;
}
#openwebrx-panel-packet-message .message {
width: 410px;
}
#openwebrx-panel-packet-message .callsign {
width: 80px;
}
#openwebrx-panel-packet-message .coord {
width: 40px;
text-align: center;
}
.aprs-symbol {
display: inline-block;
width: 15px;
height: 15px;
background-size: 240px 90px;
}
.aprs-symboltable-normal {
background-image: url(../../aprs-symbols/aprs-symbols-24-0.png)
}
.aprs-symboltable-alternate {
background-image: url(../../aprs-symbols/aprs-symbols-24-1.png)
}
.aprs-symboltable-overlay {
background-image: url(../../aprs-symbols/aprs-symbols-24-2.png)
}
.openwebrx-dialog {
background-color: #575757;
padding: 10px;
color: white;
position: fixed;
font-size: 10pt;
border-radius: 15px;
-moz-border-radius: 15px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, 0);
}
.openwebrx-dialog .form-field {
padding: 5px;
display: flex;
flex-direction: row;
}
.openwebrx-dialog .form-field:first-child {
padding-top: 0;
}
.openwebrx-dialog label {
display: inline-block;
flex: 1 0 20px;
padding-right: 20px;
margin-top: auto;
margin-bottom: auto;
}
.openwebrx-dialog .form-field input,
.openwebrx-dialog .form-field select {
flex: 2 0 20px;
height: 27px;
}
.openwebrx-dialog .form-field input {
padding: 0 5px;
}
.openwebrx-dialog .buttons {
text-align: right;
padding: 5px 5px 0;
border-top: 1px solid #666;
}
.openwebrx-dialog .buttons .openwebrx-button {
font-size: 12pt;
min-width: 50px;
text-align: center;
padding: 5px 10px;
}
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="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,
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel
{
display: none;
}
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container
{
height: 200px;
margin: -10px;
}

21
htdocs/features.html Normal file
View File

@ -0,0 +1,21 @@
<HTML><HEAD>
<TITLE>OpenWebRX Feature report</TITLE>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="static/css/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/features.js"></script>
</HEAD><BODY>
${header}
<div class="container">
<h1>OpenWebRX Feature Report</h1>
<table class="features table">
<tr>
<th>Feature</th>
<th>Requirement</th>
<th>Description</th>
<th>Available</th>
</tr>
</table>
</div>
</BODY></HTML>

24
htdocs/features.js Normal file
View File

@ -0,0 +1,24 @@
$(function(){
var converter = new showdown.Converter();
$.ajax('/api/features').done(function(data){
var $table = $('table.features');
$.each(data, function(name, details) {
var requirements = $.map(details.requirements, function(r, name){
return '<tr>' +
'<td></td>' +
'<td>' + name + '</td>' +
'<td>' + converter.makeHtml(r.description) + '</td>' +
'<td>' + (r.available ? 'YES' : 'NO') + '</td>' +
'</tr>';
});
$table.append(
'<tr>' +
'<td colspan=2>' + name + '</td>' +
'<td>' + converter.makeHtml(details.description) + '</td>' +
'<td>' + (details.available ? 'YES' : 'NO') + '</td>' +
'</tr>' +
requirements.join("")
);
})
});
});

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="5.6444445mm"
height="9.847393mm"
viewBox="0 0 20 34.892337"
id="svg3455"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="Map Pin.svg">
<defs
id="defs3457" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="12.181359"
inkscape:cx="8.4346812"
inkscape:cy="14.715224"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1024"
inkscape:window-height="705"
inkscape:window-x="-4"
inkscape:window-y="-4"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata3460">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-814.59595,-274.38623)">
<g
id="g3477"
transform="matrix(1.1855854,0,0,1.1855854,-151.17715,-57.3976)">
<path
sodipodi:nodetypes="sscccccsscs"
inkscape:connector-curvature="0"
id="path4337-3"
d="m 817.11249,282.97118 c -1.25816,1.34277 -2.04623,3.29881 -2.01563,5.13867 0.0639,3.84476 1.79693,5.3002 4.56836,10.59179 0.99832,2.32851 2.04027,4.79237 3.03125,8.87305 0.13772,0.60193 0.27203,1.16104 0.33416,1.20948 0.0621,0.0485 0.19644,-0.51262 0.33416,-1.11455 0.99098,-4.08068 2.03293,-6.54258 3.03125,-8.87109 2.77143,-5.29159 4.50444,-6.74704 4.56836,-10.5918 0.0306,-1.83986 -0.75942,-3.79785 -2.01758,-5.14062 -1.43724,-1.53389 -3.60504,-2.66908 -5.91619,-2.71655 -2.31115,-0.0475 -4.4809,1.08773 -5.91814,2.62162 z"
style="display:inline;opacity:1;fill:#ff4646;fill-opacity:1;stroke:#d73534;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
r="3.0355"
cy="288.25278"
cx="823.03064"
id="path3049"
style="display:inline;opacity:1;fill:#590000;fill-opacity:1;stroke-width:0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

View File

@ -1,85 +0,0 @@
<html>
<!--
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>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
html, body
{
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
width: 100%;
text-align: center;
margin: 0;
padding: 0;
}
img.logo
{
margin-top: 120px;
}
div.frame
{
text-align: left;
margin:0px auto;
width: 800px;
}
div.panel
{
text-align: center;
background-color:#777777;
border-radius: 15px;
padding: 12px;
font-weight: bold;
color: White;
font-size: 13pt;
/*text-shadow: 1px 1px 4px #444;*/
font-family: sans;
}
div.alt
{
font-size: 10pt;
padding-top: 10px;
}
body div a
{
color: #5ca8ff;
text-shadow: none;
}
span.browser
{
}
</style>
</head>
<body>
<div class="frame">
<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/>
<div class="panel">
Sorry, the receiver is inactive due to internal error.
</div>
</div>
</body>
</html>

View File

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

255
htdocs/index.html Normal file
View File

@ -0,0 +1,255 @@
<!DOCTYPE HTML>
<!--
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>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<html>
<head>
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
<script src="static/sdr.js"></script>
<script src="static/mathbox-bundle.min.js"></script>
<script src="static/openwebrx.js"></script>
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/jquery.nanoscroller.js"></script>
<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">
</head>
<body onload="openwebrx_init();">
<div id="webrx-page-container">
${header}
<div id="webrx-main-container">
<div id="openwebrx-frequency-container">
<div id="openwebrx-bookmarks-container"></div>
<div id="openwebrx-scale-container">
<canvas id="openwebrx-scale-canvas" width="0" height="0"></canvas>
</div>
</div>
<div id="openwebrx-mathbox-container"> </div>
<div id="webrx-canvas-container">
<div id="openwebrx-phantom-canvas"></div>
<!-- add canvas here by javascript -->
</div>
<div id="openwebrx-panels-container">
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" data-panel-pos="right" data-panel-order="0" data-panel-size="259,115">
<div class="openwebrx-panel-line frequencies-container">
<div class="frequencies">
<div id="webrx-actual-freq">---.--- MHz</div>
<div id="webrx-mouse-freq">---.--- MHz</div>
</div>
<div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;">
<img src="static/gfx/openwebrx-bookmark.png">
</div>
</div>
<div class="openwebrx-panel-line">
<select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();">
</select>
</div>
<div class="openwebrx-panel-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
onclick="demodulator_analog_replace('nfm');">FM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-am"
onclick="demodulator_analog_replace('am');">AM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-lsb"
onclick="demodulator_analog_replace('lsb');">LSB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-usb"
onclick="demodulator_analog_replace('usb');">USB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
onclick="demodulator_analog_replace('cw');">CW</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dmr"
style="display:none;" data-feature="digital_voice_digiham"
onclick="demodulator_analog_replace('dmr');">DMR</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dstar"
style="display:none;" data-feature="digital_voice_dsd"
onclick="demodulator_analog_replace('dstar');">DStar</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nxdn"
style="display:none;" data-feature="digital_voice_dsd"
onclick="demodulator_analog_replace('nxdn');">NXDN</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-ysf"
style="display:none;" data-feature="digital_voice_digiham"
onclick="demodulator_analog_replace('ysf');">YSF</div>
</div>
<div class="openwebrx-panel-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dig" onclick="demodulator_digital_replace_last();">DIG</div>
<select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();">
<option value="none"></option>
<option value="bpsk31">BPSK31</option>
<option value="ft8" data-feature="wsjt-x">FT8</option>
<option value="wspr" data-feature="wsjt-x">WSPR</option>
<option value="jt65" data-feature="wsjt-x">JT65</option>
<option value="jt9" data-feature="wsjt-x">JT9</option>
<option value="ft4" data-feature="wsjt-x">FT4</option>
<option value="packet" data-feature="packet">Packet</option>
</select>
<div id="openwebrx-secondary-demod-dial-button" class="openwebrx-button openwebrx-dial-button" onclick="dial_button_click();">
<svg version="1.1" id="Layer_1" x="0px" y="0px" width="246px" height="246px" viewBox="0 0 246 246" xmlns="http://www.w3.org/2000/svg">
<g id="ph_dial_1_" transform="matrix(1, 0, 0, 1, -45.398312, -50.931698)">
<path id="ph_dial" d="M238.875,190.125c3.853,7.148,34.267,4.219,50.242,2.145c0.891-5.977,1.508-12.043,1.508-18.27 c0-67.723-54.901-122.625-122.625-122.625c-67.723,0-122.625,54.902-122.625,122.625c0,67.723,54.902,122.625,122.625,122.625 c51.06,0,94.797-31.227,113.25-75.609c-13.969-9.668-41.625-18.891-41.625-18.891c-5.25,0-10.5-3-12.75-8.25 S233.625,180.375,238.875,190.125z M220.465,175.313c0,28.478-23.086,51.563-51.563,51.563c-28.478,0-51.563-23.086-51.563-51.563 c0-28.477,23.086-51.563,51.563-51.563C197.379,123.75,220.465,146.836,220.465,175.313z M185.25,64.125 c10.563,0,19.125,8.563,19.125,19.125s-8.563,19.125-19.125,19.125c-10.562,0-19.125-8.563-19.125-19.125 S174.688,64.125,185.25,64.125z M142.875,69C153.438,69,162,77.563,162,88.125s-8.563,19.125-19.125,19.125 c-10.562,0-19.125-8.563-19.125-19.125S132.313,69,142.875,69z M106.5,91.875c10.563,0,19.125,8.563,19.125,19.125 s-8.563,19.125-19.125,19.125c-10.562,0-19.125-8.562-19.125-19.125S95.938,91.875,106.5,91.875z M81.375,126.75 c10.563,0,19.125,8.563,19.125,19.125S91.938,165,81.375,165c-10.563,0-19.125-8.563-19.125-19.125S70.813,126.75,81.375,126.75z M58.125,188.625c0-10.559,8.563-19.125,19.125-19.125c10.563,0,19.125,8.566,19.125,19.125S87.813,207.75,77.25,207.75 C66.687,207.75,58.125,199.184,58.125,188.625z M75.75,229.875c0-10.559,8.563-19.125,19.125-19.125 c10.563,0,19.125,8.566,19.125,19.125S105.438,249,94.875,249C84.312,249,75.75,240.434,75.75,229.875z M126.375,276 c-10.563,0-19.125-8.566-19.125-19.125s8.563-19.125,19.125-19.125c10.563,0,19.125,8.566,19.125,19.125S136.938,276,126.375,276z M168,288c-10.563,0-19.125-8.566-19.125-19.125S157.438,249.75,168,249.75c10.563,0,19.125,8.566,19.125,19.125 S178.563,288,168,288z M210.375,276c-10.563,0-19.125-8.566-19.125-19.125s8.563-19.125,19.125-19.125 c10.563,0,19.125,8.566,19.125,19.125S220.938,276,210.375,276z M243.375,210.75c10.563,0,19.125,8.566,19.125,19.125 S253.938,249,243.375,249c-10.563,0-19.125-8.566-19.125-19.125S232.813,210.75,243.375,210.75z"/>
</g>
</svg>
</div>
</div>
<div class="openwebrx-panel-line">
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
<div title="Auto-adjust waterfall colors" id="openwebrx-waterfall-colors-auto" class="openwebrx-button" onclick="waterfall_measure_minmax_now=true;"><img src="static/gfx/openwebrx-waterfall-auto.png" class="openwebrx-sliderbtn-img"></div>
<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" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()">
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
</div>
<div class="openwebrx-panel-line">
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInOneStep();" title="Zoom in one step"> <img src="static/gfx/openwebrx-zoom-in.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutOneStep();" title="Zoom out one step"> <img src="static/gfx/openwebrx-zoom-out.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInTotal();" title="Zoom in totally"><img src="static/gfx/openwebrx-zoom-in-total.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutTotal();" title="Zoom out totally"><img src="static/gfx/openwebrx-zoom-out-total.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="mathbox_toggle();" title="Toggle 3D view"><img src="static/gfx/openwebrx-3d-spectrum.png" /></div>
<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>
</div>
</div>
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" data-panel-pos="left" data-panel-order="1" data-panel-size="619,137">
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
<div class="nano-content">
<div id="openwebrx-client-log-title">OpenWebRX client log</strong><span id="openwebrx-problems"></span></div>
<span id="openwebrx-client-1">Author: </span><a href="http://blog.sdr.hu/about" target="_blank">András Retzler, HA7ILM</a><br />You can support OpenWebRX development via <a href="http://blog.sdr.hu/support" target="_blank">PayPal!</a><br/>
<div id="openwebrx-debugdiv"></div>
</div>
</div>
</div>
<div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" data-panel-pos="left" data-panel-order="0" data-panel-size="615,50" data-panel-transparent="true">
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer"> <span class="openwebrx-progressbar-text">Audio buffer [0 ms]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output"> <span class="openwebrx-progressbar-text">Audio output [0 sps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed"> <span class="openwebrx-progressbar-text">Audio stream [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed"> <span class="openwebrx-progressbar-text">Network usage [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div>
</div>
<div class="openwebrx-panel" data-panel-name="client-under-devel" data-panel-pos="left" data-panel-order="9" data-panel-size="245,55" style="background-color: Red;">
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
<br />We're working on the code right now, so the application might fail.
</div>
<div class="openwebrx-panel" id="openwebrx-panel-digimodes" data-panel-name="digimodes" data-panel-pos="left" data-panel-order="3" data-panel-size="619,210">
<div id="openwebrx-digimode-canvas-container">
<div id="openwebrx-digimode-select-channel"></div>
</div>
<div id="openwebrx-digimode-content-container">
<div class="gradient"></div>
<div id="openwebrx-digimode-content">
<span id="openwebrx-cursor-blink"></span>
</div>
</div>
</div>
<table class="openwebrx-panel" id="openwebrx-panel-wsjt-message" data-panel-name="wsjt-message" data-panel-pos="left" data-panel-order="2" data-panel-size="619,200">
<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" id="openwebrx-panel-packet-message" data-panel-name="aprs-message" data-panel-pos="left" data-panel-order="2" data-panel-size="619,200">
<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>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" data-panel-name="metadata-ysf" data-panel-pos="left" data-panel-order="2" data-panel-size="145,220">
<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>
</div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" data-panel-name="metadata-dmr" data-panel-pos="left" data-panel-order="2" data-panel-size="300,220">
<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>
</div>
</div>
</div>
</div>
<div id="openwebrx-big-grey" onclick="iosPlayButtonClick();">
<div id="openwebrx-play-button-text">
<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" />
<br /><br />Start OpenWebRX
</div>
</div>
<div id="openwebrx-dialog-bookmark" class="openwebrx-dialog" style="display:none;">
<form>
<div class="form-field">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required="required">
</div>
<div class="form-field">
<label for="frequency">Frequency:</label>
<input type="number" id="frequency" name="frequency">
</div>
<div class="form-field">
<label for="modulation">Modulation:</label>
<select name="modulation" id="modulation">
<option value="nfm">FM</option>
<option value="am">AM</option>
<option value="usb">USB</option>
<option value="lsb">LSB</option>
<option value="cw">CW</option>
<option value="dmr">DMR</option>
<option value="dstar">D-Star</option>
<option value="nxdn">NXDN</option>
<option value="ysf">YSF</option>
</select>
</div>
<div class="buttons">
<div class="openwebrx-button" data-action="cancel">Cancel</div>
<div class="openwebrx-button" data-action="submit">Ok</div>
</div>
<input type="submit" style="display:none;">
</form>
</div>
</body>
</html>

View File

@ -1,180 +0,0 @@
<!DOCTYPE HTML>
<!--
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>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<html>
<head>
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
<script type="text/javascript">
//Global variables
var client_id="%[CLIENT_ID]";
var ws_url="%[WS_URL]";
var rx_photo_height=%[RX_PHOTO_HEIGHT];
var audio_buffering_fill_to=%[AUDIO_BUFSIZE];
var starting_mod="%[START_MOD]";
var starting_offset_frequency = %[START_OFFSET_FREQ];
var waterfall_colors=%[WATERFALL_COLORS];
var waterfall_min_level_default=%[WATERFALL_MIN_LEVEL];
var waterfall_max_level_default=%[WATERFALL_MAX_LEVEL];
var waterfall_auto_level_margin=%[WATERFALL_AUTO_LEVEL_MARGIN];
var server_enable_digimodes=%[DIGIMODES_ENABLE];
var mathbox_waterfall_frequency_resolution=%[MATHBOX_WATERFALL_FRES];
var mathbox_waterfall_history_length=%[MATHBOX_WATERFALL_THIST];
var mathbox_waterfall_colors=%[MATHBOX_WATERFALL_COLORS];
</script>
<script src="sdr.js"></script>
<script src="mathbox-bundle.min.js"></script>
<script src="openwebrx.js"></script>
<script src="jquery-3.2.1.min.js"></script>
<script src="jquery.nanoscroller.js"></script>
<link rel="stylesheet" type="text/css" href="nanoscroller.css" />
<link rel="stylesheet" type="text/css" href="openwebrx.css" />
<meta charset="utf-8">
</head>
<body onload="openwebrx_init();">
<div id="webrx-page-container">
<div id="webrx-top-container">
<div id="webrx-top-photo-clip">
<img src="gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/>
<div id="webrx-rx-photo-title">%[RX_PHOTO_TITLE]</div>
<div id="webrx-rx-photo-desc">%[RX_PHOTO_DESC]</div>
</div>
<div id="webrx-top-bar-background" class="webrx-top-bar-parts"></div>
<div id="webrx-top-bar" class="webrx-top-bar-parts">
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
<img id="webrx-rx-avatar-background" src="gfx/openwebrx-avatar-background.png" onclick="toggle_rx_photo();"/>
<img id="webrx-rx-avatar" src="gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/>
<div id="webrx-rx-title" onclick="toggle_rx_photo();">%[RX_TITLE]</div>
<div id="webrx-rx-desc" onclick="toggle_rx_photo();">%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, <a href="https://www.google.hu/maps/place/%[RX_GPS]" target="_blank" onclick="dont_toggle_rx_photo();">[maps]</a></div>
<div id="openwebrx-rx-details-arrow">
<a id="openwebrx-rx-details-arrow-up" onclick="toggle_rx_photo();"><img src="gfx/openwebrx-rx-details-arrow-up.png" /></a>
<a id="openwebrx-rx-details-arrow-down" onclick="toggle_rx_photo();"><img src="gfx/openwebrx-rx-details-arrow.png" /></a>
</div>
<section id="openwebrx-main-buttons">
<ul>
<li onmouseup="toggle_panel('openwebrx-panel-status');"><img src="gfx/openwebrx-panel-status.png" /><br/>Status</li>
<li onmouseup="toggle_panel('openwebrx-panel-log');"><img src="gfx/openwebrx-panel-log.png" /><br/>Log</li>
<li onmouseup="toggle_panel('openwebrx-panel-receiver');"><img src="gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li>
</ul>
</section>
</div>
</div>
<div id="webrx-main-container">
<div id="openwebrx-scale-container">
<canvas id="openwebrx-scale-canvas" width="0" height="0"></canvas>
</div>
<div id="openwebrx-mathbox-container"> </div>
<div id="webrx-canvas-container">
<div id="openwebrx-phantom-canvas"></div>
<!-- add canvas here by javascript -->
</div>
<div id="openwebrx-panels-container">
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" data-panel-pos="right" data-panel-order="0" data-panel-size="259,115">
<div id="webrx-actual-freq">---.--- MHz</div>
<div id="webrx-mouse-freq">---.--- MHz</div>
<div class="openwebrx-panel-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
onclick="demodulator_analog_replace('nfm');">FM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-am"
onclick="demodulator_analog_replace('am');">AM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-lsb"
onclick="demodulator_analog_replace('lsb');">LSB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-usb"
onclick="demodulator_analog_replace('usb');">USB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
onclick="demodulator_analog_replace('cw');">CW</div>
</div>
<div class="openwebrx-panel-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dig" onclick="demodulator_digital_replace_last();">DIG</div>
<select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();">
<option value="none"></option>
<option value="bpsk31">BPSK31</option>
</select>
</div>
<div class="openwebrx-panel-line">
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
<div title="Auto-adjust waterfall colors" id="openwebrx-waterfall-colors-auto" class="openwebrx-button" onclick="waterfall_measure_minmax_now=true;"><img src="gfx/openwebrx-waterfall-auto.png" class="openwebrx-sliderbtn-img"></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" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()">
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
</div>
<div class="openwebrx-panel-line">
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInOneStep();" title="Zoom in one step"> <img src="gfx/openwebrx-zoom-in.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutOneStep();" title="Zoom out one step"> <img src="gfx/openwebrx-zoom-out.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInTotal();" title="Zoom in totally"><img src="gfx/openwebrx-zoom-in-total.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutTotal();" title="Zoom out totally"><img src="gfx/openwebrx-zoom-out-total.png" /></div>
<div class="openwebrx-button openwebrx-square-button" onclick="mathbox_toggle();" title="Toggle 3D view"><img src="gfx/openwebrx-3d-spectrum.png" /></div>
<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>
</div>
</div>
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" data-panel-pos="left" data-panel-order="1" data-panel-size="619,137">
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
<div class="nano-content">
<div id="openwebrx-client-log-title">OpenWebRX client log</strong><span id="openwebrx-problems"></span></div>
<span id="openwebrx-client-1">Author: </span><a href="http://blog.sdr.hu/about" target="_blank">András Retzler, HA7ILM</a><br />You can <a href="http://blog.sdr.hu/support" target="_blank">donate</a> to say thanks for former development (this is the final version).<br/>
<div id="openwebrx-debugdiv"></div>
</div>
</div>
</div>
<div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" data-panel-pos="left" data-panel-order="0" data-panel-size="615,50" data-panel-transparent="true">
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer"> <span class="openwebrx-progressbar-text">Audio buffer [0 ms]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output"> <span class="openwebrx-progressbar-text">Audio output [0 sps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed"> <span class="openwebrx-progressbar-text">Audio stream [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed"> <span class="openwebrx-progressbar-text">Network usage [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div>
</div>
<div class="openwebrx-panel" data-panel-name="client-under-devel" data-panel-pos="none" data-panel-order="0" data-panel-size="245,55" style="background-color: Red;">
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
<br />We're working on the code right now, so the application might fail.
</div>
<div class="openwebrx-panel" id="openwebrx-panel-digimodes" data-panel-name="digimodes" data-panel-pos="left" data-panel-order="2" data-panel-size="619,210">
<div id="openwebrx-digimode-canvas-container">
<div id="openwebrx-digimode-select-channel"></div>
</div>
<div id="openwebrx-digimode-content-container">
<div class="gradient"></div>
<div id="openwebrx-digimode-content">
<span id="openwebrx-cursor-blink"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="openwebrx-big-grey" onclick="iosPlayButtonClick();">
<div id="openwebrx-play-button-text">
<img id="openwebrx-play-button" src="gfx/openwebrx-play-button.png" />
<br /><br />Start OpenWebRX
</div>
</div>
</body>
</html>

91
htdocs/lib/AprsMarker.js Normal file
View File

@ -0,0 +1,91 @@
function AprsMarker() {}
AprsMarker.prototype = new google.maps.OverlayView();
AprsMarker.prototype.draw = function() {
var div = this.div;
var overlay = this.overlay;
if (!div || !overlay) return;
if (this.symbol) {
var tableId = this.symbol.table == '/' ? 0 : 1;
div.style.background = 'url(/aprs-symbols/aprs-symbols-24-' + tableId + '@2x.png)';
div.style['background-size'] = '384px 144px';
div.style['background-position-x'] = -(this.symbol.index % 16) * 24 + 'px';
div.style['background-position-y'] = -Math.floor(this.symbol.index / 16) * 24 + 'px';
}
if (this.course) {
if (this.course > 180) {
div.style.transform = 'scalex(-1) rotate(' + (270 - this.course) + 'deg)'
} else {
div.style.transform = 'rotate(' + (this.course - 90) + 'deg)';
}
} else {
div.style.transform = null;
}
if (this.symbol.table != '/' && this.symbol.table != '\\') {
overlay.style.display = 'block';
overlay.style['background-position-x'] = -(this.symbol.tableindex % 16) * 24 + 'px';
overlay.style['background-position-y'] = -Math.floor(this.symbol.tableindex / 16) * 24 + 'px';
} else {
overlay.style.display = 'none';
}
if (this.opacity) {
div.style.opacity = this.opacity;
} else {
div.style.opacity = null;
}
var point = this.getProjection().fromLatLngToDivPixel(this.position);
if (point) {
div.style.left = point.x - 12 + 'px';
div.style.top = point.y - 12 + 'px';
}
};
AprsMarker.prototype.setOptions = function(options) {
google.maps.OverlayView.prototype.setOptions.apply(this, arguments);
this.draw();
};
AprsMarker.prototype.onAdd = function() {
var div = this.div = document.createElement('div');
div.style.position = 'absolute';
div.style.cursor = 'pointer';
div.style.width = '24px';
div.style.height = '24px';
var overlay = this.overlay = document.createElement('div');
overlay.style.width = '24px';
overlay.style.height = '24px';
overlay.style.background = 'url(/aprs-symbols/aprs-symbols-24-2@2x.png)';
overlay.style['background-size'] = '384px 144px';
overlay.style.display = 'none';
div.appendChild(overlay);
var self = this;
google.maps.event.addDomListener(div, "click", function(event) {
event.stopPropagation();
google.maps.event.trigger(self, "click", event);
});
var panes = this.getPanes();
panes.overlayImage.appendChild(div);
};
AprsMarker.prototype.remove = function() {
if (this.div) {
this.div.parentNode.removeChild(this.div);
this.div = null;
}
};
AprsMarker.prototype.getAnchorPoint = function() {
return new google.maps.Point(0, -12);
};

58
htdocs/lib/chroma.min.js vendored Normal file

File diff suppressed because one or more lines are too long

143
htdocs/lib/nite-overlay.js Normal file
View File

@ -0,0 +1,143 @@
/* Nite v1.7
* A tiny library to create a night overlay over the map
* Author: Rossen Georgiev @ https://github.com/rossengeorgiev
* Requires: GMaps API 3
*/
var nite = {
map: null,
date: null,
sun_position: null,
earth_radius_meters: 6371008,
marker_twilight_civil: null,
marker_twilight_nautical: null,
marker_twilight_astronomical: null,
marker_night: null,
init: function(map) {
if(typeof google === 'undefined'
|| typeof google.maps === 'undefined') throw "Nite Overlay: no google.maps detected";
this.map = map;
this.sun_position = this.calculatePositionOfSun();
this.marker_twilight_civil = new google.maps.Circle({
map: this.map,
center: this.getShadowPosition(),
radius: this.getShadowRadiusFromAngle(0.566666),
fillColor: "#000",
fillOpacity: 0.1,
strokeOpacity: 0,
clickable: false,
editable: false
});
this.marker_twilight_nautical = new google.maps.Circle({
map: this.map,
center: this.getShadowPosition(),
radius: this.getShadowRadiusFromAngle(6),
fillColor: "#000",
fillOpacity: 0.1,
strokeOpacity: 0,
clickable: false,
editable: false
});
this.marker_twilight_astronomical = new google.maps.Circle({
map: this.map,
center: this.getShadowPosition(),
radius: this.getShadowRadiusFromAngle(12),
fillColor: "#000",
fillOpacity: 0.1,
strokeOpacity: 0,
clickable: false,
editable: false
});
this.marker_night = new google.maps.Circle({
map: this.map,
center: this.getShadowPosition(),
radius: this.getShadowRadiusFromAngle(18),
fillColor: "#000",
fillOpacity: 0.1,
strokeOpacity: 0,
clickable: false,
editable: false
});
},
getShadowRadiusFromAngle: function(angle) {
var shadow_radius = this.earth_radius_meters * Math.PI * 0.5;
var twilight_dist = ((this.earth_radius_meters * 2 * Math.PI) / 360) * angle;
return shadow_radius - twilight_dist;
},
getSunPosition: function() {
return this.sun_position;
},
getShadowPosition: function() {
return (this.sun_position) ? new google.maps.LatLng(-this.sun_position.lat(), this.sun_position.lng() + 180) : null;
},
refresh: function() {
if(!this.isVisible()) return;
this.sun_position = this.calculatePositionOfSun(this.date);
var shadow_position = this.getShadowPosition();
this.marker_twilight_civil.setCenter(shadow_position);
this.marker_twilight_nautical.setCenter(shadow_position);
this.marker_twilight_astronomical.setCenter(shadow_position);
this.marker_night.setCenter(shadow_position);
},
jday: function(date) {
return (date.getTime() / 86400000.0) + 2440587.5;
},
calculatePositionOfSun: function(date) {
date = (date instanceof Date) ? date : new Date();
var rad = 0.017453292519943295;
// based on NOAA solar calculations
var ms_past_midnight = ((date.getUTCHours() * 60 + date.getUTCMinutes()) * 60 + date.getUTCSeconds()) * 1000 + date.getUTCMilliseconds();
var jc = (this.jday(date) - 2451545)/36525;
var mean_long_sun = (280.46646+jc*(36000.76983+jc*0.0003032)) % 360;
var mean_anom_sun = 357.52911+jc*(35999.05029-0.0001537*jc);
var sun_eq = Math.sin(rad*mean_anom_sun)*(1.914602-jc*(0.004817+0.000014*jc))+Math.sin(rad*2*mean_anom_sun)*(0.019993-0.000101*jc)+Math.sin(rad*3*mean_anom_sun)*0.000289;
var sun_true_long = mean_long_sun + sun_eq;
var sun_app_long = sun_true_long - 0.00569 - 0.00478*Math.sin(rad*125.04-1934.136*jc);
var mean_obliq_ecliptic = 23+(26+((21.448-jc*(46.815+jc*(0.00059-jc*0.001813))))/60)/60;
var obliq_corr = mean_obliq_ecliptic + 0.00256*Math.cos(rad*125.04-1934.136*jc);
var lat = Math.asin(Math.sin(rad*obliq_corr)*Math.sin(rad*sun_app_long)) / rad;
var eccent = 0.016708634-jc*(0.000042037+0.0000001267*jc);
var y = Math.tan(rad*(obliq_corr/2))*Math.tan(rad*(obliq_corr/2));
var rq_of_time = 4*((y*Math.sin(2*rad*mean_long_sun)-2*eccent*Math.sin(rad*mean_anom_sun)+4*eccent*y*Math.sin(rad*mean_anom_sun)*Math.cos(2*rad*mean_long_sun)-0.5*y*y*Math.sin(4*rad*mean_long_sun)-1.25*eccent*eccent*Math.sin(2*rad*mean_anom_sun))/rad);
var true_solar_time_in_deg = ((ms_past_midnight+rq_of_time*60000) % 86400000) / 240000;
var lng = -((true_solar_time_in_deg < 0) ? true_solar_time_in_deg + 180 : true_solar_time_in_deg - 180);
return new google.maps.LatLng(lat, lng);
},
setDate: function(date) {
this.date = date;
this.refresh();
},
setMap: function(map) {
this.map = map;
this.marker_twilight_civil.setMap(this.map);
this.marker_twilight_nautical.setMap(this.map);
this.marker_twilight_astronomical.setMap(this.map);
this.marker_night.setMap(this.map);
},
show: function() {
this.marker_twilight_civil.setVisible(true);
this.marker_twilight_nautical.setVisible(true);
this.marker_twilight_astronomical.setVisible(true);
this.marker_night.setVisible(true);
this.refresh();
},
hide: function() {
this.marker_twilight_civil.setVisible(false);
this.marker_twilight_nautical.setVisible(false);
this.marker_twilight_astronomical.setVisible(false);
this.marker_night.setVisible(false);
},
isVisible: function() {
return this.marker_night.getVisible();
}
}

24
htdocs/map.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Map</title>
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/chroma.min.js"></script>
<script src="static/map.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<link rel="stylesheet" type="text/css" href="static/css/map.css" />
<meta charset="utf-8">
</head>
<body>
${header}
<div class="openwebrx-map"></div>
<div class="openwebrx-map-legend">
<h3>Colors</h3>
<select id="openwebrx-map-colormode">
<option value="byband" selected="selected">By Band</option>
<option value="bymode">By Mode</option>
</select>
<div class="content"></div>
</div>
</body>
</html>

356
htdocs/map.js Normal file
View File

@ -0,0 +1,356 @@
(function(){
var protocol = 'ws';
if (window.location.toString().startsWith('https://')) {
protocol = 'wss';
}
var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){
var s = v.split('=');
var r = {};
r[s[0]] = s.slice(1).join('=');
return r;
}).reduce(function(a, b){
return a.assign(b);
});
var expectedCallsign;
if (query.callsign) expectedCallsign = query.callsign;
var expectedLocator;
if (query.locator) expectedLocator = query.locator;
var ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/";
if (!("WebSocket" in window)) return;
var map;
var markers = {};
var rectangles = {};
var updateQueue = [];
// reasonable default; will be overriden by server
var retention_time = 2 * 60 * 60 * 1000;
var strokeOpacity = 0.8;
var fillOpacity = 0.35;
var colorKeys = {};
var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl');
var getColor = function(id){
if (!id) return "#000000";
if (!colorKeys[id]) {
var keys = Object.keys(colorKeys);
keys.push(id);
keys.sort();
var colors = colorScale.colors(keys.length);
colorKeys = {};
keys.forEach(function(key, index) {
colorKeys[key] = colors[index];
});
reColor();
updateLegend();
}
return colorKeys[id];
}
// when the color palette changes, update all grid squares with new color
var reColor = function() {
$.each(rectangles, function(_, r) {
var color = getColor(colorAccessor(r));
r.setOptions({
strokeColor: color,
fillColor: color
});
});
}
var colorMode = 'byband';
var colorAccessor = function(r) {
switch (colorMode) {
case 'byband':
return r.band;
case 'bymode':
return r.mode;
}
};
$(function(){
$('#openwebrx-map-colormode').on('change', function(){
colorMode = $(this).val();
colorKeys = {};
reColor();
updateLegend();
});
});
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>';
});
$(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>');
}
var processUpdates = function(updates) {
if (typeof(AprsMarker) == 'undefined') {
updateQueue = updateQueue.concat(updates);
return;
}
updates.forEach(function(update){
switch (update.location.type) {
case 'latlon':
var pos = new google.maps.LatLng(update.location.lat, update.location.lon);
var marker;
var markerClass = google.maps.Marker;
var aprsOptions = {}
if (update.location.symbol) {
markerClass = AprsMarker;
aprsOptions.symbol = update.location.symbol;
aprsOptions.course = update.location.course;
aprsOptions.speed = update.location.speed;
}
if (markers[update.callsign]) {
marker = markers[update.callsign];
} else {
marker = new markerClass();
marker.addListener('click', function(){
showMarkerInfoWindow(update.callsign, pos);
});
markers[update.callsign] = marker;
}
marker.setOptions($.extend({
position: pos,
map: map,
title: update.callsign
}, aprsOptions, getMarkerOpacityOptions(update.lastseen) ));
marker.lastseen = update.lastseen;
marker.mode = update.mode;
marker.band = update.band;
marker.comment = update.location.comment;
// TODO the trim should happen on the server side
if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
map.panTo(pos);
showMarkerInfoWindow(update.callsign, pos);
delete(expectedCallsign);
}
break;
case 'locator':
var loc = update.location.locator;
var lat = (loc.charCodeAt(1) - 65 - 9) * 10 + Number(loc[3]);
var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2;
var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1});
var rectangle;
// the accessor is designed to work on the rectangle... but it should work on the update object, too
var color = getColor(colorAccessor(update));
if (rectangles[update.callsign]) {
rectangle = rectangles[update.callsign];
} else {
rectangle = new google.maps.Rectangle();
rectangle.addListener('click', function(){
showLocatorInfoWindow(this.locator, this.center);
});
rectangles[update.callsign] = rectangle;
}
rectangle.setOptions($.extend({
strokeColor: color,
strokeWeight: 2,
fillColor: color,
map: map,
bounds:{
north: lat,
south: lat + 1,
west: lon,
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);
showLocatorInfoWindow(expectedLocator, center);
delete(expectedLocator);
}
break;
}
});
};
var clearMap = function(){
var reset = function(callsign, item) { item.setMap(); };
$.each(markers, reset);
$.each(rectangles, reset);
markers = {};
rectangles = {};
};
var reconnect_timeout = false;
var connect = function(){
var ws = new WebSocket(ws_url);
ws.onopen = function(){
ws.send("SERVER DE CLIENT client=map.js type=map");
reconnect_timeout = false
};
ws.onmessage = function(e){
if (typeof e.data != 'string') {
console.error("unsupported binary data on websocket; ignoring");
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[0],
lng: config.receiver_gps[1]
},
zoom: 5
});
$.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 = [];
});
map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($(".openwebrx-map-legend")[0]);
});
retention_time = config.map_position_retention_time * 1000;
break;
case "update":
processUpdates(json.value);
break;
}
} catch (e) {
// don't lose exception
console.error(e);
}
};
ws.onclose = function(){
clearMap();
if (reconnect_timeout) {
// max value: roundabout 8 and a half minutes
reconnect_timeout = Math.min(reconnect_timeout * 2, 512000);
} else {
// initial value: 1s
reconnect_timeout = 1000;
}
setTimeout(connect, reconnect_timeout);
};
window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript
ws.onclose = function () {};
ws.close();
};
/*
ws.onerror = function(){
console.info("websocket error");
};
*/
};
connect();
var infowindow;
var showLocatorInfoWindow = function(locator, pos) {
if (!infowindow) infowindow = new google.maps.InfoWindow();
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) {
return d.locator == locator;
}).sort(function(a, b){
return b.lastseen - a.lastseen;
});
infowindow.setContent(
'<h3>Locator: ' + locator + '</h3>' +
'<div>Active Callsigns:</div>' +
'<ul>' +
inLocator.map(function(i){
var timestring = moment(i.lastseen).fromNow();
var message = i.callsign + ' (' + timestring + ' using ' + i.mode;
if (i.band) message += ' on ' + i.band;
message += ')';
return '<li>' + message + '</li>'
}).join("") +
'</ul>'
);
infowindow.setPosition(pos);
infowindow.open(map);
};
var showMarkerInfoWindow = function(callsign, pos) {
if (!infowindow) infowindow = new google.maps.InfoWindow();
var marker = markers[callsign];
var timestring = moment(marker.lastseen).fromNow();
var commentString = "";
if (marker.comment) {
commentString = '<div>' + marker.comment + '</div>';
}
infowindow.setContent(
'<h3>' + callsign + '</h3>' +
'<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' +
commentString
);
infowindow.open(map, marker);
}
var getScale = function(lastseen) {
var age = new Date().getTime() - lastseen;
var scale = 1;
if (age >= retention_time / 2) {
scale = (retention_time - age) / (retention_time / 2);
}
return Math.max(0, Math.min(1, scale));
};
var getRectangleOpacityOptions = function(lastseen) {
var scale = getScale(lastseen);
return {
strokeOpacity: strokeOpacity * scale,
fillOpacity: fillOpacity * scale
};
};
var getMarkerOpacityOptions = function(lastseen) {
var scale = getScale(lastseen);
return {
opacity: scale
};
};
// fade out / remove positions after time
setInterval(function(){
var now = new Date().getTime();
$.each(rectangles, function(callsign, m) {
var age = now - m.lastseen;
if (age > retention_time) {
delete rectangles[callsign];
m.setMap();
return;
}
m.setOptions(getRectangleOpacityOptions(m.lastseen));
});
$.each(markers, function(callsign, m) {
var age = now - m.lastseen;
if (age > retention_time) {
delete markers[callsign];
m.setMap();
return;
}
m.setOptions(getMarkerOpacityOptions(m.lastseen));
});
}, 1000);
})();

File diff suppressed because it is too large Load Diff

View File

@ -1,94 +0,0 @@
<html>
<!--
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>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
html, body
{
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
width: 100%;
text-align: center;
margin: 0;
padding: 0;
}
img.logo
{
margin-top: 120px;
}
div.frame
{
text-align: left;
margin:0px auto;
width: 800px;
}
div.panel
{
text-align: center;
background-color:#777777;
border-radius: 15px;
padding: 12px;
font-weight: bold;
color: White;
font-size: 13pt;
/*text-shadow: 1px 1px 4px #444;*/
font-family: sans;
}
div.alt
{
font-size: 10pt;
padding-top: 10px;
}
body div a
{
color: #5ca8ff;
text-shadow: none;
}
span.browser
{
}
</style>
<script>
var irt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c>="a"?97:65)<=(c=c.charCodeAt(0)-n)?c:c+26);});}
var sendmail2 = function (s) { window.location.href="mailto:"+irt(s.replace("=",String.fromCharCode(0100)).replace("$","."),8); }
window.addEventListener("load",function(){rs=document.getElementById("reconnect-secs"); rt=document.getElementById("reconnect-text"); cnt=29;window.setInterval(function(){if(cnt<=-1) window.location.href=window.location.href.split("retry.")[0]; else if(cnt==0) {rt.innerHTML="Reconnecting..."; cnt--;} else rs.innerHTML=(cnt--).toString();},1000);},false);
</script>
</head>
<body>
<div class="frame">
<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/>
<div class="panel">
There are no client slots left on this server.
<div class="alt">
Please wait until a client disconnects.<br /><span id="reconnect-text">We will try to reconnect in <span id="reconnect-secs">30</span> seconds...</span>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,95 +0,0 @@
<html>
<!--
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>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
html, body
{
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
width: 100%;
text-align: center;
margin: 0;
padding: 0;
}
img.logo
{
margin-top: 120px;
}
div.frame
{
text-align: left;
margin:0px auto;
width: 800px;
}
div.panel
{
text-align: center;
background-color:#777777;
border-radius: 15px;
padding: 12px;
font-weight: bold;
color: White;
font-size: 13pt;
/*text-shadow: 1px 1px 4px #444;*/
font-family: sans;
}
div.alt
{
font-size: 10pt;
padding-top: 10px;
}
body div a
{
color: #5ca8ff;
text-shadow: none;
}
span.browser
{
}
</style>
<script>
var irt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c>="a"?97:65)<=(c=c.charCodeAt(0)-n)?c:c+26);});}
var sendmail2 = function (s) { window.location.href="mailto:"+irt(s.replace("=",String.fromCharCode(0100)).replace("$","."),8); }
</script>
</head>
<body>
<div class="frame">
<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/>
<div class="panel">
Only the latest <span class="browser">Google Chrome</span> browser is supported at the moment.<br/>
Please <a href="http://chrome.google.com/">download and install Google Chrome.</a><br />
<div class="alt">
Alternatively, you may proceed to OpenWebRX, but it's not supposed to work as expected. <br />
<a href="/?unsupported">Click here</a> if you still want to try OpenWebRX.</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,739 +1,62 @@
#!/usr/bin/python2 #!/usr/bin/env python3
print "" # python2.7 is required to run OpenWebRX instead of python3. Please run me by: python2 openwebrx.py
"""
This file is part of OpenWebRX, from http.server import HTTPServer
an open-source SDR receiver software with a web UI. from owrx.http import RequestHandler
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> from owrx.config import PropertyManager
from owrx.feature import FeatureDetector
from owrx.source import SdrService
from socketserver import ThreadingMixIn
from owrx.sdrhu import SdrHuUpdater
from owrx.service import Services
from owrx.websocket import WebSocketConnection
This program is free software: you can redistribute it and/or modify import logging
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
""" class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
sw_version="v0.17"
#0.15 (added nmux)
import os
import code
import importlib
import csdr
import thread
import time
import datetime
import subprocess
import os
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from SocketServer import ThreadingMixIn
import fcntl
import time
import md5
import random
import threading
import sys
import traceback
from collections import namedtuple
import Queue
import ctypes
#import rtl_mus
import rxws
import uuid
import signal
import socket
try: import sdrhu
except: sdrhu=False
avatar_ctime=""
#pypy compatibility
try: import dl
except: pass
try: import __pypy__
except: pass
pypy="__pypy__" in globals()
"""
def import_all_plugins(directory):
for subdir in os.listdir(directory):
if os.path.isdir(directory+subdir) and not subdir[0]=="_":
exact_path=directory+subdir+"/plugin.py"
if os.path.isfile(exact_path):
importname=(directory+subdir+"/plugin").replace("/",".")
print "[openwebrx-import] Found plugin:",importname
importlib.import_module(importname)
"""
class MultiThreadHTTPServer(ThreadingMixIn, HTTPServer):
pass pass
def handle_signal(sig, frame):
global spectrum_dsp
if sig == signal.SIGUSR1:
print "[openwebrx] Verbose status information on USR1 signal"
print
print "time.time() =", time.time()
print "clients_mutex.locked() =", clients_mutex.locked()
print "clients_mutex_locker =", clients_mutex_locker
if server_fail: print "server_fail = ", server_fail
print "spectrum_thread_watchdog_last_tick =", spectrum_thread_watchdog_last_tick
print
print "clients:",len(clients)
for client in clients:
print
for key in client._fields:
print "\t%s = %s"%(key,str(getattr(client,key)))
elif sig == signal.SIGUSR2:
code.interact(local=globals())
else:
print "[openwebrx] Ctrl+C: aborting."
cleanup_clients(True)
spectrum_dsp.stop()
os._exit(1) #not too graceful exit
def access_log(data):
global logs
logs.access_log.write("["+datetime.datetime.now().isoformat()+"] "+data+"\n")
logs.access_log.flush()
receiver_failed=spectrum_thread_watchdog_last_tick=rtl_thread=spectrum_dsp=server_fail=None
def main(): def main():
global clients, clients_mutex, pypy, lock_try_time, avatar_ctime, cfg, logs print(
global serverfail, rtl_thread """
print
print "OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package"
print "_________________________________________________________________________________________________"
print
print "Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>"
print
no_arguments=len(sys.argv)==1 OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package
if no_arguments: print "[openwebrx-main] Configuration script not specified. I will use: \"config_webrx.py\"" _________________________________________________________________________________________________
cfg=__import__("config_webrx" if no_arguments else sys.argv[1])
for option in ("access_log","csdr_dynamic_bufsize","csdr_print_bufsizes","csdr_through"):
if not option in dir(cfg): setattr(cfg, option, False) #initialize optional config parameters
#Open log files Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>
logs = type("logs_class", (object,), {"access_log":open(cfg.access_log if cfg.access_log else "/dev/null","a"), "error_log":""})()
#Set signal handler """
signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python
signal.signal(signal.SIGUSR1, handle_signal)
signal.signal(signal.SIGUSR2, handle_signal)
#Pypy
if pypy: print "pypy detected (and now something completely different: c code is expected to run at a speed of 3*10^8 m/s?)"
#Change process name to "openwebrx" (to be seen in ps)
try:
for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]:
if os.path.exists(libcpath):
libc = dl.open(libcpath)
libc.call("prctl", 15, "openwebrx", 0, 0, 0)
break
except:
pass
#Start rtl thread
if os.system("csdr 2> /dev/null") == 32512: #check for csdr
print "[openwebrx-main] You need to install \"csdr\" to run OpenWebRX!\n"
return
if os.system("nmux --help 2> /dev/null") == 32512: #check for nmux
print "[openwebrx-main] You need to install an up-to-date version of \"csdr\" that contains the \"nmux\" tool to run OpenWebRX! Please upgrade \"csdr\"!\n"
return
if cfg.start_rtl_thread:
nmux_bufcnt = nmux_bufsize = 0
while nmux_bufsize < cfg.samp_rate/4: nmux_bufsize += 4096
while nmux_bufsize * nmux_bufcnt < cfg.nmux_memory * 1e6: nmux_bufcnt += 1
if nmux_bufcnt == 0 or nmux_bufsize == 0:
print "[openwebrx-main] Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
return
print "[openwebrx-main] nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)
cfg.start_rtl_command += "| nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, cfg.iq_server_port)
rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=())
rtl_thread.start()
print "[openwebrx-main] Started rtl_thread: "+cfg.start_rtl_command
print "[openwebrx-main] Waiting for I/Q server to start..."
while True:
testsock=socket.socket()
try: testsock.connect(("127.0.0.1", cfg.iq_server_port))
except:
time.sleep(0.1)
continue
testsock.close()
break
print "[openwebrx-main] I/Q server started."
#Initialize clients
clients=[]
clients_mutex=threading.Lock()
lock_try_time=0
#Start watchdog thread
print "[openwebrx-main] Starting watchdog threads."
mutex_test_thread=threading.Thread(target = mutex_test_thread_function, args = ())
mutex_test_thread.start()
mutex_watchdog_thread=threading.Thread(target = mutex_watchdog_thread_function, args = ())
mutex_watchdog_thread.start()
#Start spectrum thread
print "[openwebrx-main] Starting spectrum thread."
spectrum_thread=threading.Thread(target = spectrum_thread_function, args = ())
spectrum_thread.start()
#spectrum_watchdog_thread=threading.Thread(target = spectrum_watchdog_thread_function, args = ())
#spectrum_watchdog_thread.start()
get_cpu_usage()
bcastmsg_thread=threading.Thread(target = bcastmsg_thread_function, args = ())
bcastmsg_thread.start()
#threading.Thread(target = measure_thread_function, args = ()).start()
#Start sdr.hu update thread
if sdrhu and cfg.sdrhu_key and cfg.sdrhu_public_listing:
print "[openwebrx-main] Starting sdr.hu update thread..."
avatar_ctime=str(os.path.getctime("htdocs/gfx/openwebrx-avatar.png"))
sdrhu_thread=threading.Thread(target = sdrhu.run, args = ())
sdrhu_thread.start()
#Start HTTP thread
httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler)
print('[openwebrx-main] Starting HTTP server.')
access_log("Starting OpenWebRX...")
httpd.serve_forever()
# This is a debug function below:
measure_value=0
def measure_thread_function():
global measure_value
while True:
print "[openwebrx-measure] value is",measure_value
measure_value=0
time.sleep(1)
def bcastmsg_thread_function():
global clients
while True:
time.sleep(3)
try: cpu_usage=get_cpu_usage()
except: cpu_usage=0
cma("bcastmsg_thread")
for i in range(0,len(clients)):
clients[i].bcastmsg="MSG cpu_usage={0} clients={1}".format(int(cpu_usage*100),len(clients))
cmr()
def mutex_test_thread_function():
global clients_mutex, lock_try_time
while True:
time.sleep(0.5)
lock_try_time=time.time()
clients_mutex.acquire()
clients_mutex.release()
lock_try_time=0
def cma(what): #clients_mutex acquire
global clients_mutex
global clients_mutex_locker
if not clients_mutex.locked(): clients_mutex_locker = what
clients_mutex.acquire()
def cmr():
global clients_mutex
global clients_mutex_locker
clients_mutex_locker = None
clients_mutex.release()
def mutex_watchdog_thread_function():
global lock_try_time
global clients_mutex_locker
global clients_mutex
while True:
if lock_try_time != 0 and time.time()-lock_try_time > 3.0:
#if 3 seconds pass without unlock
print "[openwebrx-mutex-watchdog] Mutex unlock timeout. Locker: \""+str(clients_mutex_locker)+"\" Now unlocking..."
clients_mutex.release()
time.sleep(0.5)
def spectrum_watchdog_thread_function():
global spectrum_thread_watchdog_last_tick, receiver_failed
while True:
time.sleep(60)
if spectrum_thread_watchdog_last_tick and time.time()-spectrum_thread_watchdog_last_tick > 60.0:
print "[openwebrx-spectrum-watchdog] Spectrum timeout. Seems like no I/Q data is coming from the receiver.\nIf you're using RTL-SDR, the receiver hardware may randomly fail under some circumstances:\n1) high temperature,\n2) insufficient current available from the USB port."
print "[openwebrx-spectrum-watchdog] Deactivating receiver."
receiver_failed="spectrum"
return
def check_server():
global spectrum_dsp, server_fail, rtl_thread
if server_fail: return server_fail
#print spectrum_dsp.process.poll()
if spectrum_dsp and spectrum_dsp.process.poll()!=None: server_fail = "spectrum_thread dsp subprocess failed"
#if rtl_thread and not rtl_thread.is_alive(): server_fail = "rtl_thread failed"
if server_fail: print "[openwebrx-check_server] >>>>>>> ERROR:", server_fail
return server_fail
def apply_csdr_cfg_to_dsp(dsp):
dsp.csdr_dynamic_bufsize = cfg.csdr_dynamic_bufsize
dsp.csdr_print_bufsizes = cfg.csdr_print_bufsizes
dsp.csdr_through = cfg.csdr_through
def spectrum_thread_function():
global clients, spectrum_dsp, spectrum_thread_watchdog_last_tick
spectrum_dsp=dsp=csdr.dsp()
dsp.nc_port=cfg.iq_server_port
dsp.set_demodulator("fft")
dsp.set_samp_rate(cfg.samp_rate)
dsp.set_fft_size(cfg.fft_size)
dsp.set_fft_fps(cfg.fft_fps)
dsp.set_fft_averages(int(round(1.0 * cfg.samp_rate / cfg.fft_size / cfg.fft_fps / (1.0 - cfg.fft_voverlap_factor))) if cfg.fft_voverlap_factor>0 else 0)
dsp.set_fft_compression(cfg.fft_compression)
dsp.set_format_conversion(cfg.format_conversion)
apply_csdr_cfg_to_dsp(dsp)
sleep_sec=0.87/cfg.fft_fps
print "[openwebrx-spectrum] Spectrum thread initialized successfully."
dsp.start()
if cfg.csdr_dynamic_bufsize:
dsp.read(8) #dummy read to skip bufsize & preamble
print "[openwebrx-spectrum] Note: CSDR_DYNAMIC_BUFSIZE_ON = 1"
print "[openwebrx-spectrum] Spectrum thread started."
bytes_to_read=int(dsp.get_fft_bytes_to_read())
spectrum_thread_counter=0
while True:
data=dsp.read(bytes_to_read)
#print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()"
if spectrum_thread_counter >= cfg.fft_fps:
spectrum_thread_counter=0
spectrum_thread_watchdog_last_tick = time.time() #once every second
else: spectrum_thread_counter+=1
cma("spectrum_thread")
correction=0
for i in range(0,len(clients)):
i-=correction
if (clients[i].ws_started):
if clients[i].spectrum_queue.full():
print "[openwebrx-spectrum] client spectrum queue full, closing it."
close_client(i, False)
correction+=1
else:
clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients
cmr()
def get_client_by_id(client_id, use_mutex=True):
global clients
output=-1
if use_mutex: cma("get_client_by_id")
for i in range(0,len(clients)):
if(clients[i].id==client_id):
output=i
break
if use_mutex: cmr()
if output==-1:
raise ClientNotFoundException
else:
return output
def log_client(client, what):
print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what)
def cleanup_clients(end_all=False):
# - if a client doesn't open websocket for too long time, we drop it
# - or if end_all is true, we drop all clients
global clients
cma("cleanup_clients")
correction=0
for i in range(0,len(clients)):
i-=correction
#print "cleanup_clients:: len(clients)=", len(clients), "i=", i
if end_all or ((not clients[i].ws_started) and (time.time()-clients[i].gen_time)>45):
if not end_all: print "[openwebrx] cleanup_clients :: client timeout to open WebSocket"
close_client(i, False)
correction+=1
cmr()
def generate_client_id(ip):
#add a client
global clients
new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip closed bcastmsg dsp loopstat")
new_client.id=md5.md5(str(random.random())).hexdigest()
new_client.gen_time=time.time()
new_client.ws_started=False # to check whether client has ever tried to open the websocket
new_client.spectrum_queue=Queue.Queue(1000)
new_client.ip=ip
new_client.bcastmsg=""
new_client.closed=[False] #byref, not exactly sure if required
new_client.dsp=None
cma("generate_client_id")
clients.append(new_client)
log_client(new_client,"client added. Clients now: {0}".format(len(clients)))
cmr()
cleanup_clients()
return new_client.id
def close_client(i, use_mutex=True):
global clients
log_client(clients[i],"client being closed.")
if use_mutex: cma("close_client")
try:
clients[i].dsp.stop()
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
print "[openwebrx] close_client dsp.stop() :: error -",exc_type,exc_value
traceback.print_tb(exc_traceback)
clients[i].closed[0]=True
access_log("Stopped streaming to client: "+clients[i].ip+"#"+str(clients[i].id)+" (users now: "+str(len(clients)-1)+")")
del clients[i]
if use_mutex: cmr()
# http://www.codeproject.com/Articles/462525/Simple-HTTP-Server-and-Client-in-Python
# some ideas are used from the artice above
class WebRXHandler(BaseHTTPRequestHandler):
def proc_read_thread():
pass
def send_302(self,what):
self.send_response(302)
self.send_header('Content-type','text/html')
self.send_header("Location", "http://{0}:{1}/{2}".format(cfg.server_hostname,cfg.web_port,what))
self.end_headers()
self.wfile.write("<html><body><h1>Object moved</h1>Please <a href=\"/{0}\">click here</a> to continue.</body></html>".format(what))
def do_GET(self):
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
global dsp_plugin, clients_mutex, clients, avatar_ctime, sw_version, receiver_failed
rootdir = 'htdocs'
self.path=self.path.replace("..","")
path_temp_parts=self.path.split("?")
self.path=path_temp_parts[0]
request_param=path_temp_parts[1] if(len(path_temp_parts)>1) else ""
access_log("GET "+self.path+" from "+self.client_address[0])
try:
if self.path=="/":
self.path="/index.wrx"
# there's even another cool tip at http://stackoverflow.com/questions/4419650/how-to-implement-timeout-in-basehttpserver-basehttprequesthandler-python
#if self.path[:5]=="/lock": cma("do_GET /lock/") # to test mutex_watchdog_thread. Do not uncomment in production environment!
if self.path[:4]=="/ws/":
print "[openwebrx-ws] Client requested WebSocket connection"
if receiver_failed: self.send_error(500,"Internal server error")
try:
# ========= WebSocket handshake =========
ws_success=True
try:
rxws.handshake(self)
cma("do_GET /ws/")
client_i=get_client_by_id(self.path[4:], False)
myclient=clients[client_i]
except rxws.WebSocketException: ws_success=False
except ClientNotFoundException: ws_success=False
finally:
if clients_mutex.locked(): cmr()
if not ws_success:
self.send_error(400, 'Bad request.')
return
# ========= Client handshake =========
if myclient.ws_started:
print "[openwebrx-httpd] error: second WS connection with the same client id, throwing it."
self.send_error(400, 'Bad request.') #client already started
return
rxws.send(self, "CLIENT DE SERVER openwebrx.py")
client_ans=rxws.recv(self, True)
if client_ans[:16]!="SERVER DE CLIENT":
rxws.send("ERR Bad answer.")
return
myclient.ws_started=True
#send default parameters
rxws.send(self, "MSG center_freq={0} bandwidth={1} fft_size={2} fft_fps={3} audio_compression={4} fft_compression={5} max_clients={6} setup".format(str(cfg.shown_center_freq),str(cfg.samp_rate),cfg.fft_size,cfg.fft_fps,cfg.audio_compression,cfg.fft_compression,cfg.max_clients))
# ========= Initialize DSP =========
dsp=csdr.dsp()
dsp_initialized=False
dsp.set_audio_compression(cfg.audio_compression)
dsp.set_fft_compression(cfg.fft_compression) #used by secondary chains
dsp.set_format_conversion(cfg.format_conversion)
dsp.set_offset_freq(0)
dsp.set_bpf(-4000,4000)
dsp.set_secondary_fft_size(cfg.digimodes_fft_size)
dsp.nc_port=cfg.iq_server_port
apply_csdr_cfg_to_dsp(dsp)
myclient.dsp=dsp
do_secondary_demod=False
access_log("Started streaming to client: "+self.client_address[0]+"#"+myclient.id+" (users now: "+str(len(clients))+")")
while True:
myclient.loopstat=0
if myclient.closed[0]:
print "[openwebrx-httpd:ws] client closed by other thread"
break
# ========= send audio =========
if dsp_initialized:
myclient.loopstat=10
temp_audio_data=dsp.read(256)
myclient.loopstat=11
rxws.send(self, temp_audio_data, "AUD ")
# ========= send spectrum =========
while not myclient.spectrum_queue.empty():
myclient.loopstat=20
spectrum_data=myclient.spectrum_queue.get()
#spectrum_data_mid=len(spectrum_data[0])/2
#rxws.send(self, spectrum_data[0][spectrum_data_mid:]+spectrum_data[0][:spectrum_data_mid], "FFT ")
# (it seems GNU Radio exchanges the first and second part of the FFT output, we correct it)
myclient.loopstat=21
rxws.send(self, spectrum_data[0],"FFT ")
# ========= send smeter_level =========
smeter_level=None
while True:
try:
myclient.loopstat=30
smeter_level=dsp.get_smeter_level()
if smeter_level == None: break
except:
break
if smeter_level!=None:
myclient.loopstat=31
rxws.send(self, "MSG s={0}".format(smeter_level))
# ========= send bcastmsg =========
if myclient.bcastmsg!="":
myclient.loopstat=40
rxws.send(self,myclient.bcastmsg)
myclient.bcastmsg=""
# ========= send secondary =========
if do_secondary_demod:
myclient.loopstat=41
while True:
try:
secondary_spectrum_data=dsp.read_secondary_fft(dsp.get_secondary_fft_bytes_to_read())
if len(secondary_spectrum_data) == 0: break
# print "len(secondary_spectrum_data)", len(secondary_spectrum_data) #TODO digimodes
rxws.send(self, secondary_spectrum_data, "FFTS")
except: break
myclient.loopstat=42
while True:
try:
myclient.loopstat=422
secondary_demod_data=dsp.read_secondary_demod(1)
myclient.loopstat=423
if len(secondary_demod_data) == 0: break
# print "len(secondary_demod_data)", len(secondary_demod_data), secondary_demod_data #TODO digimodes
rxws.send(self, secondary_demod_data, "DAT ")
except: break
# ========= process commands =========
while True:
myclient.loopstat=50
rdata=rxws.recv(self, False)
myclient.loopstat=51
#try:
if not rdata: break
elif rdata[:3]=="SET":
print "[openwebrx-httpd:ws,%d] command: %s"%(client_i,rdata)
pairs=rdata[4:].split(" ")
bpf_set=False
new_bpf=dsp.get_bpf()
filter_limit=dsp.get_output_rate()/2
for pair in pairs:
param_name, param_value = pair.split("=")
if param_name == "low_cut" and -filter_limit <= int(param_value) <= filter_limit:
bpf_set=True
new_bpf[0]=int(param_value)
elif param_name == "high_cut" and -filter_limit <= int(param_value) <= filter_limit:
bpf_set=True
new_bpf[1]=int(param_value)
elif param_name == "offset_freq" and -cfg.samp_rate/2 <= int(param_value) <= cfg.samp_rate/2:
myclient.loopstat=510
dsp.set_offset_freq(int(param_value))
elif param_name == "squelch_level" and float(param_value) >= 0:
myclient.loopstat=520
dsp.set_squelch_level(float(param_value))
elif param_name=="mod":
if (dsp.get_demodulator()!=param_value):
myclient.loopstat=530
if dsp_initialized: dsp.stop()
dsp.set_demodulator(param_value)
if dsp_initialized: dsp.start()
elif param_name == "output_rate":
if not dsp_initialized:
myclient.loopstat=540
dsp.set_output_rate(int(param_value))
myclient.loopstat=541
dsp.set_samp_rate(cfg.samp_rate)
elif param_name=="action" and param_value=="start":
if not dsp_initialized:
myclient.loopstat=550
dsp.start()
dsp_initialized=True
elif param_name=="secondary_mod" and cfg.digimodes_enable:
if (dsp.get_secondary_demodulator() != param_value):
if dsp_initialized: dsp.stop()
if param_value == "off":
dsp.set_secondary_demodulator(None)
do_secondary_demod = False
else:
dsp.set_secondary_demodulator(param_value)
do_secondary_demod = True
rxws.send(self, "MSG secondary_fft_size={0} if_samp_rate={1} secondary_bw={2} secondary_setup".format(cfg.digimodes_fft_size, dsp.if_samp_rate(), dsp.secondary_bw()))
if dsp_initialized: dsp.start()
elif param_name=="secondary_offset_freq" and 0 <= int(param_value) <= dsp.if_samp_rate()/2 and cfg.digimodes_enable:
dsp.set_secondary_offset_freq(int(param_value))
else:
print "[openwebrx-httpd:ws] invalid parameter"
if bpf_set:
myclient.loopstat=560
dsp.set_bpf(*new_bpf)
#code.interact(local=locals())
except:
myclient.loopstat=990
exc_type, exc_value, exc_traceback = sys.exc_info()
print "[openwebrx-httpd:ws] exception: ",exc_type,exc_value
traceback.print_tb(exc_traceback) #TODO digimodes
#if exc_value[0]==32: #"broken pipe", client disconnected
# pass
#elif exc_value[0]==11: #"resource unavailable" on recv, client disconnected
# pass
#else:
# print "[openwebrx-httpd] error in /ws/ handler: ",exc_type,exc_value
# traceback.print_tb(exc_traceback)
#stop dsp for the disconnected client
myclient.loopstat=991
try:
dsp.stop()
del dsp
except:
print "[openwebrx-httpd] error in dsp.stop()"
#delete disconnected client
myclient.loopstat=992
try:
cma("do_GET /ws/ delete disconnected")
id_to_close=get_client_by_id(myclient.id,False)
close_client(id_to_close,False)
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
print "[openwebrx-httpd] client cannot be closed: ",exc_type,exc_value
traceback.print_tb(exc_traceback)
finally:
cmr()
myclient.loopstat=1000
return
elif self.path in ("/status", "/status/"):
#self.send_header('Content-type','text/plain')
self.send_response(200)
self.end_headers()
getbands=lambda: str(int(cfg.shown_center_freq-cfg.samp_rate/2))+"-"+str(int(cfg.shown_center_freq+cfg.samp_rate/2))
self.wfile.write("status="+("inactive" if receiver_failed else "active")+"\nname="+cfg.receiver_name+"\nsdr_hw="+cfg.receiver_device+"\nop_email="+cfg.receiver_admin+"\nbands="+getbands()+"\nusers="+str(len(clients))+"\nusers_max="+str(cfg.max_clients)+"\navatar_ctime="+avatar_ctime+"\ngps="+str(cfg.receiver_gps)+"\nasl="+str(cfg.receiver_asl)+"\nloc="+cfg.receiver_location+"\nsw_version="+sw_version+"\nantenna="+cfg.receiver_ant+"\n")
print "[openwebrx-httpd] GET /status/ from",self.client_address[0]
else:
f=open(rootdir+self.path)
data=f.read()
extension=self.path[(len(self.path)-4):len(self.path)]
extension=extension[2:] if extension[1]=='.' else extension[1:]
checkresult=check_server()
if extension == "wrx" and (checkresult or receiver_failed):
self.send_302("inactive.html")
return
anyStringsPresentInUserAgent=lambda a: reduce(lambda x,y:x or y, map(lambda b:self.headers['user-agent'].count(b), a), False)
if extension == "wrx" and ( (not anyStringsPresentInUserAgent(("Chrome","Firefox","Googlebot","iPhone","iPad","iPod"))) if 'user-agent' in self.headers.keys() else True ) and (not request_param.count("unsupported")):
self.send_302("upgrade.html")
return
if extension == "wrx":
cleanup_clients(False)
if cfg.max_clients<=len(clients):
self.send_302("retry.html")
return
self.send_response(200)
if(("wrx","html","htm").count(extension)):
self.send_header('Content-type','text/html')
elif(extension=="js"):
self.send_header('Content-type','text/javascript')
elif(extension=="css"):
self.send_header('Content-type','text/css')
self.end_headers()
if extension == "wrx":
replace_dictionary=(
("%[RX_PHOTO_DESC]",cfg.photo_desc),
("%[CLIENT_ID]", generate_client_id(self.client_address[0])) if "%[CLIENT_ID]" in data else "",
("%[WS_URL]","ws://"+cfg.server_hostname+":"+str(cfg.web_port)+"/ws/"),
("%[RX_TITLE]",cfg.receiver_name),
("%[RX_LOC]",cfg.receiver_location),
("%[RX_QRA]",cfg.receiver_qra),
("%[RX_ASL]",str(cfg.receiver_asl)),
("%[RX_GPS]",str(cfg.receiver_gps[0])+","+str(cfg.receiver_gps[1])),
("%[RX_PHOTO_HEIGHT]",str(cfg.photo_height)),("%[RX_PHOTO_TITLE]",cfg.photo_title),
("%[RX_ADMIN]",cfg.receiver_admin),
("%[RX_ANT]",cfg.receiver_ant),
("%[RX_DEVICE]",cfg.receiver_device),
("%[AUDIO_BUFSIZE]",str(cfg.client_audio_buffer_size)),
("%[START_OFFSET_FREQ]",str(cfg.start_freq-cfg.center_freq)),
("%[START_MOD]",cfg.start_mod),
("%[WATERFALL_COLORS]",cfg.waterfall_colors),
("%[WATERFALL_MIN_LEVEL]",str(cfg.waterfall_min_level)),
("%[WATERFALL_MAX_LEVEL]",str(cfg.waterfall_max_level)),
("%[WATERFALL_AUTO_LEVEL_MARGIN]","[%d,%d]"%cfg.waterfall_auto_level_margin),
("%[DIGIMODES_ENABLE]",("true" if cfg.digimodes_enable else "false")),
("%[MATHBOX_WATERFALL_FRES]",str(cfg.mathbox_waterfall_frequency_resolution)),
("%[MATHBOX_WATERFALL_THIST]",str(cfg.mathbox_waterfall_history_length)),
("%[MATHBOX_WATERFALL_COLORS]",cfg.mathbox_waterfall_colors)
) )
for rule in replace_dictionary:
while data.find(rule[0])!=-1: pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
data=data.replace(rule[0],rule[1])
self.wfile.write(data) featureDetector = FeatureDetector()
f.close() if not featureDetector.is_available("core"):
print(
"you are missing required dependencies to run openwebrx. "
"please check that the following core requirements are installed:"
)
print(", ".join(featureDetector.get_requirements("core")))
return return
except IOError:
self.send_error(404, 'Invalid path.')
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
print "[openwebrx-httpd] error (@outside):", exc_type, exc_value
traceback.print_tb(exc_traceback)
# Get error messages about unknown / unavailable features as soon as possible
SdrService.loadProps()
class ClientNotFoundException(Exception): if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
pass updater = SdrHuUpdater()
updater.start()
last_worktime=0 Services.start()
last_idletime=0
def get_cpu_usage(): server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler)
global last_worktime, last_idletime server.serve_forever()
try:
f=open("/proc/stat","r")
except:
return 0 #Workaround, possibly we're on a Mac
line=""
while not "cpu " in line: line=f.readline()
f.close()
spl=line.split(" ")
worktime=int(spl[2])+int(spl[3])+int(spl[4])
idletime=int(spl[5])
dworktime=(worktime-last_worktime)
didletime=(idletime-last_idletime)
rate=float(dworktime)/(didletime+dworktime)
last_worktime=worktime
last_idletime=idletime
if(last_worktime==0): return 0
return rate
if __name__ == "__main__": if __name__ == "__main__":
try:
main() main()
except KeyboardInterrupt:
WebSocketConnection.closeAll()

577
owrx/aprs.py Normal file
View File

@ -0,0 +1,577 @@
from owrx.kiss import KissDeframer
from owrx.map import Map, LatLngLocation
from owrx.bands import Bandplan
from owrx.metrics import Metrics, CounterMetric
from datetime import datetime, timezone
import re
import logging
logger = logging.getLogger(__name__)
# speed is in knots... convert to metric (km/h)
knotsToKilometers = 1.852
feetToMeters = 0.3048
milesToKilometers = 1.609344
inchesToMilimeters = 25.4
def fahrenheitToCelsius(f):
return (f - 32) * 5 / 9
# not sure what the correct encoding is. it seems TAPR has set utf-8 as a standard, but not everybody is following it.
encoding = "utf-8"
# regex for altitute in comment field
altitudeRegex = re.compile("(^.*)\\/A=([0-9]{6})(.*$)")
# regex for parsing third-party headers
thirdpartyeRegex = re.compile("^([a-zA-Z0-9-]+)>((([a-zA-Z0-9-]+\\*?,)*)([a-zA-Z0-9-]+\\*?)):(.*)$")
# regex for getting the message id out of message
messageIdRegex = re.compile("^(.*){([0-9]{1,5})$")
def decodeBase91(input):
base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0
return base + (ord(input[-1]) - 33)
def getSymbolData(symbol, table):
return {"symbol": symbol, "table": table, "index": ord(symbol) - 33, "tableindex": ord(table) - 33}
class Ax25Parser(object):
def parse(self, ax25frame):
control_pid = ax25frame.find(bytes([0x03, 0xF0]))
if control_pid % 7 > 0:
logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data")
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i : i + n]
return {
"destination": self.extractCallsign(ax25frame[0:7]),
"source": self.extractCallsign(ax25frame[7:14]),
"path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)],
"data": ax25frame[control_pid + 2 :],
}
def extractCallsign(self, input):
cs = bytes([b >> 1 for b in input[0:6]]).decode(encoding, "replace").strip()
ssid = (input[6] & 0b00011110) >> 1
if ssid > 0:
return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid)
else:
return cs
class WeatherMapping(object):
def __init__(self, char, key, length, scale=None):
self.char = char
self.key = key
self.length = length
self.scale = scale
def matches(self, input):
return self.char == input[0] and len(input) > self.length
def updateWeather(self, weather, input):
def deepApply(obj, key, v):
keys = key.split(".")
if len(keys) > 1:
if not keys[0] in obj:
obj[keys[0]] = {}
deepApply(obj[keys[0]], ".".join(keys[1:]), v)
else:
obj[key] = v
try:
value = int(input[1 : 1 + self.length])
if self.scale:
value = self.scale(value)
deepApply(weather, self.key, value)
except ValueError:
pass
remain = input[1 + self.length :]
return weather, remain
class WeatherParser(object):
mappings = [
WeatherMapping("c", "wind.direction", 3),
WeatherMapping("s", "wind.speed", 3, lambda x: x * milesToKilometers),
WeatherMapping("g", "wind.gust", 3, lambda x: x * milesToKilometers),
WeatherMapping("t", "temperature", 3, fahrenheitToCelsius),
WeatherMapping("r", "rain.hour", 3, lambda x: x / 100 * inchesToMilimeters),
WeatherMapping("p", "rain.day", 3, lambda x: x / 100 * inchesToMilimeters),
WeatherMapping("P", "rain.sincemidnight", 3, lambda x: x / 100 * inchesToMilimeters),
WeatherMapping("h", "humidity", 2),
WeatherMapping("b", "barometricpressure", 5, lambda x: x / 10),
WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4),
]
def __init__(self, data, weather={}):
self.data = data
self.weather = weather
def getWeather(self):
doWork = True
weather = self.weather
while doWork:
mapping = next((m for m in WeatherParser.mappings if m.matches(self.data)), None)
if mapping:
(weather, remain) = mapping.updateWeather(weather, self.data)
self.data = remain
doWork = len(self.data) > 0
else:
doWork = False
return weather
def getRemainder(self):
return self.data
class AprsLocation(LatLngLocation):
def __init__(self, data):
super().__init__(data["lat"], data["lon"])
self.data = data
def __dict__(self):
res = super(AprsLocation, self).__dict__()
for key in ["comment", "symbol", "course", "speed"]:
if key in self.data:
res[key] = self.data[key]
return res
class AprsParser(object):
def __init__(self, handler):
self.ax25parser = Ax25Parser()
self.deframer = KissDeframer()
self.dial_freq = None
self.band = None
self.handler = handler
self.metric = self.getMetric()
def setDialFrequency(self, freq):
self.dial_freq = freq
self.band = Bandplan.getSharedInstance().findBand(freq)
self.metric = self.getMetric()
def getMetric(self):
band = "unknown"
if self.band is not None:
band = self.band.getName()
name = "aprs.decodes.{band}.aprs".format(band=band)
metrics = Metrics.getSharedInstance()
metric = metrics.getMetric(name)
if metric is None:
metric = CounterMetric()
metrics.addMetric(name, metric)
return metric
def parse(self, raw):
for frame in self.deframer.parse(raw):
try:
data = self.ax25parser.parse(frame)
# TODO how can we tell if this is an APRS frame at all?
aprsData = self.parseAprsData(data)
logger.debug("decoded APRS data: %s", aprsData)
self.updateMap(aprsData)
self.metric.inc()
self.handler.write_aprs_data(aprsData)
except Exception:
logger.exception("exception while parsing aprs data")
def updateMap(self, mapData):
if "type" in mapData and mapData["type"] == "thirdparty" and "data" in mapData:
mapData = mapData["data"]
if "lat" in mapData and "lon" in mapData:
loc = AprsLocation(mapData)
source = mapData["source"]
if "type" in mapData:
if mapData["type"] == "item":
source = mapData["item"]
elif mapData["type"] == "object":
source = mapData["object"]
Map.getSharedInstance().updateLocation(source, loc, "APRS", self.band)
def hasCompressedCoordinates(self, raw):
return raw[0] == "/" or raw[0] == "\\"
def parseUncompressedCoordinates(self, raw):
lat = int(raw[0:2]) + float(raw[2:7]) / 60
if raw[7] == "S":
lat *= -1
lon = int(raw[9:12]) + float(raw[12:17]) / 60
if raw[17] == "W":
lon *= -1
return {"lat": lat, "lon": lon, "symbol": getSymbolData(raw[18], raw[8])}
def parseCompressedCoordinates(self, raw):
return {
"lat": 90 - decodeBase91(raw[1:5]) / 380926,
"lon": -180 + decodeBase91(raw[5:9]) / 190463,
"symbol": getSymbolData(raw[9], raw[0]),
}
def parseTimestamp(self, raw):
now = datetime.now()
if raw[6] == "h":
ts = datetime.strptime(raw[0:6], "%H%M%S")
ts = ts.replace(year=now.year, month=now.month, day=now.month, tzinfo=timezone.utc)
else:
ts = datetime.strptime(raw[0:6], "%d%H%M")
ts = ts.replace(year=now.year, month=now.month)
if raw[6] == "z":
ts = ts.replace(tzinfo=timezone.utc)
elif raw[6] == "/":
ts = ts.replace(tzinfo=now.tzinfo)
else:
logger.warning("invalid timezone info byte: %s", raw[6])
return int(ts.timestamp() * 1000)
def parseStatusUpate(self, raw):
res = {"type": "status"}
if raw[6] == "z":
res["timestamp"] = self.parseTimestamp(raw[0:7])
res["comment"] = raw[7:]
else:
res["comment"] = raw
return res
def parseAprsData(self, data):
information = data["data"]
# forward some of the ax25 data
aprsData = {"source": data["source"], "destination": data["destination"], "path": data["path"]}
if information[0] == 0x1C or information[0] == ord("`") or information[0] == ord("'"):
aprsData.update(MicEParser().parse(data))
return aprsData
information = information.decode(encoding, "replace")
# APRS data type identifier
dti = information[0]
if dti == "!" or dti == "=":
# position without timestamp
aprsData.update(self.parseRegularAprsData(information[1:]))
elif dti == "/" or dti == "@":
# position with timestamp
aprsData["timestamp"] = self.parseTimestamp(information[1:8])
aprsData.update(self.parseRegularAprsData(information[8:]))
elif dti == ">":
# status update
aprsData.update(self.parseStatusUpate(information[1:]))
elif dti == "}":
# third party
aprsData.update(self.parseThirdpartyAprsData(information[1:]))
elif dti == ":":
# message
aprsData.update(self.parseMessage(information[1:]))
elif dti == ";":
# object
aprsData.update(self.parseObject(information[1:]))
elif dti == ")":
# item
aprsData.update(self.parseItem(information[1:]))
return aprsData
def parseObject(self, information):
result = {"type": "object"}
if len(information) > 16:
result["object"] = information[0:9].strip()
result["live"] = information[9] == "*"
result["timestamp"] = self.parseTimestamp(information[10:17])
result.update(self.parseRegularAprsData(information[17:]))
# override type, losing information about compression
result["type"] = "object"
return result
def parseItem(self, information):
result = {"type": "item"}
if len(information) > 3:
indexes = [information[0:10].find(p) for p in ["!", "_"]]
filtered = [i for i in indexes if i >= 3]
filtered.sort()
if len(filtered):
index = filtered[0]
result["item"] = information[0:index]
result["live"] = information[index] == "!"
result.update(self.parseRegularAprsData(information[index + 1 :]))
# override type, losing information about compression
result["type"] = "item"
return result
def parseMessage(self, information):
result = {"type": "message"}
if len(information) > 9 and information[9] == ":":
result["adressee"] = information[0:9]
message = information[10:]
if len(message) > 3 and message[0:3] == "ack":
result["type"] = "messageacknowledgement"
result["messageid"] = int(message[3:8])
elif len(message) > 3 and message[0:3] == "rej":
result["type"] = "messagerejection"
result["messageid"] = int(message[3:8])
else:
matches = messageIdRegex.match(message)
if matches:
result["messageid"] = int(matches.group(2))
message = matches.group(1)
result["message"] = message
return result
def parseThirdpartyAprsData(self, information):
matches = thirdpartyeRegex.match(information)
if matches:
path = matches.group(2).split(",")
destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None)
data = self.parseAprsData(
{
"source": matches.group(1).upper(),
"destination": destination,
"path": path,
"data": matches.group(6).encode(encoding),
}
)
return {"type": "thirdparty", "data": data}
return {"type": "thirdparty"}
def parseRegularAprsData(self, information):
if self.hasCompressedCoordinates(information):
aprsData = self.parseCompressedCoordinates(information[0:10])
aprsData["type"] = "compressed"
if information[10] != " ":
if information[10] == "{":
# pre-calculated radio range
aprsData["range"] = 2 * 1.08 ** (ord(information[11]) - 33) * milesToKilometers
else:
aprsData["course"] = (ord(information[10]) - 33) * 4
# speed is in knots... convert to metric (km/h)
aprsData["speed"] = (1.08 ** (ord(information[11]) - 33) - 1) * knotsToKilometers
# compression type
t = ord(information[12])
aprsData["fix"] = (t & 0b00100000) > 0
sources = ["other", "GLL", "GGA", "RMC"]
aprsData["nmeasource"] = sources[(t & 0b00011000) >> 3]
origins = [
"Compressed",
"TNC BText",
"Software",
"[tbd]",
"KPC3",
"Pico",
"Other tracker",
"Digipeater conversion",
]
aprsData["compressionorigin"] = origins[t & 0b00000111]
comment = information[13:]
else:
aprsData = self.parseUncompressedCoordinates(information[0:19])
aprsData["type"] = "regular"
comment = information[19:]
def decodeHeightGainDirectivity(comment):
res = {"height": 2 ** int(comment[4]) * 10 * feetToMeters, "gain": int(comment[5])}
directivity = int(comment[6])
if directivity == 0:
res["directivity"] = "omni"
elif 0 < directivity < 9:
res["directivity"] = directivity * 45
return res
# aprs data extensions
# yes, weather stations are officially identified by their symbols. go figure...
if "symbol" in aprsData and aprsData["symbol"]["index"] == 62:
# weather report
weather = {}
if len(comment) > 6 and comment[3] == "/":
try:
weather["wind"] = {"direction": int(comment[0:3]), "speed": int(comment[4:7]) * milesToKilometers}
except ValueError:
pass
comment = comment[7:]
parser = WeatherParser(comment, weather)
aprsData["weather"] = parser.getWeather()
comment = parser.getRemainder()
elif len(comment) > 6:
if comment[3] == "/":
# course and speed
# for a weather report, this would be wind direction and speed
try:
aprsData["course"] = int(comment[0:3])
aprsData["speed"] = int(comment[4:7]) * knotsToKilometers
except ValueError:
pass
comment = comment[7:]
elif comment[0:3] == "PHG":
# station power and effective antenna height/gain/directivity
try:
powerCodes = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
aprsData["power"] = powerCodes[int(comment[3])]
aprsData.update(decodeHeightGainDirectivity(comment))
except ValueError:
pass
comment = comment[7:]
elif comment[0:3] == "RNG":
# pre-calculated radio range
try:
aprsData["range"] = int(comment[3:7]) * milesToKilometers
except ValueError:
pass
comment = comment[7:]
elif comment[0:3] == "DFS":
# direction finding signal strength and antenna height/gain
try:
aprsData["strength"] = int(comment[3])
aprsData.update(decodeHeightGainDirectivity(comment))
except ValueError:
pass
comment = comment[7:]
matches = altitudeRegex.match(comment)
if matches:
aprsData["altitude"] = int(matches.group(2)) * feetToMeters
comment = matches.group(1) + matches.group(3)
aprsData["comment"] = comment
return aprsData
class MicEParser(object):
def extractNumber(self, input):
n = ord(input)
if n >= ord("P"):
return n - ord("P")
if n >= ord("A"):
return n - ord("A")
return n - ord("0")
def listToNumber(self, input):
base = self.listToNumber(input[:-1]) * 10 if len(input) > 1 else 0
return base + input[-1]
def extractAltitude(self, comment):
if len(comment) < 4 or comment[3] != "}":
return (comment, None)
return comment[4:], decodeBase91(comment[:3]) - 10000
def extractDevice(self, comment):
if len(comment) > 0:
if comment[0] == ">":
if len(comment) > 1:
if comment[-1] == "=":
return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D72"}
if comment[-1] == "^":
return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D74"}
return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D7A"}
if comment[0] == "]":
if len(comment) > 1 and comment[-1] == "=":
return comment[1:-1], {"manufacturer": "Kenwood", "device": "TM-D710"}
return comment[1:], {"manufacturer": "Kenwood", "device": "TM-D700"}
if len(comment) > 2 and (comment[0] == "`" or comment[0] == "'"):
if comment[-2] == "_":
devices = {
"b": "VX-8",
'"': "FTM-350",
"#": "VX-8G",
"$": "FT1D",
"%": "FTM-400DR",
")": "FTM-100D",
"(": "FT2D",
"0": "FT3D",
}
return comment[1:-2], {"manufacturer": "Yaesu", "device": devices.get(comment[-1], "Unknown")}
if comment[-2:] == " X":
return comment[1:-2], {"manufacturer": "SainSonic", "device": "AP510"}
if comment[-2] == "(":
devices = {"5": "D578UV", "8": "D878UV"}
return comment[1:-2], {"manufacturer": "Anytone", "device": devices.get(comment[-1], "Unknown")}
if comment[-2] == "|":
devices = {"3": "TinyTrack3", "4": "TinyTrack4"}
return comment[1:-2], {"manufacturer": "Byonics", "device": devices.get(comment[-1], "Unknown")}
if comment[-2:] == "^v":
return comment[1:-2], {"manufacturer": "HinzTec", "device": "anyfrog"}
if comment[-2] == ":":
devices = {"4": "P4dragon DR-7400 modem", "8": "P4dragon DR-7800 modem"}
return (
comment[1:-2],
{"manufacturer": "SCS GmbH & Co.", "device": devices.get(comment[-1], "Unknown")},
)
if comment[-2:] == "~v":
return comment[1:-2], {"manufacturer": "Other", "device": "Other"}
return comment[1:-2], None
return comment, None
def parse(self, data):
information = data["data"]
destination = data["destination"]
rawLatitude = [self.extractNumber(c) for c in destination[0:6]]
lat = self.listToNumber(rawLatitude[0:2]) + self.listToNumber(rawLatitude[2:6]) / 6000
if ord(destination[3]) <= ord("9"):
lat *= -1
lon = information[1] - 28
if ord(destination[4]) >= ord("P"):
lon += 100
if 180 <= lon <= 189:
lon -= 80
if 190 <= lon <= 199:
lon -= 190
minutes = information[2] - 28
if minutes >= 60:
minutes -= 60
lon += minutes / 60 + (information[3] - 28) / 6000
if ord(destination[5]) >= ord("P"):
lon *= -1
speed = (information[4] - 28) * 10
dc28 = information[5] - 28
speed += int(dc28 / 10)
course = (dc28 % 10) * 100
course += information[6] - 28
if speed >= 800:
speed -= 800
if course >= 400:
course -= 400
# speed is in knots... convert to metric (km/h)
speed *= knotsToKilometers
comment = information[9:].decode(encoding, "replace").strip()
(comment, altitude) = self.extractAltitude(comment)
(comment, device) = self.extractDevice(comment)
# altitude might be inside the device string, so repeat and choose one
(comment, insideAltitude) = self.extractAltitude(comment)
altitude = next((a for a in [altitude, insideAltitude] if a is not None), None)
return {
"fix": information[0] == ord("`") or information[0] == 0x1C,
"lat": lat,
"lon": lon,
"comment": comment,
"altitude": altitude,
"speed": speed,
"course": course,
"device": device,
"type": "Mic-E",
"symbol": getSymbolData(chr(information[7]), chr(information[8])),
}

65
owrx/bands.py Normal file
View File

@ -0,0 +1,65 @@
import json
import logging
logger = logging.getLogger(__name__)
class Band(object):
def __init__(self, dict):
self.name = dict["name"]
self.lower_bound = dict["lower_bound"]
self.upper_bound = dict["upper_bound"]
self.frequencies = []
if "frequencies" in dict:
for (mode, freqs) in dict["frequencies"].items():
if not isinstance(freqs, list):
freqs = [freqs]
for f in freqs:
if not self.inBand(f):
logger.warning(
"Frequency for {mode} on {band} is not within band limits: {frequency}".format(
mode=mode, frequency=f, band=self.name
)
)
else:
self.frequencies.append({"mode": mode, "frequency": f})
def inBand(self, freq):
return self.lower_bound <= freq <= self.upper_bound
def getName(self):
return self.name
def getDialFrequencies(self, range):
(low, hi) = range
return [e for e in self.frequencies if low <= e["frequency"] <= hi]
class Bandplan(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if Bandplan.sharedInstance is None:
Bandplan.sharedInstance = Bandplan()
return Bandplan.sharedInstance
def __init__(self):
f = open("bands.json", "r")
bands_json = json.load(f)
f.close()
self.bands = [Band(d) for d in bands_json]
def findBands(self, freq):
return [band for band in self.bands if band.inBand(freq)]
def findBand(self, freq):
bands = self.findBands(freq)
if bands:
return bands[0]
else:
return None
def collectDialFrequencies(self, range):
return [e for b in self.bands for e in b.getDialFrequencies(range)]

43
owrx/bookmarks.py Normal file
View File

@ -0,0 +1,43 @@
import json
class Bookmark(object):
def __init__(self, j):
self.name = j["name"]
self.frequency = j["frequency"]
self.modulation = j["modulation"]
def getName(self):
return self.name
def getFrequency(self):
return self.frequency
def getModulation(self):
return self.modulation
def __dict__(self):
return {
"name": self.getName(),
"frequency": self.getFrequency(),
"modulation": self.getModulation(),
}
class Bookmarks(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if Bookmarks.sharedInstance is None:
Bookmarks.sharedInstance = Bookmarks()
return Bookmarks.sharedInstance
def __init__(self):
f = open("bookmarks.json", "r")
bookmarks_json = json.load(f)
f.close()
self.bookmarks = [Bookmark(d) for d in bookmarks_json]
def getBookmarks(self, range):
(lo, hi) = range
return [b for b in self.bookmarks if lo <= b.getFrequency() <= hi]

137
owrx/config.py Normal file
View File

@ -0,0 +1,137 @@
import logging
logger = logging.getLogger(__name__)
class Subscription(object):
def __init__(self, subscriptee, subscriber):
self.subscriptee = subscriptee
self.subscriber = subscriber
def call(self, *args, **kwargs):
self.subscriber(*args, **kwargs)
def cancel(self):
self.subscriptee.unwire(self)
class Property(object):
def __init__(self, value=None):
self.value = value
self.subscribers = []
def getValue(self):
return self.value
def setValue(self, value):
if self.value == value:
return self
self.value = value
for c in self.subscribers:
try:
c.call(self.value)
except Exception as e:
logger.exception(e)
return self
def wire(self, callback):
sub = Subscription(self, callback)
self.subscribers.append(sub)
if not self.value is None:
sub.call(self.value)
return sub
def unwire(self, sub):
try:
self.subscribers.remove(sub)
except ValueError:
# happens when already removed before
pass
return self
class PropertyManager(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if PropertyManager.sharedInstance is None:
PropertyManager.sharedInstance = PropertyManager()
return PropertyManager.sharedInstance
def collect(self, *props):
return PropertyManager(
{name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}
)
def __init__(self, properties=None):
self.properties = {}
self.subscribers = []
if properties is not None:
for (name, prop) in properties.items():
self.add(name, prop)
def add(self, name, prop):
self.properties[name] = prop
def fireCallbacks(value):
for c in self.subscribers:
try:
c.call(name, value)
except Exception as e:
logger.exception(e)
prop.wire(fireCallbacks)
return self
def __contains__(self, name):
return self.hasProperty(name)
def __getitem__(self, name):
return self.getPropertyValue(name)
def __setitem__(self, name, value):
if not self.hasProperty(name):
self.add(name, Property())
self.getProperty(name).setValue(value)
def __dict__(self):
return {k: v.getValue() for k, v in self.properties.items()}
def hasProperty(self, name):
return name in self.properties
def getProperty(self, name):
if not self.hasProperty(name):
self.add(name, Property())
return self.properties[name]
def getPropertyValue(self, name):
return self.getProperty(name).getValue()
def wire(self, callback):
sub = Subscription(self, callback)
self.subscribers.append(sub)
return sub
def unwire(self, sub):
try:
self.subscribers.remove(sub)
except ValueError:
# happens when already removed before
pass
return self
def defaults(self, other_pm):
for (key, p) in self.properties.items():
if p.getValue() is None:
p.setValue(other_pm[key])
return self
def loadConfig(self, filename):
cfg = __import__(filename)
for name, value in cfg.__dict__.items():
if name.startswith("__"):
continue
self[name] = value
return self

324
owrx/connection.py Normal file
View File

@ -0,0 +1,324 @@
from owrx.config import PropertyManager
from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry
from owrx.feature import FeatureDetector
from owrx.version import openwebrx_version
from owrx.bands import Bandplan
from owrx.bookmarks import Bookmarks
from owrx.map import Map
from multiprocessing import Queue
import json
import threading
import logging
logger = logging.getLogger(__name__)
class Client(object):
def __init__(self, conn):
self.conn = conn
self.multiprocessingPipe = Queue()
def mp_passthru():
run = True
while run:
try:
data = self.multiprocessingPipe.get()
self.send(data)
except (EOFError, OSError):
run = False
threading.Thread(target=mp_passthru).start()
def send(self, data):
self.conn.send(data)
def close(self):
self.conn.close()
self.multiprocessingPipe.close()
def mp_send(self, data):
self.multiprocessingPipe.put(data, block=False)
def handleTextMessage(self, conn, message):
pass
def handleBinaryMessage(self, conn, data):
logger.error("unsupported binary message, discarding")
def handleClose(self):
self.close()
class OpenWebRxReceiverClient(Client):
config_keys = [
"waterfall_colors",
"waterfall_min_level",
"waterfall_max_level",
"waterfall_auto_level_margin",
"lfo_offset",
"samp_rate",
"fft_size",
"fft_fps",
"audio_compression",
"fft_compression",
"max_clients",
"start_mod",
"client_audio_buffer_size",
"start_freq",
"center_freq",
"mathbox_waterfall_colors",
"mathbox_waterfall_history_length",
"mathbox_waterfall_frequency_resolution",
]
def __init__(self, conn):
super().__init__(conn)
self.dsp = None
self.sdr = None
self.configSub = None
ClientRegistry.getSharedInstance().addClient(self)
pm = PropertyManager.getSharedInstance()
self.setSdr()
# send receiver info
receiver_keys = [
"receiver_name",
"receiver_location",
"receiver_qra",
"receiver_asl",
"receiver_gps",
"photo_title",
"photo_desc",
]
receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys)
self.write_receiver_details(receiver_details)
profiles = [
{"name": s.getName() + " " + p["name"], "id": sid + "|" + pid}
for (sid, s) in SdrService.getSources().items()
for (pid, p) in s.getProfiles().items()
]
self.write_profiles(profiles)
features = FeatureDetector().feature_availability()
self.write_features(features)
CpuUsageThread.getSharedInstance().add_client(self)
def handleTextMessage(self, conn, message):
try:
message = json.loads(message)
if "type" in message:
if message["type"] == "dspcontrol":
if "action" in message and message["action"] == "start":
self.startDsp()
if "params" in message:
params = message["params"]
self.setDspProperties(params)
if message["type"] == "config":
if "params" in message:
self.setParams(message["params"])
if message["type"] == "setsdr":
if "params" in message:
self.setSdr(message["params"]["sdr"])
if message["type"] == "selectprofile":
if "params" in message and "profile" in message["params"]:
profile = message["params"]["profile"].split("|")
self.setSdr(profile[0])
self.sdr.activateProfile(profile[1])
else:
logger.warning("received message without type: {0}".format(message))
except json.JSONDecodeError:
logger.warning("message is not json: {0}".format(message))
def setSdr(self, id=None):
next = SdrService.getSource(id)
if next == self.sdr:
return
self.stopDsp()
if self.configSub is not None:
self.configSub.cancel()
self.configSub = None
self.sdr = next
# send initial config
configProps = (
self.sdr.getProps()
.collect(*OpenWebRxReceiverClient.config_keys)
.defaults(PropertyManager.getSharedInstance())
)
def sendConfig(key, value):
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
# TODO mathematical properties? hmmmm
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
self.write_config(config)
cf = configProps["center_freq"]
srh = configProps["samp_rate"] / 2
frequencyRange = (cf - srh, cf + srh)
self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange))
bookmarks = [b.__dict__() for b in Bookmarks.getSharedInstance().getBookmarks(frequencyRange)]
self.write_bookmarks(bookmarks)
self.configSub = configProps.wire(sendConfig)
sendConfig(None, None)
self.sdr.addSpectrumClient(self)
def startDsp(self):
if self.dsp is None:
self.dsp = DspManager(self, self.sdr)
self.dsp.start()
def close(self):
self.stopDsp()
CpuUsageThread.getSharedInstance().remove_client(self)
ClientRegistry.getSharedInstance().removeClient(self)
if self.configSub is not None:
self.configSub.cancel()
self.configSub = None
super().close()
def stopDsp(self):
if self.dsp is not None:
self.dsp.stop()
self.dsp = None
if self.sdr is not None:
self.sdr.removeSpectrumClient(self)
def setParams(self, params):
# only the keys in the protected property manager can be overridden from the web
protected = (
self.sdr.getProps()
.collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain")
.defaults(PropertyManager.getSharedInstance())
)
for key, value in params.items():
protected[key] = value
def setDspProperties(self, params):
for key, value in params.items():
self.dsp.setProperty(key, value)
def write_spectrum_data(self, data):
self.mp_send(bytes([0x01]) + data)
def write_dsp_data(self, data):
self.send(bytes([0x02]) + data)
def write_s_meter_level(self, level):
self.send({"type": "smeter", "value": level})
def write_cpu_usage(self, usage):
self.mp_send({"type": "cpuusage", "value": usage})
def write_clients(self, clients):
self.mp_send({"type": "clients", "value": clients})
def write_secondary_fft(self, data):
self.send(bytes([0x03]) + data)
def write_secondary_demod(self, data):
self.send(bytes([0x04]) + data)
def write_secondary_dsp_config(self, cfg):
self.send({"type": "secondary_config", "value": cfg})
def write_config(self, cfg):
self.send({"type": "config", "value": cfg})
def write_receiver_details(self, details):
self.send({"type": "receiver_details", "value": details})
def write_profiles(self, profiles):
self.send({"type": "profiles", "value": profiles})
def write_features(self, features):
self.send({"type": "features", "value": features})
def write_metadata(self, metadata):
self.send({"type": "metadata", "value": metadata})
def write_wsjt_message(self, message):
self.send({"type": "wsjt_message", "value": message})
def write_dial_frequendies(self, frequencies):
self.send({"type": "dial_frequencies", "value": frequencies})
def write_bookmarks(self, bookmarks):
self.send({"type": "bookmarks", "value": bookmarks})
def write_aprs_data(self, data):
self.send({"type": "aprs_data", "value": data})
class MapConnection(Client):
def __init__(self, conn):
super().__init__(conn)
pm = PropertyManager.getSharedInstance()
self.write_config(pm.collect("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__())
Map.getSharedInstance().addClient(self)
def handleTextMessage(self, conn, message):
pass
def close(self):
Map.getSharedInstance().removeClient(self)
super().close()
def write_config(self, cfg):
self.send({"type": "config", "value": cfg})
def write_update(self, update):
self.mp_send({"type": "update", "value": update})
class WebSocketMessageHandler(object):
def __init__(self):
self.handshake = None
def handleTextMessage(self, conn, message):
if message[:16] == "SERVER DE CLIENT":
meta = message[17:].split(" ")
self.handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)}
conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version))
logger.debug("client connection intitialized")
if "type" in self.handshake:
if self.handshake["type"] == "receiver":
client = OpenWebRxReceiverClient(conn)
if self.handshake["type"] == "map":
client = MapConnection(conn)
# backwards compatibility
else:
client = OpenWebRxReceiverClient(conn)
# hand off all further communication to the correspondig connection
conn.setMessageHandler(client)
return
if not self.handshake:
logger.warning("not answering client request since handshake is not complete")
return
def handleBinaryMessage(self, conn, data):
pass
def handleClose(self):
pass

155
owrx/controllers.py Normal file
View File

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

219
owrx/feature.py Normal file
View File

@ -0,0 +1,219 @@
import os
import subprocess
from functools import reduce
from operator import and_
import re
from distutils.version import LooseVersion
import inspect
import logging
logger = logging.getLogger(__name__)
class UnknownFeatureException(Exception):
pass
class FeatureDetector(object):
features = {
"core": ["csdr", "nmux", "nc"],
"rtl_sdr": ["rtl_sdr"],
"sdrplay": ["rx_tools"],
"hackrf": ["hackrf_transfer"],
"airspy": ["airspy_rx"],
"digital_voice_digiham": ["digiham", "sox"],
"digital_voice_dsd": ["dsd", "sox", "digiham"],
"wsjt-x": ["wsjtx", "sox"],
"packet": ["direwolf"],
}
def feature_availability(self):
return {name: self.is_available(name) for name in FeatureDetector.features}
def feature_report(self):
def requirement_details(name):
available = self.has_requirement(name)
return {
"available": available,
# as of now, features are always enabled as soon as they are available. this may change in the future.
"enabled": available,
"description": self.get_requirement_description(name),
}
def feature_details(name):
return {
"description": "",
"available": self.is_available(name),
"requirements": {name: requirement_details(name) for name in self.get_requirements(name)},
}
return {name: feature_details(name) for name in FeatureDetector.features}
def is_available(self, feature):
return self.has_requirements(self.get_requirements(feature))
def get_requirements(self, feature):
try:
return FeatureDetector.features[feature]
except KeyError:
raise UnknownFeatureException('Feature "{0}" is not known.'.format(feature))
def has_requirements(self, requirements):
passed = True
for requirement in requirements:
passed = passed and self.has_requirement(requirement)
return passed
def _get_requirement_method(self, requirement):
methodname = "has_" + requirement
if hasattr(self, methodname) and callable(getattr(self, methodname)):
return getattr(self, methodname)
return None
def has_requirement(self, requirement):
method = self._get_requirement_method(requirement)
if method is not None:
return method()
else:
logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement))
return False
def get_requirement_description(self, requirement):
return inspect.getdoc(self._get_requirement_method(requirement))
def command_is_runnable(self, command):
return os.system("{0} 2>/dev/null >/dev/null".format(command)) != 32512
def has_csdr(self):
"""
OpenWebRX uses the demodulator and pipeline tools provided by the csdr project. Please check out [the project
page on github](https://github.com/simonyiszk/csdr) for further details and installation instructions.
"""
return self.command_is_runnable("csdr")
def has_nmux(self):
"""
Nmux is another tool provided by the csdr project. It is used for internal multiplexing of the IQ data streams.
If you're missing nmux even though you have csdr installed, please update your csdr version.
"""
return self.command_is_runnable("nmux --help")
def has_nc(self):
"""
Nc is the client used to connect to the nmux multiplexer. It is provided by either the BSD netcat (recommended
for better performance) or GNU netcat packages. Please check your distribution package manager for options.
"""
return self.command_is_runnable("nc --help")
def has_rtl_sdr(self):
"""
The rtl-sdr command is required to read I/Q data from an RTL SDR USB-Stick. It is available in most
distribution package managers.
"""
return self.command_is_runnable("rtl_sdr --help")
def has_rx_tools(self):
"""
The rx_tools package can be used to interface with SDR devices compatible with SoapySDR. It is currently used
to connect to SDRPlay devices. Please check the following pages for more details:
* [rx_tools GitHub page](https://github.com/rxseger/rx_tools)
* [SoapySDR Project wiki](https://github.com/pothosware/SoapySDR/wiki)
* [SDRPlay homepage](https://www.sdrplay.com/)
"""
return self.command_is_runnable("rx_sdr --help")
def has_hackrf_transfer(self):
"""
To use a HackRF, compile the HackRF host tools from its "stdout" branch:
```
git clone https://github.com/mossmann/hackrf/
cd hackrf
git fetch
git checkout origin/stdout
cd host
mkdir build
cd build
cmake .. -DINSTALL_UDEV_RULES=ON
make
sudo make install
```
"""
# TODO i don't have a hackrf, so somebody doublecheck this.
# TODO also check if it has the stdout feature
return self.command_is_runnable("hackrf_transfer --help")
def command_exists(self, command):
return os.system("which {0}".format(command)) == 0
def has_digiham(self):
"""
To use digital voice modes, the digiham package is required. You can find the package and installation
instructions [here](https://github.com/jketterl/digiham).
Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work.
If you have an older verison of digiham installed, please update it along with openwebrx.
As of now, we require version 0.2 of digiham.
"""
required_version = LooseVersion("0.2")
digiham_version_regex = re.compile("^digiham version (.*)$")
def check_digiham_version(command):
try:
process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
version = LooseVersion(digiham_version_regex.match(process.stdout.readline().decode()).group(1))
process.wait(1)
return version >= required_version
except FileNotFoundError:
return False
return reduce(
and_,
map(
check_digiham_version,
[
"rrc_filter",
"ysf_decoder",
"dmr_decoder",
"mbe_synthesizer",
"gfsk_demodulator",
"digitalvoice_filter",
],
),
True,
)
def has_dsd(self):
"""
The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version
modified by F4EXB that provides stdin/stdout support. You can find it [here](https://github.com/f4exb/dsd).
"""
return self.command_is_runnable("dsd")
def has_sox(self):
"""
The sox audio library is used to convert between the typical 8 kHz audio sampling rate used by digital modes and
the audio sampling rate requested by the client.
It is available for most distributions through the respective package manager.
"""
return self.command_is_runnable("sox")
def has_direwolf(self):
return self.command_is_runnable("direwolf --help")
def has_airspy_rx(self):
"""
In order to use an Airspy Receiver, you need to install the airspy_rx receiver software.
"""
return self.command_is_runnable("airspy_rx --help 2> /dev/null")
def has_wsjtx(self):
"""
To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the
[WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions
on how to build from source.
"""
return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True)

76
owrx/http.py Normal file
View File

@ -0,0 +1,76 @@
from owrx.controllers import (
StatusController,
IndexController,
OwrxAssetsController,
WebSocketController,
MapController,
FeatureController,
ApiController,
MetricsController,
AprsSymbolsController,
)
from http.server import BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import re
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class RequestHandler(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server):
self.router = Router()
super().__init__(request, client_address, server)
def log_message(self, format, *args):
logger.debug("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format % args)
def do_GET(self):
self.router.route(self)
class Request(object):
def __init__(self, query=None, matches=None):
self.query = query
self.matches = matches
class Router(object):
mappings = [
{"route": "/", "controller": IndexController},
{"route": "/status", "controller": StatusController},
{"regex": "/static/(.+)", "controller": OwrxAssetsController},
{"regex": "/aprs-symbols/(.+)", "controller": AprsSymbolsController},
{"route": "/ws/", "controller": WebSocketController},
{"regex": "(/favicon.ico)", "controller": OwrxAssetsController},
# backwards compatibility for the sdr.hu portal
{"regex": "/(gfx/openwebrx-avatar.png)", "controller": OwrxAssetsController},
{"route": "/map", "controller": MapController},
{"route": "/features", "controller": FeatureController},
{"route": "/api/features", "controller": ApiController},
{"route": "/metrics", "controller": MetricsController},
]
def find_controller(self, path):
for m in Router.mappings:
if "route" in m:
if m["route"] == path:
return (m["controller"], None)
if "regex" in m:
regex = re.compile(m["regex"])
matches = regex.match(path)
if matches:
return (m["controller"], matches)
def route(self, handler):
url = urlparse(handler.path)
res = self.find_controller(url.path)
if res is not None:
(controller, matches) = res
query = parse_qs(url.query)
request = Request(query, matches)
controller(handler, request).handle_request()
else:
handler.send_error(404, "Not Found", "The page you requested could not be found.")

104
owrx/kiss.py Normal file
View File

@ -0,0 +1,104 @@
import socket
import time
import logging
import random
from owrx.config import PropertyManager
logger = logging.getLogger(__name__)
FEND = 0xC0
FESC = 0xDB
TFEND = 0xDC
TFESC = 0xDD
class DirewolfConfig(object):
def getConfig(self, port, is_service):
pm = PropertyManager.getSharedInstance()
config = """
ACHANNELS 1
CHANNEL 0
MYCALL {callsign}
MODEM 1200
KISSPORT {port}
AGWPORT off
""".format(
port=port, callsign=pm["aprs_callsign"]
)
if is_service and pm["aprs_igate_enabled"]:
config += """
IGSERVER {server}
IGLOGIN {callsign} {password}
""".format(
server=pm["aprs_igate_server"], callsign=pm["aprs_callsign"], password=pm["aprs_igate_password"]
)
if pm["aprs_igate_beacon"]:
(lat, lon) = pm["receiver_gps"]
lat = "{0}^{1:.2f}{2}".format(int(lat), (lat - int(lat)) * 60, "N" if lat > 0 else "S")
lon = "{0}^{1:.2f}{2}".format(int(lon), (lon - int(lon)) * 60, "E" if lon > 0 else "W")
config += """
PBEACON sendto=IG delay=0:30 every=60:00 symbol="igate" overlay=R lat={lat} long={lon} comment="OpenWebRX APRS gateway"
""".format(
lat=lat, lon=lon
)
return config
class KissClient(object):
@staticmethod
def getFreePort():
# direwolf has some strange hardcoded port ranges
while True:
try:
port = random.randrange(1024, 49151)
# test if port is available for use
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("localhost", port))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.close()
return port
except OSError:
pass
def __init__(self, port):
time.sleep(1)
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect(("localhost", port))
def read(self):
return self.socket.recv(1)
class KissDeframer(object):
def __init__(self):
self.escaped = False
self.buf = bytearray()
def parse(self, input):
frames = []
for b in input:
if b == FESC:
self.escaped = True
elif self.escaped:
if b == TFEND:
self.buf.append(FEND)
elif b == TFESC:
self.buf.append(FESC)
else:
logger.warning("invalid escape char: %s", str(input[0]))
self.escaped = False
elif input[0] == FEND:
# data frames start with 0x00
if len(self.buf) > 1 and self.buf[0] == 0x00:
frames += [self.buf[1:]]
self.buf = bytearray()
else:
self.buf.append(b)
return frames

24
owrx/locator.py Normal file
View File

@ -0,0 +1,24 @@
class Locator(object):
@staticmethod
def fromCoordinates(coordinates, depth=3):
lat, lon = coordinates
lon = lon + 180
lat = lat + 90
res = ""
res += chr(65 + int(lon / 20))
res += chr(65 + int(lat / 10))
if depth >= 2:
lon = lon % 20
lat = lat % 10
res += str(int(lon / 2))
res += str(int(lat))
if depth >= 3:
lon = lon % 2
lat = lat % 1
res += chr(97 + int(lon * 12))
res += chr(97 + int(lat * 24))
return res

116
owrx/map.py Normal file
View File

@ -0,0 +1,116 @@
from datetime import datetime, timedelta
import threading, time
from owrx.config import PropertyManager
from owrx.bands import Band
import logging
logger = logging.getLogger(__name__)
class Location(object):
def __dict__(self):
return {}
class Map(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if Map.sharedInstance is None:
Map.sharedInstance = Map()
return Map.sharedInstance
def __init__(self):
self.clients = []
self.positions = {}
def removeLoop():
while True:
try:
self.removeOldPositions()
except Exception:
logger.exception("error while removing old map positions")
time.sleep(60)
threading.Thread(target=removeLoop, daemon=True).start()
super().__init__()
def broadcast(self, update):
for c in self.clients:
c.write_update(update)
def addClient(self, client):
self.clients.append(client)
client.write_update(
[
{
"callsign": callsign,
"location": record["location"].__dict__(),
"lastseen": record["updated"].timestamp() * 1000,
"mode": record["mode"],
"band": record["band"].getName() if record["band"] is not None else None,
}
for (callsign, record) in self.positions.items()
]
)
def removeClient(self, client):
try:
self.clients.remove(client)
except ValueError:
pass
def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None):
ts = datetime.now()
self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band}
self.broadcast(
[
{
"callsign": callsign,
"location": loc.__dict__(),
"lastseen": ts.timestamp() * 1000,
"mode": mode,
"band": band.getName() if band is not None else None,
}
]
)
def touchLocation(self, callsign):
# not implemented on the client side yet, so do not use!
ts = datetime.now()
if callsign in self.positions:
self.positions[callsign]["updated"] = ts
self.broadcast([{"callsign": callsign, "lastseen": ts.timestamp() * 1000}])
def removeLocation(self, callsign):
self.positions.pop(callsign, None)
# TODO broadcast removal to clients
def removeOldPositions(self):
pm = PropertyManager.getSharedInstance()
retention = timedelta(seconds=pm["map_position_retention_time"])
cutoff = datetime.now() - retention
to_be_removed = [callsign for (callsign, pos) in self.positions.items() if pos["updated"] < cutoff]
for callsign in to_be_removed:
self.removeLocation(callsign)
class LatLngLocation(Location):
def __init__(self, lat: float, lon: float):
self.lat = lat
self.lon = lon
def __dict__(self):
res = {"type": "latlon", "lat": self.lat, "lon": self.lon}
return res
class LocatorLocation(Location):
def __init__(self, locator: str):
self.locator = locator
def __dict__(self):
return {"type": "locator", "locator": self.locator}

108
owrx/meta.py Normal file
View File

@ -0,0 +1,108 @@
from owrx.config import PropertyManager
from urllib import request
import json
from datetime import datetime, timedelta
import logging
import threading
from owrx.map import Map, LatLngLocation
from owrx.bands import Bandplan
logger = logging.getLogger(__name__)
class DmrCache(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if DmrCache.sharedInstance is None:
DmrCache.sharedInstance = DmrCache()
return DmrCache.sharedInstance
def __init__(self):
self.cache = {}
self.cacheTimeout = timedelta(seconds=86400)
def isValid(self, key):
if not key in self.cache:
return False
entry = self.cache[key]
return entry["timestamp"] + self.cacheTimeout > datetime.now()
def put(self, key, value):
self.cache[key] = {"timestamp": datetime.now(), "data": value}
def get(self, key):
if not self.isValid(key):
return None
return self.cache[key]["data"]
class DmrMetaEnricher(object):
def __init__(self):
self.threads = {}
def downloadRadioIdData(self, id):
cache = DmrCache.getSharedInstance()
try:
logger.debug("requesting DMR metadata for id=%s", id)
res = request.urlopen("https://www.radioid.net/api/dmr/user/?id={0}".format(id), timeout=30).read()
data = json.loads(res.decode("utf-8"))
cache.put(id, data)
except json.JSONDecodeError:
cache.put(id, None)
del self.threads[id]
def enrich(self, meta):
if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]:
return None
if not "source" in meta:
return None
id = meta["source"]
cache = DmrCache.getSharedInstance()
if not cache.isValid(id):
if not id in self.threads:
self.threads[id] = threading.Thread(target=self.downloadRadioIdData, args=[id])
self.threads[id].start()
return None
data = cache.get(id)
if "count" in data and data["count"] > 0 and "results" in data:
return data["results"][0]
return None
class YsfMetaEnricher(object):
def __init__(self, parser):
self.parser = parser
def enrich(self, meta):
if "source" in meta and "lat" in meta and "lon" in meta:
# TODO parsing the float values should probably happen earlier
loc = LatLngLocation(float(meta["lat"]), float(meta["lon"]))
Map.getSharedInstance().updateLocation(meta["source"], loc, "YSF", self.parser.getBand())
return None
class MetaParser(object):
def __init__(self, handler):
self.handler = handler
self.enrichers = {"DMR": DmrMetaEnricher(), "YSF": YsfMetaEnricher(self)}
self.band = None
def setDialFrequency(self, freq):
self.band = Bandplan.getSharedInstance().findBand(freq)
def getBand(self):
return self.band
def parse(self, meta):
fields = meta.split(";")
meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""}
if "protocol" in meta:
protocol = meta["protocol"]
if protocol in self.enrichers:
additional_data = self.enrichers[protocol].enrich(meta)
if additional_data is not None:
meta["additional"] = additional_data
self.handler.write_metadata(meta)

65
owrx/metrics.py Normal file
View File

@ -0,0 +1,65 @@
import threading
class Metric(object):
def getValue(self):
return 0
class CounterMetric(Metric):
def __init__(self):
self.counter = 0
def inc(self, increment=1):
self.counter += increment
def getValue(self):
return {"count": self.counter}
class DirectMetric(Metric):
def __init__(self, getter):
self.getter = getter
def getValue(self):
return self.getter()
class Metrics(object):
sharedInstance = None
creationLock = threading.Lock()
@staticmethod
def getSharedInstance():
with Metrics.creationLock:
if Metrics.sharedInstance is None:
Metrics.sharedInstance = Metrics()
return Metrics.sharedInstance
def __init__(self):
self.metrics = {}
def addMetric(self, name, metric):
self.metrics[name] = metric
def hasMetric(self, name):
return name in self.metrics
def getMetric(self, name):
if not self.hasMetric(name):
return None
return self.metrics[name]
def getMetrics(self):
result = {}
for (key, metric) in self.metrics.items():
partial = result
keys = key.split(".")
for keypart in keys[0:-1]:
if not keypart in partial:
partial[keypart] = {}
partial = partial[keypart]
partial[keys[-1]] = metric.getValue()
return result

193
owrx/pskreporter.py Normal file
View File

@ -0,0 +1,193 @@
import logging
import threading
import time
import random
import socket
from sched import scheduler
from owrx.config import PropertyManager
from owrx.version import openwebrx_version
from owrx.locator import Locator
logger = logging.getLogger(__name__)
class PskReporterDummy(object):
"""
used in place of the PskReporter when reporting is disabled.
does nothing.
"""
def spot(self, spot):
pass
class PskReporter(object):
sharedInstance = None
creationLock = threading.Lock()
interval = 300
supportedModes = ["FT8", "FT4", "JT9", "JT65"]
@staticmethod
def getSharedInstance():
with PskReporter.creationLock:
if PskReporter.sharedInstance is None:
if PropertyManager.getSharedInstance()["pskreporter_enabled"]:
PskReporter.sharedInstance = PskReporter()
else:
PskReporter.sharedInstance = PskReporterDummy()
return PskReporter.sharedInstance
def __init__(self):
self.spots = []
self.spotLock = threading.Lock()
self.uploader = Uploader()
self.scheduler = scheduler(time.time, time.sleep)
self.scheduleNextUpload()
threading.Thread(target=self.scheduler.run).start()
def scheduleNextUpload(self):
delay = PskReporter.interval + random.uniform(0, 30)
logger.debug("scheduling next pskreporter upload in %f seconds", delay)
self.scheduler.enter(delay, 1, self.upload)
def spot(self, spot):
if not spot["mode"] in PskReporter.supportedModes:
return
with self.spotLock:
self.spots.append(spot)
def upload(self):
try:
with self.spotLock:
spots = self.spots
self.spots = []
if spots:
self.uploader.upload(spots)
except Exception:
logger.exception("Failed to upload spots")
self.scheduleNextUpload()
class Uploader(object):
receieverDelimiter = [0x99, 0x92]
senderDelimiter = [0x99, 0x93]
def __init__(self):
self.sequence = 0
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def upload(self, spots):
logger.debug("uploading %i spots", len(spots))
for packet in self.getPackets(spots):
self.socket.sendto(packet, ("report.pskreporter.info", 4739))
def getPackets(self, spots):
encoded = [self.encodeSpot(spot) for spot in spots]
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i : i + n]
rHeader = self.getReceiverInformationHeader()
rInfo = self.getReceiverInformation()
sHeader = self.getSenderInformationHeader()
packets = []
# 50 seems to be a safe bet
for chunk in chunks(encoded, 50):
sInfo = self.getSenderInformation(chunk)
length = 16 + len(rHeader) + len(sHeader) + len(rInfo) + len(sInfo)
header = self.getHeader(length)
packets.append(header + rHeader + sHeader + rInfo + sInfo)
return packets
def getHeader(self, length):
self.sequence += 1
return bytes(
# protocol version
[0x00, 0x0A]
+ list(length.to_bytes(2, "big"))
+ list(int(time.time()).to_bytes(4, "big"))
+ list(self.sequence.to_bytes(4, "big"))
+ list((id(self) & 0xFFFFFFFF).to_bytes(4, "big"))
)
def encodeString(self, s):
return [len(s)] + list(s.encode("utf-8"))
def encodeSpot(self, spot):
return bytes(
self.encodeString(spot["callsign"])
+ list(spot["freq"].to_bytes(4, "big"))
+ list(int(spot["db"]).to_bytes(1, "big", signed=True))
+ self.encodeString(spot["mode"])
+ self.encodeString(spot["locator"])
# informationsource. 1 means "automatically extracted
+ [0x01]
+ list(int(spot["timestamp"] / 1000).to_bytes(4, "big"))
)
def getReceiverInformationHeader(self):
return bytes(
# id, length
[0x00, 0x03, 0x00, 0x24]
+ Uploader.receieverDelimiter
# number of fields
+ [0x00, 0x03, 0x00, 0x00]
# receiverCallsign
+ [0x80, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# receiverLocator
+ [0x80, 0x04, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# decodingSoftware
+ [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# padding
+ [0x00, 0x00]
)
def getReceiverInformation(self):
pm = PropertyManager.getSharedInstance()
callsign = pm["pskreporter_callsign"]
locator = Locator.fromCoordinates(pm["receiver_gps"])
decodingSoftware = "OpenWebRX " + openwebrx_version
body = [b for s in [callsign, locator, decodingSoftware] for b in self.encodeString(s)]
body = self.pad(body, 4)
body = bytes(Uploader.receieverDelimiter + list((len(body) + 4).to_bytes(2, "big")) + body)
return body
def getSenderInformationHeader(self):
return bytes(
# id, length
[0x00, 0x02, 0x00, 0x3C]
+ Uploader.senderDelimiter
# number of fields
+ [0x00, 0x07]
# senderCallsign
+ [0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# frequency
+ [0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F]
# sNR
+ [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F]
# mode
+ [0x80, 0x0A, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# senderLocator
+ [0x80, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# informationSource
+ [0x80, 0x0B, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F]
# flowStartSeconds
+ [0x00, 0x96, 0x00, 0x04]
)
def getSenderInformation(self, chunk):
sInfo = self.padBytes(b"".join(chunk), 4)
sInfoLength = len(sInfo) + 4
return bytes(Uploader.senderDelimiter) + sInfoLength.to_bytes(2, "big") + sInfo
def pad(self, b, l):
return b + [0x00 for _ in range(0, -1 * len(b) % l)]
def padBytes(self, b, l):
return b + bytes([0x00 for _ in range(0, -1 * len(b) % l)])

39
owrx/sdrhu.py Normal file
View File

@ -0,0 +1,39 @@
import threading
import subprocess
import time
from owrx.config import PropertyManager
import logging
logger = logging.getLogger(__name__)
class SdrHuUpdater(threading.Thread):
def __init__(self):
self.doRun = True
super().__init__(daemon=True)
def update(self):
pm = PropertyManager.getSharedInstance()
cmd = 'wget --timeout=15 -4qO- https://sdr.hu/update --post-data "url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}" 2>&1'.format(
**pm.__dict__()
)
logger.debug(cmd)
returned = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
returned = returned[0].decode("utf-8")
if "UPDATE:" in returned:
retrytime_mins = 20
value = returned.split("UPDATE:")[1].split("\n", 1)[0]
if value.startswith("SUCCESS"):
logger.info("Update succeeded!")
else:
logger.warning("Update failed, your receiver cannot be listed on sdr.hu! Reason: %s", value)
else:
retrytime_mins = 2
logger.warning("wget failed while updating, your receiver cannot be listed on sdr.hu!")
return retrytime_mins
def run(self):
while self.doRun:
retrytime_mins = self.update()
time.sleep(60 * retrytime_mins)

349
owrx/service.py Normal file
View File

@ -0,0 +1,349 @@
import threading
import socket
from datetime import datetime, timezone, timedelta
from owrx.source import SdrService
from owrx.bands import Bandplan
from csdr import dsp, output
from owrx.wsjt import WsjtParser
from owrx.aprs import AprsParser
from owrx.config import PropertyManager
from owrx.source import Resampler
import logging
logger = logging.getLogger(__name__)
class ServiceOutput(output):
def __init__(self, frequency):
self.frequency = frequency
def getParser(self):
# abstract method; implement in subclasses
pass
def receive_output(self, t, read_fn):
parser = self.getParser()
parser.setDialFrequency(self.frequency)
target = self.pump(read_fn, parser.parse)
threading.Thread(target=target).start()
class WsjtServiceOutput(ServiceOutput):
def getParser(self):
return WsjtParser(WsjtHandler())
def supports_type(self, t):
return t == "wsjt_demod"
class AprsServiceOutput(ServiceOutput):
def getParser(self):
return AprsParser(AprsHandler())
def supports_type(self, t):
return t == "packet_demod"
class ScheduleEntry(object):
def __init__(self, startTime, endTime, profile):
self.startTime = startTime
self.endTime = endTime
self.profile = profile
def isCurrent(self, time):
if self.startTime < self.endTime:
return self.startTime <= time < self.endTime
else:
return self.startTime <= time or time < self.endTime
def getProfile(self):
return self.profile
def getScheduledEnd(self):
now = datetime.utcnow()
end = now.combine(date=now.date(), time=self.endTime)
while end < now:
end += timedelta(days=1)
return end
def getNextActivation(self):
now = datetime.utcnow()
start = now.combine(date=now.date(), time=self.startTime)
while start < now:
start += timedelta(days=1)
return start
class Schedule(object):
@staticmethod
def parse(scheduleDict):
entries = []
for time, profile in scheduleDict.items():
if len(time) != 9:
logger.warning("invalid schedule spec: %s", time)
continue
startTime = datetime.strptime(time[0:4], "%H%M").replace(tzinfo=timezone.utc).time()
endTime = datetime.strptime(time[5:9], "%H%M").replace(tzinfo=timezone.utc).time()
entries.append(ScheduleEntry(startTime, endTime, profile))
return Schedule(entries)
def __init__(self, entries):
self.entries = entries
def getCurrentEntry(self):
current = [p for p in self.entries if p.isCurrent(datetime.utcnow().time())]
if current:
return current[0]
return None
def getNextEntry(self):
s = sorted(self.entries, key=lambda e: e.getNextActivation())
if s:
return s[0]
return None
class ServiceScheduler(object):
def __init__(self, source, schedule):
self.source = source
self.schedule = Schedule.parse(schedule)
self.active = False
self.source.addClient(self)
self.selectionTimer = None
self.scheduleSelection()
def scheduleSelection(self, time=None):
seconds = 10
if time is not None:
delta = time - datetime.utcnow()
seconds = delta.total_seconds()
if self.selectionTimer:
self.selectionTimer.cancel()
self.selectionTimer = threading.Timer(seconds, self.selectProfile)
self.selectionTimer.start()
def isActive(self):
return self.active
def onSdrAvailable(self):
pass
def onSdrUnavailable(self):
self.scheduleSelection()
def selectProfile(self):
self.active = False
if self.source.hasActiveClients():
logger.debug("source has active clients; not touching")
return
logger.debug("source seems to be idle, selecting profile for background services")
entry = self.schedule.getCurrentEntry()
if entry is None:
logger.debug("schedule did not return a profile. checking next entry...")
nextEntry = self.schedule.getNextEntry()
if nextEntry is not None:
self.scheduleSelection(nextEntry.getNextActivation())
return
logger.debug("scheduling end for current profile: %s", entry.getScheduledEnd())
self.scheduleSelection(entry.getScheduledEnd())
try:
self.active = True
self.source.activateProfile(entry.getProfile())
self.source.start()
except KeyError:
pass
class ServiceHandler(object):
def __init__(self, source):
self.lock = threading.Lock()
self.services = []
self.source = source
self.startupTimer = None
self.source.addClient(self)
props = self.source.getProps()
props.collect("center_freq", "samp_rate").wire(self.onFrequencyChange)
if self.source.isAvailable():
self.scheduleServiceStartup()
if "schedule" in props:
ServiceScheduler(self.source, props["schedule"])
def isActive(self):
return False
def onSdrAvailable(self):
self.scheduleServiceStartup()
def onSdrUnavailable(self):
logger.debug("sdr source becoming unavailable; stopping services.")
self.stopServices()
def isSupported(self, mode):
return mode in PropertyManager.getSharedInstance()["services_decoders"]
def stopServices(self):
with self.lock:
services = self.services
self.services = []
for service in services:
service.stop()
def onFrequencyChange(self, key, value):
self.stopServices()
if not self.source.isAvailable():
return
self.scheduleServiceStartup()
def scheduleServiceStartup(self):
if self.startupTimer:
self.startupTimer.cancel()
self.startupTimer = threading.Timer(10, self.updateServices)
self.startupTimer.start()
def getAvailablePort(self):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("", 0))
s.listen(1)
port = s.getsockname()[1]
s.close()
return port
def updateServices(self):
logger.debug("re-scheduling services due to sdr changes")
self.stopServices()
if not self.source.isAvailable():
logger.debug("sdr source is unavailable")
return
cf = self.source.getProps()["center_freq"]
sr = self.source.getProps()["samp_rate"]
srh = sr / 2
frequency_range = (cf - srh, cf + srh)
dials = [
dial
for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range)
if self.isSupported(dial["mode"])
]
if not dials:
logger.debug("no services available")
return
with self.lock:
self.services = []
for group in self.optimizeResampling(dials, sr):
frequencies = sorted([f["frequency"] for f in group])
min = frequencies[0]
max = frequencies[-1]
cf = (min + max) / 2
bw = max - min
logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw))
resampler_props = PropertyManager()
resampler_props["center_freq"] = cf
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
resampler_props["samp_rate"] = bw + 24000
resampler = Resampler(resampler_props, self.getAvailablePort(), self.source)
resampler.start()
self.services.append(resampler)
for dial in group:
self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler))
def optimizeResampling(self, freqs, bandwidth):
freqs = sorted(freqs, key=lambda f: f["frequency"])
distances = [
{"frequency": freqs[i]["frequency"], "distance": freqs[i + 1]["frequency"] - freqs[i]["frequency"]}
for i in range(0, len(freqs) - 1)
]
distances = [d for d in distances if d["distance"] > 0]
distances = sorted(distances, key=lambda f: f["distance"], reverse=True)
def calculate_usage(num_splits):
splits = sorted([f["frequency"] for f in distances[0:num_splits]])
previous = 0
groups = []
for split in splits:
groups.append([f for f in freqs if previous < f["frequency"] <= split])
previous = split
groups.append([f for f in freqs if previous < f["frequency"]])
def get_bandwitdh(group):
freqs = sorted([f["frequency"] for f in group])
# the group will process the full BW once, plus the reduced BW once for each group member
return bandwidth + len(group) * (freqs[-1] - freqs[0] + 24000)
total_bandwidth = sum([get_bandwitdh(group) for group in groups])
return {"num_splits": num_splits, "total_bandwidth": total_bandwidth, "groups": groups}
usages = [calculate_usage(i) for i in range(0, len(freqs))]
# this is simulating no resampling. i haven't seen this as the best result yet
usages += [{"num_splits": None, "total_bandwidth": bandwidth * len(freqs), "groups": [freqs]}]
results = sorted(usages, key=lambda f: f["total_bandwidth"])
for r in results:
logger.debug("splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"]))
return results[0]["groups"]
def setupService(self, mode, frequency, source):
logger.debug("setting up service {0} on frequency {1}".format(mode, frequency))
# TODO selecting outputs will need some more intelligence here
if mode == "packet":
output = AprsServiceOutput(frequency)
else:
output = WsjtServiceOutput(frequency)
d = dsp(output)
d.nc_port = source.getPort()
d.set_offset_freq(frequency - source.getProps()["center_freq"])
if mode == "packet":
d.set_demodulator("nfm")
d.set_bpf(-4000, 4000)
elif mode == "wspr":
d.set_demodulator("usb")
# WSPR only samples between 1400 and 1600 Hz
d.set_bpf(1350, 1650)
else:
d.set_demodulator("usb")
d.set_bpf(0, 3000)
d.set_secondary_demodulator(mode)
d.set_audio_compression("none")
d.set_samp_rate(source.getProps()["samp_rate"])
d.set_service()
d.start()
return d
class WsjtHandler(object):
def write_wsjt_message(self, msg):
pass
class AprsHandler(object):
def write_aprs_data(self, data):
pass
class Services(object):
@staticmethod
def start():
if not PropertyManager.getSharedInstance()["services_enabled"]:
return
for source in SdrService.getSources().values():
ServiceHandler(source)
class Service(object):
pass
class WsjtService(Service):
pass

739
owrx/source.py Normal file
View File

@ -0,0 +1,739 @@
import subprocess
from owrx.config import PropertyManager
from owrx.feature import FeatureDetector, UnknownFeatureException
from owrx.meta import MetaParser
from owrx.wsjt import WsjtParser
from owrx.aprs import AprsParser
from owrx.metrics import Metrics, DirectMetric
import threading
import csdr
import time
import os
import signal
import sys
import socket
import logging
logger = logging.getLogger(__name__)
class SdrService(object):
sdrProps = None
sources = {}
lastPort = None
@staticmethod
def getNextPort():
pm = PropertyManager.getSharedInstance()
(start, end) = pm["iq_port_range"]
if SdrService.lastPort is None:
SdrService.lastPort = start
else:
SdrService.lastPort += 1
if SdrService.lastPort > end:
raise IndexError("no more available ports to start more sdrs")
return SdrService.lastPort
@staticmethod
def loadProps():
if SdrService.sdrProps is None:
pm = PropertyManager.getSharedInstance()
featureDetector = FeatureDetector()
def loadIntoPropertyManager(dict: dict):
propertyManager = PropertyManager()
for (name, value) in dict.items():
propertyManager[name] = value
return propertyManager
def sdrTypeAvailable(value):
try:
if not featureDetector.is_available(value["type"]):
logger.error(
'The RTL source type "{0}" is not available. please check requirements.'.format(
value["type"]
)
)
return False
return True
except UnknownFeatureException:
logger.error(
'The RTL source type "{0}" is invalid. Please check your configuration'.format(value["type"])
)
return False
# transform all dictionary items into PropertyManager object, filtering out unavailable ones
SdrService.sdrProps = {
name: loadIntoPropertyManager(value) for (name, value) in pm["sdrs"].items() if sdrTypeAvailable(value)
}
logger.info(
"SDR sources loaded. Availables SDRs: {0}".format(
", ".join(map(lambda x: x["name"], SdrService.sdrProps.values()))
)
)
@staticmethod
def getSource(id=None):
SdrService.loadProps()
if id is None:
# TODO: configure default sdr in config? right now it will pick the first one off the list.
id = list(SdrService.sdrProps.keys())[0]
sources = SdrService.getSources()
return sources[id]
@staticmethod
def getSources():
SdrService.loadProps()
for id in SdrService.sdrProps.keys():
if not id in SdrService.sources:
props = SdrService.sdrProps[id]
className = "".join(x for x in props["type"].title() if x.isalnum()) + "Source"
cls = getattr(sys.modules[__name__], className)
SdrService.sources[id] = cls(props, SdrService.getNextPort())
return SdrService.sources
class SdrSourceException(Exception):
pass
class SdrSource(object):
def __init__(self, props, port):
self.props = props
self.activateProfile()
self.rtlProps = self.props.collect(
"samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain"
).defaults(PropertyManager.getSharedInstance())
def restart(name, value):
logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value))
self.stop()
self.start()
self.rtlProps.wire(restart)
self.port = port
self.monitor = None
self.clients = []
self.spectrumClients = []
self.spectrumThread = None
self.process = None
self.modificationLock = threading.Lock()
# override this in subclasses
def getCommand(self):
pass
# override this in subclasses, if necessary
def getFormatConversion(self):
return None
def activateProfile(self, profile_id=None):
profiles = self.props["profiles"]
if profile_id is None:
profile_id = list(profiles.keys())[0]
logger.debug("activating profile {0}".format(profile_id))
profile = profiles[profile_id]
for (key, value) in profile.items():
# skip the name, that would overwrite the source name.
if key == "name":
continue
self.props[key] = value
def getProfiles(self):
return self.props["profiles"]
def getName(self):
return self.props["name"]
def getProps(self):
return self.props
def getPort(self):
return self.port
def start(self):
self.modificationLock.acquire()
if self.monitor:
self.modificationLock.release()
return
props = self.rtlProps
start_sdr_command = self.getCommand().format(
**props.collect(
"samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain"
).__dict__()
)
format_conversion = self.getFormatConversion()
if format_conversion is not None:
start_sdr_command += " | " + format_conversion
nmux_bufcnt = nmux_bufsize = 0
while nmux_bufsize < props["samp_rate"] / 4:
nmux_bufsize += 4096
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6:
nmux_bufcnt += 1
if nmux_bufcnt == 0 or nmux_bufsize == 0:
logger.error(
"Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
)
self.modificationLock.release()
return
logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt))
cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (
nmux_bufsize,
nmux_bufcnt,
self.port,
)
self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp)
logger.info("Started rtl source: " + cmd)
available = False
def wait_for_process_to_end():
rc = self.process.wait()
logger.debug("shut down with RC={0}".format(rc))
self.monitor = None
self.monitor = threading.Thread(target=wait_for_process_to_end)
self.monitor.start()
retries = 1000
while retries > 0:
retries -= 1
if self.monitor is None:
break
testsock = socket.socket()
try:
testsock.connect(("127.0.0.1", self.getPort()))
testsock.close()
available = True
break
except:
time.sleep(0.1)
self.modificationLock.release()
if not available:
raise SdrSourceException("rtl source failed to start up")
for c in self.clients:
c.onSdrAvailable()
def isAvailable(self):
return self.monitor is not None
def stop(self):
for c in self.clients:
c.onSdrUnavailable()
self.modificationLock.acquire()
if self.process is not None:
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
except ProcessLookupError:
# been killed by something else, ignore
pass
if self.monitor:
self.monitor.join()
self.sleepOnRestart()
self.modificationLock.release()
def sleepOnRestart(self):
pass
def hasActiveClients(self):
activeClients = [c for c in self.clients if c.isActive()]
return len(activeClients) > 0
def addClient(self, c):
self.clients.append(c)
if self.hasActiveClients():
self.start()
def removeClient(self, c):
try:
self.clients.remove(c)
except ValueError:
pass
if not self.hasActiveClients():
self.stop()
def addSpectrumClient(self, c):
self.spectrumClients.append(c)
if self.spectrumThread is None:
self.spectrumThread = SpectrumThread(self)
self.spectrumThread.start()
def removeSpectrumClient(self, c):
try:
self.spectrumClients.remove(c)
except ValueError:
pass
if not self.spectrumClients and self.spectrumThread is not None:
self.spectrumThread.stop()
self.spectrumThread = None
def writeSpectrumData(self, data):
for c in self.spectrumClients:
c.write_spectrum_data(data)
class Resampler(SdrSource):
def __init__(self, props, port, sdr):
sdrProps = sdr.getProps()
self.shift = (sdrProps["center_freq"] - props["center_freq"]) / sdrProps["samp_rate"]
self.decimation = int(float(sdrProps["samp_rate"]) / props["samp_rate"])
if_samp_rate = sdrProps["samp_rate"] / self.decimation
self.transition_bw = 0.15 * (if_samp_rate / float(sdrProps["samp_rate"]))
props["samp_rate"] = if_samp_rate
self.sdr = sdr
super().__init__(props, port)
def start(self):
self.modificationLock.acquire()
if self.monitor:
self.modificationLock.release()
return
props = self.rtlProps
resampler_command = [
"nc -v 127.0.0.1 {nc_port}".format(nc_port=self.sdr.getPort()),
"csdr shift_addition_cc {shift}".format(shift=self.shift),
"csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING".format(
decimation=self.decimation, ddc_transition_bw=self.transition_bw
),
]
nmux_bufcnt = nmux_bufsize = 0
while nmux_bufsize < props["samp_rate"] / 4:
nmux_bufsize += 4096
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6:
nmux_bufcnt += 1
if nmux_bufcnt == 0 or nmux_bufsize == 0:
logger.error(
"Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
)
self.modificationLock.release()
return
logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt))
resampler_command += [
"nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, self.port)
]
cmd = " | ".join(resampler_command)
logger.debug("resampler command: %s", cmd)
self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp)
logger.info("Started resampler source: " + cmd)
available = False
def wait_for_process_to_end():
rc = self.process.wait()
logger.debug("shut down with RC={0}".format(rc))
self.monitor = None
self.monitor = threading.Thread(target=wait_for_process_to_end)
self.monitor.start()
retries = 1000
while retries > 0:
retries -= 1
if self.monitor is None:
break
testsock = socket.socket()
try:
testsock.connect(("127.0.0.1", self.getPort()))
testsock.close()
available = True
break
except:
time.sleep(0.1)
self.modificationLock.release()
if not available:
raise SdrSourceException("resampler source failed to start up")
for c in self.clients:
c.onSdrAvailable()
def activateProfile(self, profile_id=None):
pass
class RtlSdrSource(SdrSource):
def getCommand(self):
return "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -"
def getFormatConversion(self):
return "csdr convert_u8_f"
class HackrfSource(SdrSource):
def getCommand(self):
return "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-"
def getFormatConversion(self):
return "csdr convert_s8_f"
class SdrplaySource(SdrSource):
def getCommand(self):
command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}"
gainMap = {"rf_gain": "RFGR", "if_gain": "IFGR"}
gains = [
"{0}={{{1}}}".format(gainMap[name], name)
for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items()
if value is not None
]
if gains:
command += " -g {gains}".format(gains=",".join(gains))
if self.rtlProps["antenna"] is not None:
command += ' -a "{antenna}"'
command += " -"
return command
def sleepOnRestart(self):
time.sleep(1)
class AirspySource(SdrSource):
def getCommand(self):
frequency = self.props["center_freq"] / 1e6
command = "airspy_rx"
command += " -f{0}".format(frequency)
command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}"
return command
def getFormatConversion(self):
return "csdr convert_s16_f"
class SpectrumThread(csdr.output):
def __init__(self, sdrSource):
self.sdrSource = sdrSource
super().__init__()
self.props = props = self.sdrSource.props.collect(
"samp_rate",
"fft_size",
"fft_fps",
"fft_voverlap_factor",
"fft_compression",
"csdr_dynamic_bufsize",
"csdr_print_bufsizes",
"csdr_through",
"temporary_directory",
).defaults(PropertyManager.getSharedInstance())
self.dsp = dsp = csdr.dsp(self)
dsp.nc_port = self.sdrSource.getPort()
dsp.set_demodulator("fft")
def set_fft_averages(key, value):
samp_rate = props["samp_rate"]
fft_size = props["fft_size"]
fft_fps = props["fft_fps"]
fft_voverlap_factor = props["fft_voverlap_factor"]
dsp.set_fft_averages(
int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor)))
if fft_voverlap_factor > 0
else 0
)
self.subscriptions = [
props.getProperty("samp_rate").wire(dsp.set_samp_rate),
props.getProperty("fft_size").wire(dsp.set_fft_size),
props.getProperty("fft_fps").wire(dsp.set_fft_fps),
props.getProperty("fft_compression").wire(dsp.set_fft_compression),
props.getProperty("temporary_directory").wire(dsp.set_temporary_directory),
props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages),
]
set_fft_averages(None, None)
dsp.csdr_dynamic_bufsize = props["csdr_dynamic_bufsize"]
dsp.csdr_print_bufsizes = props["csdr_print_bufsizes"]
dsp.csdr_through = props["csdr_through"]
logger.debug("Spectrum thread initialized successfully.")
def start(self):
self.sdrSource.addClient(self)
if self.sdrSource.isAvailable():
self.dsp.start()
def supports_type(self, t):
return t == "audio"
def receive_output(self, type, read_fn):
if self.props["csdr_dynamic_bufsize"]:
read_fn(8) # dummy read to skip bufsize & preamble
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
threading.Thread(target=self.pump(read_fn, self.sdrSource.writeSpectrumData)).start()
def stop(self):
self.dsp.stop()
self.sdrSource.removeClient(self)
for c in self.subscriptions:
c.cancel()
self.subscriptions = []
def isActive(self):
return True
def onSdrAvailable(self):
self.dsp.start()
def onSdrUnavailable(self):
self.dsp.stop()
class DspManager(csdr.output):
def __init__(self, handler, sdrSource):
self.handler = handler
self.sdrSource = sdrSource
self.metaParser = MetaParser(self.handler)
self.wsjtParser = WsjtParser(self.handler)
self.aprsParser = AprsParser(self.handler)
self.localProps = (
self.sdrSource.getProps()
.collect(
"audio_compression",
"fft_compression",
"digimodes_fft_size",
"csdr_dynamic_bufsize",
"csdr_print_bufsizes",
"csdr_through",
"digimodes_enable",
"samp_rate",
"digital_voice_unvoiced_quality",
"dmr_filter",
"temporary_directory",
"center_freq",
)
.defaults(PropertyManager.getSharedInstance())
)
self.dsp = csdr.dsp(self)
self.dsp.nc_port = self.sdrSource.getPort()
def set_low_cut(cut):
bpf = self.dsp.get_bpf()
bpf[0] = cut
self.dsp.set_bpf(*bpf)
def set_high_cut(cut):
bpf = self.dsp.get_bpf()
bpf[1] = cut
self.dsp.set_bpf(*bpf)
def set_dial_freq(key, value):
freq = self.localProps["center_freq"] + self.localProps["offset_freq"]
self.wsjtParser.setDialFrequency(freq)
self.aprsParser.setDialFrequency(freq)
self.metaParser.setDialFrequency(freq)
self.subscriptions = [
self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression),
self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression),
self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size),
self.localProps.getProperty("samp_rate").wire(self.dsp.set_samp_rate),
self.localProps.getProperty("output_rate").wire(self.dsp.set_output_rate),
self.localProps.getProperty("offset_freq").wire(self.dsp.set_offset_freq),
self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level),
self.localProps.getProperty("low_cut").wire(set_low_cut),
self.localProps.getProperty("high_cut").wire(set_high_cut),
self.localProps.getProperty("mod").wire(self.dsp.set_demodulator),
self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality),
self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter),
self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory),
self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq),
]
self.dsp.set_offset_freq(0)
self.dsp.set_bpf(-4000, 4000)
self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"]
self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"]
self.dsp.csdr_through = self.localProps["csdr_through"]
if self.localProps["digimodes_enable"]:
def set_secondary_mod(mod):
if mod == False:
mod = None
self.dsp.set_secondary_demodulator(mod)
if mod is not None:
self.handler.write_secondary_dsp_config(
{
"secondary_fft_size": self.localProps["digimodes_fft_size"],
"if_samp_rate": self.dsp.if_samp_rate(),
"secondary_bw": self.dsp.secondary_bw(),
}
)
self.subscriptions += [
self.localProps.getProperty("secondary_mod").wire(set_secondary_mod),
self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq),
]
self.sdrSource.addClient(self)
super().__init__()
def start(self):
if self.sdrSource.isAvailable():
self.dsp.start()
def receive_output(self, t, read_fn):
logger.debug("adding new output of type %s", t)
writers = {
"audio": self.handler.write_dsp_data,
"smeter": self.handler.write_s_meter_level,
"secondary_fft": self.handler.write_secondary_fft,
"secondary_demod": self.handler.write_secondary_demod,
"meta": self.metaParser.parse,
"wsjt_demod": self.wsjtParser.parse,
"packet_demod": self.aprsParser.parse,
}
write = writers[t]
threading.Thread(target=self.pump(read_fn, write)).start()
def stop(self):
self.dsp.stop()
self.sdrSource.removeClient(self)
for sub in self.subscriptions:
sub.cancel()
self.subscriptions = []
def setProperty(self, prop, value):
self.localProps.getProperty(prop).setValue(value)
def isActive(self):
return True
def onSdrAvailable(self):
logger.debug("received onSdrAvailable, attempting DspSource restart")
self.dsp.start()
def onSdrUnavailable(self):
logger.debug("received onSdrUnavailable, shutting down DspSource")
self.dsp.stop()
class CpuUsageThread(threading.Thread):
sharedInstance = None
@staticmethod
def getSharedInstance():
if CpuUsageThread.sharedInstance is None:
CpuUsageThread.sharedInstance = CpuUsageThread()
return CpuUsageThread.sharedInstance
def __init__(self):
self.clients = []
self.doRun = True
self.last_worktime = 0
self.last_idletime = 0
self.endEvent = threading.Event()
super().__init__()
def run(self):
while self.doRun:
try:
cpu_usage = self.get_cpu_usage()
except:
cpu_usage = 0
for c in self.clients:
c.write_cpu_usage(cpu_usage)
self.endEvent.wait(timeout=3)
logger.debug("cpu usage thread shut down")
def get_cpu_usage(self):
try:
f = open("/proc/stat", "r")
except:
return 0 # Workaround, possibly we're on a Mac
line = ""
while not "cpu " in line:
line = f.readline()
f.close()
spl = line.split(" ")
worktime = int(spl[2]) + int(spl[3]) + int(spl[4])
idletime = int(spl[5])
dworktime = worktime - self.last_worktime
didletime = idletime - self.last_idletime
rate = float(dworktime) / (didletime + dworktime)
self.last_worktime = worktime
self.last_idletime = idletime
if self.last_worktime == 0:
return 0
return rate
def add_client(self, c):
self.clients.append(c)
if not self.is_alive():
self.start()
def remove_client(self, c):
try:
self.clients.remove(c)
except ValueError:
pass
if not self.clients:
self.shutdown()
def shutdown(self):
CpuUsageThread.sharedInstance = None
self.doRun = False
self.endEvent.set()
class TooManyClientsException(Exception):
pass
class ClientRegistry(object):
sharedInstance = None
creationLock = threading.Lock()
@staticmethod
def getSharedInstance():
with ClientRegistry.creationLock:
if ClientRegistry.sharedInstance is None:
ClientRegistry.sharedInstance = ClientRegistry()
return ClientRegistry.sharedInstance
def __init__(self):
self.clients = []
Metrics.getSharedInstance().addMetric("openwebrx.users", DirectMetric(self.clientCount))
super().__init__()
def broadcast(self):
n = self.clientCount()
for c in self.clients:
c.write_clients(n)
def addClient(self, client):
pm = PropertyManager.getSharedInstance()
if len(self.clients) >= pm["max_clients"]:
raise TooManyClientsException()
self.clients.append(client)
self.broadcast()
def clientCount(self):
return len(self.clients)
def removeClient(self, client):
try:
self.clients.remove(client)
except ValueError:
pass
self.broadcast()

1
owrx/version.py Normal file
View File

@ -0,0 +1 @@
openwebrx_version = "v0.18"

239
owrx/websocket.py Normal file
View File

@ -0,0 +1,239 @@
import base64
import hashlib
import json
from multiprocessing import Pipe
import select
import threading
import logging
logger = logging.getLogger(__name__)
OPCODE_TEXT_MESSAGE = 0x01
OPCODE_BINARY_MESSAGE = 0x02
OPCODE_CLOSE = 0x08
OPCODE_PING = 0x09
OPCODE_PONG = 0x0A
class IncompleteRead(Exception):
pass
class Drained(Exception):
pass
class WebSocketConnection(object):
connections = []
@staticmethod
def closeAll():
for c in WebSocketConnection.connections:
try:
c.close()
except:
logger.exception("exception while shutting down websocket connections")
def __init__(self, handler, messageHandler):
self.handler = handler
self.handler.connection.setblocking(0)
self.setMessageHandler(messageHandler)
(self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False)
self.open = True
self.sendLock = threading.Lock()
my_headers = self.handler.headers.items()
my_header_keys = list(map(lambda x: x[0], my_headers))
h_key_exists = lambda x: my_header_keys.count(x)
h_value = lambda x: my_headers[my_header_keys.index(x)][1]
if (
(not h_key_exists("Upgrade"))
or not (h_value("Upgrade") == "websocket")
or (not h_key_exists("Sec-WebSocket-Key"))
):
raise WebSocketException
ws_key = h_value("Sec-WebSocket-Key")
shakey = hashlib.sha1()
shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode())
ws_key_toreturn = base64.b64encode(shakey.digest())
self.handler.wfile.write(
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format(
ws_key_toreturn.decode()
).encode()
)
self.pingTimer = None
self.resetPing()
def setMessageHandler(self, messageHandler):
self.messageHandler = messageHandler
def get_header(self, size, opcode):
ws_first_byte = 0b10000000 | (opcode & 0x0F)
if size > 2 ** 16 - 1:
# frame size can be increased up to 2^64 by setting the size to 127
# anything beyond that would need to be segmented into frames. i don't really think we'll need more.
return bytes(
[
ws_first_byte,
127,
(size >> 56) & 0xFF,
(size >> 48) & 0xFF,
(size >> 40) & 0xFF,
(size >> 32) & 0xFF,
(size >> 24) & 0xFF,
(size >> 16) & 0xFF,
(size >> 8) & 0xFF,
size & 0xFF,
]
)
elif size > 125:
# up to 2^16 can be sent using the extended payload size field by putting the size to 126
return bytes([ws_first_byte, 126, (size >> 8) & 0xFF, size & 0xFF])
else:
# 125 bytes binary message in a single unmasked frame
return bytes([ws_first_byte, size])
def send(self, data):
# convenience
if type(data) == dict:
# allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway.
data = json.dumps(data, allow_nan=False)
# string-type messages are sent as text frames
if type(data) == str:
header = self.get_header(len(data), OPCODE_TEXT_MESSAGE)
data_to_send = header + data.encode("utf-8")
# anything else as binary
else:
header = self.get_header(len(data), OPCODE_BINARY_MESSAGE)
data_to_send = header + data
self._sendBytes(data_to_send)
def _sendBytes(self, data_to_send):
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i : i + n]
try:
with self.sendLock:
for chunk in chunks(data_to_send, 1024):
(_, write, _) = select.select([], [self.handler.wfile], [], 10)
if self.handler.wfile in write:
written = self.handler.wfile.write(chunk)
if written != len(chunk):
logger.error("incomplete write! closing socket!")
self.close()
else:
logger.debug("socket not returned from select; closing")
self.close()
# these exception happen when the socket is closed
except OSError:
logger.exception("OSError while writing data")
self.close()
except ValueError:
logger.exception("ValueError while writing data")
self.close()
def interrupt(self):
self.interruptPipeSend.send(bytes(0x00))
def read_loop(self):
def protected_read(num):
data = self.handler.rfile.read(num)
if data is None:
raise Drained()
if len(data) != num:
raise IncompleteRead()
return data
WebSocketConnection.connections.append(self)
self.open = True
while self.open:
(read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], [])
if self.handler.rfile in read:
available = True
self.resetPing()
while self.open and available:
try:
header = protected_read(2)
opcode = header[0] & 0x0F
length = header[1] & 0x7F
mask = (header[1] & 0x80) >> 7
if length == 126:
header = protected_read(2)
length = (header[0] << 8) + header[1]
if mask:
masking_key = protected_read(4)
data = protected_read(length)
if mask:
data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)])
if opcode == OPCODE_TEXT_MESSAGE:
message = data.decode("utf-8")
self.messageHandler.handleTextMessage(self, message)
elif opcode == OPCODE_BINARY_MESSAGE:
self.messageHandler.handleBinaryMessage(self, data)
elif opcode == OPCODE_PING:
self.sendPong()
elif opcode == OPCODE_PONG:
# since every read resets the ping timer, there's nothing to do here.
pass
elif opcode == OPCODE_CLOSE:
logger.debug("websocket close frame received; closing connection")
self.open = False
else:
logger.warning("unsupported opcode: {0}".format(opcode))
except Drained:
available = False
except IncompleteRead:
logger.warning("incomplete read on websocket; closing connection")
self.open = False
except TimeoutError:
logger.warning("websocket timed out; closing connection")
self.open = False
logger.debug("websocket loop ended; shutting down")
self.messageHandler.handleClose()
self.cancelPing()
logger.debug("websocket loop ended; sending close frame")
header = self.get_header(0, OPCODE_CLOSE)
self._sendBytes(header)
try:
WebSocketConnection.connections.remove(self)
except ValueError:
pass
def close(self):
self.open = False
self.interrupt()
def cancelPing(self):
if self.pingTimer:
self.pingTimer.cancel()
def resetPing(self):
self.cancelPing()
if not self.open:
logger.debug("resetPing() while closed. passing...")
return
self.pingTimer = threading.Timer(30, self.sendPing)
self.pingTimer.start()
def sendPing(self):
header = self.get_header(0, OPCODE_PING)
self._sendBytes(header)
self.resetPing()
def sendPong(self):
header = self.get_header(0, OPCODE_PONG)
self._sendBytes(header)
class WebSocketException(Exception):
pass

396
owrx/wsjt.py Normal file
View File

@ -0,0 +1,396 @@
import threading
import wave
from datetime import datetime, timedelta, date, timezone
import time
import sched
import subprocess
import os
from multiprocessing.connection import Pipe
from owrx.map import Map, LocatorLocation
import re
from queue import Queue, Full
from owrx.config import PropertyManager
from owrx.bands import Bandplan
from owrx.metrics import Metrics, CounterMetric, DirectMetric
from owrx.pskreporter import PskReporter
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class WsjtQueueWorker(threading.Thread):
def __init__(self, queue):
self.queue = queue
self.doRun = True
super().__init__(daemon=True)
def run(self) -> None:
while self.doRun:
(processor, file) = self.queue.get()
try:
logger.debug("processing file %s", file)
processor.decode(file)
except Exception:
logger.exception("failed to decode job")
self.queue.onError()
self.queue.task_done()
class WsjtQueue(Queue):
sharedInstance = None
creationLock = threading.Lock()
@staticmethod
def getSharedInstance():
with WsjtQueue.creationLock:
if WsjtQueue.sharedInstance is None:
pm = PropertyManager.getSharedInstance()
WsjtQueue.sharedInstance = WsjtQueue(maxsize=pm["wsjt_queue_length"], workers=pm["wsjt_queue_workers"])
return WsjtQueue.sharedInstance
def __init__(self, maxsize, workers):
super().__init__(maxsize)
metrics = Metrics.getSharedInstance()
metrics.addMetric("wsjt.queue.length", DirectMetric(self.qsize))
self.inCounter = CounterMetric()
metrics.addMetric("wsjt.queue.in", self.inCounter)
self.outCounter = CounterMetric()
metrics.addMetric("wsjt.queue.out", self.outCounter)
self.overflowCounter = CounterMetric()
metrics.addMetric("wsjt.queue.overflow", self.overflowCounter)
self.errorCounter = CounterMetric()
metrics.addMetric("wsjt.queue.error", self.errorCounter)
self.workers = [self.newWorker() for _ in range(0, workers)]
def put(self, item):
self.inCounter.inc()
try:
super(WsjtQueue, 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(WsjtQueue, self).get(**kwargs)
self.outCounter.inc()
return out
def newWorker(self):
worker = WsjtQueueWorker(self)
worker.start()
return worker
def onError(self):
self.errorCounter.inc()
class WsjtChopper(threading.Thread):
def __init__(self, source):
self.source = source
self.tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"]
(self.wavefilename, self.wavefile) = self.getWaveFile()
self.switchingLock = threading.Lock()
self.scheduler = sched.scheduler(time.time, time.sleep)
self.schedulerLock = threading.Lock()
(self.outputReader, self.outputWriter) = Pipe()
self.doRun = True
super().__init__()
def getWaveFile(self):
filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format(
tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.fileTimestampFormat)
)
wavefile = wave.open(filename, "wb")
wavefile.setnchannels(1)
wavefile.setsampwidth(2)
wavefile.setframerate(12000)
return filename, wavefile
def getNextDecodingTime(self):
t = datetime.now()
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.timestamp()
def startScheduler(self):
self._scheduleNextSwitch()
threading.Thread(target=self.scheduler.run).start()
def emptyScheduler(self):
with self.schedulerLock:
for event in self.scheduler.queue:
self.scheduler.cancel(event)
def _scheduleNextSwitch(self):
with self.schedulerLock:
if self.doRun:
self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles)
def switchFiles(self):
self.switchingLock.acquire()
file = self.wavefile
filename = self.wavefilename
(self.wavefilename, self.wavefile) = self.getWaveFile()
self.switchingLock.release()
file.close()
try:
WsjtQueue.getSharedInstance().put((self, filename))
except Full:
logger.warning("wsjt decoding queue overflow; dropping one file")
os.unlink(filename)
self._scheduleNextSwitch()
def decoder_commandline(self, file):
"""
must be overridden in child classes
"""
return []
def decode(self, file):
decoder = subprocess.Popen(
self.decoder_commandline(file), stdout=subprocess.PIPE, cwd=self.tmp_dir, preexec_fn=lambda: os.nice(10)
)
while True:
line = decoder.stdout.readline()
if line is None or (isinstance(line, bytes) and len(line) == 0):
break
self.outputWriter.send(line)
rc = decoder.wait()
if rc != 0:
logger.warning("decoder return code: %i", rc)
os.unlink(file)
def run(self) -> None:
logger.debug("WSJT chopper starting up")
self.startScheduler()
while self.doRun:
data = self.source.read(256)
if data is None or (isinstance(data, bytes) and len(data) == 0):
self.doRun = False
else:
self.switchingLock.acquire()
self.wavefile.writeframes(data)
self.switchingLock.release()
logger.debug("WSJT chopper shutting down")
self.outputReader.close()
self.outputWriter.close()
self.emptyScheduler()
try:
os.unlink(self.wavefilename)
except Exception:
logger.exception("error removing undecoded file")
def read(self):
try:
return self.outputReader.recv()
except EOFError:
return None
def decoding_depth(self, mode):
pm = PropertyManager.getSharedInstance()
# mode-specific setting?
if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]:
return pm["wsjt_decoding_depths"][mode]
# return global default
if "wsjt_decoding_depth" in pm:
return pm["wsjt_decoding_depth"]
# default when no setting is provided
return 3
class Ft8Chopper(WsjtChopper):
def __init__(self, source):
self.interval = 15
self.fileTimestampFormat = "%y%m%d_%H%M%S"
super().__init__(source)
def decoder_commandline(self, file):
return ["jt9", "--ft8", "-d", str(self.decoding_depth("ft8")), file]
class WsprChopper(WsjtChopper):
def __init__(self, source):
self.interval = 120
self.fileTimestampFormat = "%y%m%d_%H%M"
super().__init__(source)
def decoder_commandline(self, file):
cmd = ["wsprd"]
if self.decoding_depth("wspr") > 1:
cmd += ["-d"]
cmd += [file]
return cmd
class Jt65Chopper(WsjtChopper):
def __init__(self, source):
self.interval = 60
self.fileTimestampFormat = "%y%m%d_%H%M"
super().__init__(source)
def decoder_commandline(self, file):
return ["jt9", "--jt65", "-d", str(self.decoding_depth("jt65")), file]
class Jt9Chopper(WsjtChopper):
def __init__(self, source):
self.interval = 60
self.fileTimestampFormat = "%y%m%d_%H%M"
super().__init__(source)
def decoder_commandline(self, file):
return ["jt9", "--jt9", "-d", str(self.decoding_depth("jt9")), file]
class Ft4Chopper(WsjtChopper):
def __init__(self, source):
self.interval = 7.5
self.fileTimestampFormat = "%y%m%d_%H%M%S"
super().__init__(source)
def decoder_commandline(self, file):
return ["jt9", "--ft4", "-d", str(self.decoding_depth("ft4")), file]
class WsjtParser(object):
def __init__(self, handler):
self.handler = handler
self.dial_freq = None
self.band = None
modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"}
def parse(self, data):
try:
msg = data.decode().rstrip()
# known debug messages we know to skip
if msg.startswith("<DecodeFinished>"):
return
if msg.startswith(" EOF on input file"):
return
modes = list(WsjtParser.modes.keys())
if msg[21] in modes or msg[19] in modes:
decoder = Jt9Decoder()
else:
decoder = WsprDecoder()
out = decoder.parse(msg, self.dial_freq)
if "mode" in out:
self.pushDecode(out["mode"])
if "callsign" in out and "locator" in out:
Map.getSharedInstance().updateLocation(
out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band
)
PskReporter.getSharedInstance().spot(out)
self.handler.write_wsjt_message(out)
except ValueError:
logger.exception("error while parsing wsjt message")
def pushDecode(self, mode):
metrics = Metrics.getSharedInstance()
band = "unknown"
if self.band is not None:
band = self.band.getName()
if band is None:
band = "unknown"
if mode is None:
mode = "unknown"
name = "wsjt.decodes.{band}.{mode}".format(band=band, mode=mode)
metric = metrics.getMetric(name)
if metric is None:
metric = CounterMetric()
metrics.addMetric(name, metric)
metric.inc()
def setDialFrequency(self, freq):
self.dial_freq = freq
self.band = Bandplan.getSharedInstance().findBand(freq)
class Decoder(object):
def parse_timestamp(self, instring, dateformat):
ts = datetime.strptime(instring, dateformat)
return int(
datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000
)
class Jt9Decoder(Decoder):
locator_pattern = re.compile("[A-Z0-9]+\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$")
def parse(self, msg, dial_freq):
# ft8 sample
# '222100 -15 -0.0 508 ~ CQ EA7MJ IM66'
# jt65 sample
# '2352 -7 0.4 1801 # R0WAS R2ABM KO85'
# '0003 -4 0.4 1762 # CQ R2ABM KO85'
modes = list(WsjtParser.modes.keys())
if msg[19] in modes:
dateformat = "%H%M"
else:
dateformat = "%H%M%S"
timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat)
msg = msg[len(dateformat) + 1 :]
modeChar = msg[14:15]
mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown"
wsjt_msg = msg[17:53].strip()
result = {
"timestamp": timestamp,
"db": float(msg[0:3]),
"dt": float(msg[4:8]),
"freq": dial_freq + int(msg[9:13]),
"mode": mode,
"msg": wsjt_msg,
}
result.update(self.parseMessage(wsjt_msg))
return result
def parseMessage(self, msg):
m = Jt9Decoder.locator_pattern.match(msg)
if m is None:
return {}
# this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very
# likely this just means roger roger goodbye.
if m.group(2) == "RR73":
return {"callsign": m.group(1)}
return {"callsign": m.group(1), "locator": m.group(2)}
class WsprDecoder(Decoder):
wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")
def parse(self, msg, dial_freq):
# wspr sample
# '2600 -24 0.4 0.001492 -1 G8AXA JO01 33'
# '0052 -29 2.6 0.001486 0 G02CWT IO92 23'
wsjt_msg = msg[29:].strip()
result = {
"timestamp": self.parse_timestamp(msg[0:4], "%H%M"),
"db": float(msg[5:8]),
"dt": float(msg[9:13]),
"freq": dial_freq + int(float(msg[14:24]) * 1e6),
"drift": int(msg[25:28]),
"mode": "WSPR",
"msg": wsjt_msg,
}
result.update(self.parseMessage(wsjt_msg))
return result
def parseMessage(self, msg):
m = WsprDecoder.wspr_splitter_pattern.match(msg)
if m is None:
return {}
return {"callsign": m.group(1), "locator": m.group(2)}

8
push.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -euxo pipefail
ARCH=$(uname -m)
for image in openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-full openwebrx; do
docker push jketterl/$image:$ARCH
done

171
rxws.py
View File

@ -1,171 +0,0 @@
"""
rxws: WebSocket methods implemented 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>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import base64
import sha
import select
import code
class WebSocketException(Exception):
pass
def handshake(myself):
my_client_id=myself.path[4:]
my_headers=myself.headers.items()
my_header_keys=map(lambda x:x[0],my_headers)
h_key_exists=lambda x:my_header_keys.count(x)
h_value=lambda x:my_headers[my_header_keys.index(x)][1]
#print "The Lambdas(tm)"
#print h_key_exists("upgrade")
#print h_value("upgrade")
#print h_key_exists("sec-websocket-key")
if (not h_key_exists("upgrade")) or not (h_value("upgrade")=="websocket") or (not h_key_exists("sec-websocket-key")):
raise WebSocketException
ws_key=h_value("sec-websocket-key")
ws_key_toreturn=base64.b64encode(sha.new(ws_key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest())
#A sample list of keys we get: [('origin', 'http://localhost:8073'), ('upgrade', 'websocket'), ('sec-websocket-extensions', 'x-webkit-deflate-frame'), ('sec-websocket-version', '13'), ('host', 'localhost:8073'), ('sec-websocket-key', 't9J1rgy4fc9fg2Hshhnkmg=='), ('connection', 'Upgrade'), ('pragma', 'no-cache'), ('cache-control', 'no-cache')]
myself.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "+ws_key_toreturn+"\r\nCQ-CQ-de: HA5KFU\r\n\r\n")
def get_header(size):
#this does something similar: https://github.com/lemmingzshadow/php-websocket/blob/master/server/lib/WebSocket/Connection.php
ws_first_byte=0b10000010 # FIN=1, OP=2
if(size>125):
ws_second_byte=126 # The following two bytes will indicate frame size
extended_size=chr((size>>8)&0xff)+chr(size&0xff) #Okay, it uses reverse byte order (little-endian) compared to anything else sent on TCP
else:
ws_second_byte=size
#256 bytes binary message in a single unmasked frame | 0x82 0x7E 0x0100 [256 bytes of binary data]
extended_size=""
return chr(ws_first_byte)+chr(ws_second_byte)+extended_size
def code_payload(data, masking_key=""):
# both encode or decode
if masking_key=="":
key = (61, 84, 35, 6)
else:
key = [ord(i) for i in masking_key]
encoded=""
for i in range(0,len(data)):
encoded+=chr(ord(data[i])^key[i%4])
return encoded
def xxdg(data):
output=""
for i in range(0,len(data)/8):
output+=xxd(data[i:i+8])
if i%2: output+="\n"
else: output+=" "
return output
def xxd(data):
#diagnostic purposes only
output=""
for d in data:
output+=hex(ord(d))[2:].zfill(2)+" "
return output
#for R/W the WebSocket, use recv/send
#for reading the TCP socket, use readsock
#for writing the TCP socket, use myself.wfile.write and flush
def readsock(myself,size,blocking):
#http://thenestofheliopolis.blogspot.hu/2011/01/how-to-implement-non-blocking-two-way.html
if blocking:
return myself.rfile.read(size)
else:
poll = select.poll()
poll.register(myself.rfile.fileno(), select.POLLIN or select.POLLPRI)
fd = poll.poll(0) #timeout is 0
if len(fd):
f = fd[0]
if f[1] > 0:
return myself.rfile.read(size)
return ""
def recv(myself, blocking=False, debug=False):
bufsize=70000
#myself.connection.setblocking(blocking) #umm... we cannot do that with rfile
if debug: print "ws_recv begin"
try:
data=readsock(myself,6,blocking)
#print "rxws.recv bytes:",xxd(data)
except:
if debug: print "ws_recv error"
return ""
if debug: print "ws_recv recved"
if(len(data)==0): return ""
fin=ord(data[0])&128!=0
is_text_frame=ord(data[0])&15==1
length=ord(data[1])&0x7f
data+=readsock(myself,length,blocking)
#print "rxws.recv length is ",length," (multiple packets together?) len(data) =",len(data)
has_one_byte_length=length<125
masked=ord(data[1])&0x80!=0
#print "len=", length, len(data)-2
#print "fin, is_text_frame, has_one_byte_length, masked = ", (fin, is_text_frame, has_one_byte_length, masked)
#print xxd(data)
if fin and is_text_frame and has_one_byte_length:
if masked:
return code_payload(data[6:], data[2:6])
else:
return data[2:]
#Useful links for ideas on WebSockets:
# http://stackoverflow.com/questions/8125507/how-can-i-send-and-receive-websocket-messages-on-the-server-side
# https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server
# http://tools.ietf.org/html/rfc6455#section-5.2
def flush(myself):
myself.wfile.flush()
#or the socket, not the rfile:
#lR,lW,lX = select.select([],[myself.connection,],[],60)
def send(myself, data, begin_id="", debug=0):
base_frame_size=35000 #could guess by MTU?
debug=0
#try:
while True:
counter=0
from_end=len(data)-counter
if from_end+len(begin_id)>base_frame_size:
data_to_send=begin_id+data[counter:counter+base_frame_size-len(begin_id)]
header=get_header(len(data_to_send))
flush(myself)
myself.wfile.write(header+data_to_send)
flush(myself)
if debug: print "rxws.send ==================== #1 if branch :: from={0} to={1} dlen={2} hlen={3}".format(counter,counter+base_frame_size-len(begin_id),len(data_to_send),len(header))
else:
data_to_send=begin_id+data[counter:]
header=get_header(len(data_to_send))
flush(myself)
myself.wfile.write(header+data_to_send)
flush(myself)
if debug: print "rxws.send :: #2 else branch :: dlen={0} hlen={1}".format(len(data_to_send),len(header))
#if debug: print "header:\n"+xxdg(header)+"\n\nws data:\n"+xxdg(data_to_send)
break
counter+=base_frame_size-len(begin_id)
#except:
# pass

View File

@ -20,31 +20,12 @@
""" """
import config_webrx as cfg, time, subprocess from owrx.sdrhu import SdrHuUpdater
from owrx.config import PropertyManager
def run(continuously=True):
if not cfg.sdrhu_key: return
firsttime="(Your receiver is soon getting listed on sdr.hu!)"
while True:
cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://"+cfg.server_hostname+":"+str(cfg.web_port)+"&apikey="+cfg.sdrhu_key+"\" 2>&1"
print "[openwebrx-sdrhu]", cmd
returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
returned=returned[0]
#print returned
if "UPDATE:" in returned:
retrytime_mins = 20
value=returned.split("UPDATE:")[1].split("\n",1)[0]
if value.startswith("SUCCESS"):
print "[openwebrx-sdrhu] Update succeeded! "+firsttime
firsttime=""
else:
print "[openwebrx-sdrhu] Update failed, your receiver cannot be listed on sdr.hu! Reason:", value
else:
retrytime_mins = 2
print "[openwebrx-sdrhu] wget failed while updating, your receiver cannot be listed on sdr.hu!"
if not continuously: break
time.sleep(60*retrytime_mins)
if __name__ == "__main__": if __name__ == "__main__":
run(False) pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
if not "sdrhu_key" in pm:
exit(1)
SdrHuUpdater().update()