Compare commits
676 Commits
Author | SHA1 | Date | |
---|---|---|---|
0c9d37e381 | |||
dc848a7006 | |||
093ad6cd0d | |||
fd26acca68 | |||
3daf005c81 | |||
1b31c5fc90 | |||
0206a6f94c | |||
484b829b90 | |||
ad8877f83c | |||
e205953bfc | |||
8a7182f9d5 | |||
f86487f459 | |||
7fc7fe5e82 | |||
3057c3ffd7 | |||
282ba4d095 | |||
1b4b87b14e | |||
55254b1c44 | |||
cd935c0dcb | |||
a17690dc91 | |||
fe1a1207e6 | |||
041e8930bf | |||
d9fe604171 | |||
290f67735d | |||
0fa8774493 | |||
53c5c0f045 | |||
11568256ed | |||
2152184bf9 | |||
f8971ac704 | |||
540198b12a | |||
48d498941e | |||
318cb728e1 | |||
f481c3f8e3 | |||
af553c422d | |||
7115d5c951 | |||
7642341b2e | |||
29bce9e07a | |||
35dcff90ea | |||
bc193c834c | |||
3bc39a9ca3 | |||
4a77d2cc38 | |||
a7e2aae292 | |||
c6e01eed1a | |||
118335b2b6 | |||
0c7b0d2eaa | |||
cb8ec3c760 | |||
e408c66702 | |||
d97d66c787 | |||
96ada02e38 | |||
ae729990ca | |||
afc4fc2d00 | |||
25d04f4cbc | |||
5a60869f8e | |||
7962da9454 | |||
4691987cc4 | |||
05985ff46a | |||
159c231884 | |||
86e64225bd | |||
1156916631 | |||
a6ed578a0f | |||
8c5546ad90 | |||
f3ed4a719a | |||
2da2a57e13 | |||
6de91c0c4e | |||
cc3e43c6cd | |||
d04cf5f5a1 | |||
b7e38960c0 | |||
1e684f9bf1 | |||
259d036083 | |||
71b0fa968b | |||
6ad3a80fc6 | |||
b1cfe79ddd | |||
5e6508cd47 | |||
5f5881cdfa | |||
f6b0e37664 | |||
1bc5633b27 | |||
1c23fdf3ff | |||
bd29f9c572 | |||
89cd17042a | |||
8b5cf9983e | |||
04a5e6705f | |||
77de488521 | |||
52b535c608 | |||
05ea11f5d1 | |||
e8cf014903 | |||
1968e15237 | |||
da698e7a3c | |||
b9db64d4f9 | |||
51af299aa2 | |||
440b3a3822 | |||
5ec0005f81 | |||
11b0d2d90a | |||
322a52e854 | |||
1b8153c461 | |||
dae32f2e95 | |||
b4c2923dd2 | |||
68739724d4 | |||
4993a56235 | |||
cb3cb50cbd | |||
7e4671afe4 | |||
19c8432371 | |||
9351e4793c | |||
1f91908e06 | |||
907359df82 | |||
e210c3a667 | |||
9c4d7377d0 | |||
8ce1192811 | |||
d18a4c83ac | |||
bbad34cec3 | |||
22ec80c8ea | |||
5487861da1 | |||
ebd4d93908 | |||
fcbaa4f22a | |||
c0ca216e4d | |||
a9990f1f41 | |||
b877d8439a | |||
6cca37a9df | |||
7a2f62a307 | |||
1932890dd0 | |||
02e699c597 | |||
46d742a12c | |||
b3e99e0a3d | |||
96cce831ef | |||
3e00a4f390 | |||
0abd121fda | |||
b605927207 | |||
3696272ef7 | |||
5a7c12dfac | |||
170b720e48 | |||
c6962b4f42 | |||
8e7b758ef8 | |||
1b9e77982d | |||
2d142e45ed | |||
620ba11565 | |||
e297cffbfe | |||
af211739fb | |||
a86a2f31cd | |||
6796699e35 | |||
df72147b93 | |||
65443eb0ba | |||
29c0f7148a | |||
e1dd9d32f4 | |||
287a04be94 | |||
20cd3f6efe | |||
69237c0bb4 | |||
383c08ed48 | |||
19496d46a3 | |||
6ddced4689 | |||
4cbce9c840 | |||
b01792c3d2 | |||
5f7daba3b2 | |||
a90f77e545 | |||
d50d08ad2c | |||
deeaccba12 | |||
62e67afc9c | |||
c9d303c43e | |||
5fc8672dd6 | |||
acee318dae | |||
8fa1796037 | |||
2a82f4e452 | |||
341e254640 | |||
d872152cc8 | |||
3b9763eee5 | |||
cfeab98620 | |||
792f76f831 | |||
c58ebfa657 | |||
c50473fea5 | |||
f1619b81fe | |||
364c7eb505 | |||
9dcf342b13 | |||
d573561c67 | |||
37e7331627 | |||
b25a673829 | |||
916f19ac60 | |||
620771eaf2 | |||
161408dbf4 | |||
e0985c3802 | |||
3d20e3ed80 | |||
6af0ad0262 | |||
b4460f4f70 | |||
ff9f771e1b | |||
4c5ec23ba7 | |||
1b44229ec3 | |||
2e28694b49 | |||
2ba2ec38e0 | |||
a3cfde02c4 | |||
a14f247859 | |||
45e9bd12a5 | |||
190c90ccdf | |||
60df3afe26 | |||
4e14b29537 | |||
3814200452 | |||
a9dbedee6d | |||
8671f98c14 | |||
400ed3541d | |||
03315d7960 | |||
d123232f28 | |||
eab1c6ce80 | |||
fdbb76bca1 | |||
c0b7cf5f8d | |||
37d89c074b | |||
2b1dc76e48 | |||
e0b289b6a5 | |||
d81f0ae96c | |||
6bd47cf914 | |||
c7db144f7b | |||
d0ddf72b10 | |||
92cce78320 | |||
1871fc359a | |||
a92ead3261 | |||
094f470ebb | |||
06b6054071 | |||
0537e23e38 | |||
7a0c934af5 | |||
e787336fc4 | |||
71acad3b4f | |||
c389d3b619 | |||
ccdb010e9d | |||
6a9bbf7bc9 | |||
ccba3e8597 | |||
beb3d696c9 | |||
54142f4f15 | |||
b6ed06dff4 | |||
36c4a16fb5 | |||
1b44c31a89 | |||
45d4d868d7 | |||
e9cb5d54be | |||
7dcafab2c1 | |||
baef88bd94 | |||
ad3ed1e626 | |||
0a76801a03 | |||
3164683e74 | |||
4e7f02fc2c | |||
0231d98ab8 | |||
6822475674 | |||
412e0a51c7 | |||
91c4d6f568 | |||
d8b3974728 | |||
5cd9d386a6 | |||
f6f0a87002 | |||
8c767be53a | |||
bccb87e660 | |||
0c1dc70217 | |||
388d9d46fe | |||
2785f43c6a | |||
45a70a1079 | |||
2d823b2945 | |||
65758a0098 | |||
ea96038201 | |||
ed3d84b974 | |||
710a18aae3 | |||
f69d78926e | |||
4199a583f8 | |||
dfaecdb357 | |||
631232fe7c | |||
f9772faa6f | |||
4e32d724c4 | |||
c5df6a1527 | |||
ed258cc9a0 | |||
437943c26c | |||
d15d9d8c76 | |||
436010ffe3 | |||
679f99d701 | |||
1eff7a3b69 | |||
54a34b2084 | |||
f8beae5f46 | |||
9beb3b9168 | |||
770fd749cd | |||
683a711b49 | |||
bd31fa5149 | |||
7f3d421b25 | |||
44250f9719 | |||
c2e8ac516c | |||
dd5ab32b47 | |||
361ed55b93 | |||
8b24eff72e | |||
18e8ca5e43 | |||
0ab6729fcc | |||
0e64f15e65 | |||
058463a9b3 | |||
bd7e5b7166 | |||
d0d946e09f | |||
86278ff44d | |||
039b57d28b | |||
27c16c3720 | |||
3aa238727e | |||
4316832b95 | |||
bec61465c9 | |||
012952f6f3 | |||
872c7a4bfd | |||
d65743f2ea | |||
c5585e290a | |||
54fde2c1c0 | |||
d612792593 | |||
0d77aaff26 | |||
b06a629ffb | |||
a29d72d67f | |||
1a6f738c97 | |||
50e19085b0 | |||
e70ff075ca | |||
34b369b200 | |||
fc5d560345 | |||
e8ad4588ce | |||
74aea63b9b | |||
a750726459 | |||
eb8b8c4a5a | |||
1956907d6d | |||
8f49337b81 | |||
5e37b75cfb | |||
c09f17579c | |||
06d4b24b09 | |||
9492bbebbb | |||
ad5166cf9e | |||
0714ce5703 | |||
2eec29db05 | |||
3122077603 | |||
518588885c | |||
8271eddefb | |||
404f995e39 | |||
8fcfa689ae | |||
f488a01c78 | |||
06361754b3 | |||
b7688c3c97 | |||
691d88f841 | |||
9aebeb51f8 | |||
8d2763930b | |||
409370aba2 | |||
9175629838 | |||
3c0a26eaa8 | |||
496e771e17 | |||
c8496a2547 | |||
d3ba866800 | |||
8267aa8d9d | |||
c2617fcfaf | |||
1112334ea8 | |||
578f165bdc | |||
a664770881 | |||
c0193e677c | |||
819790cbc8 | |||
b2d4046d8a | |||
28b1abfa40 | |||
a72a11d3c7 | |||
2d37f63f2c | |||
48a9c76c18 | |||
7f9c0539bb | |||
e61dde7d0e | |||
d998ab5c61 | |||
49640b5e33 | |||
391069653a | |||
830d7ae656 | |||
48c594fdae | |||
29a161b7b7 | |||
9b1659d3dd | |||
dbf23baa45 | |||
3d97d362b5 | |||
8ea4d11e9c | |||
48f26d00d6 | |||
3b60e0b737 | |||
3e4ba42aab | |||
cda43b5c5c | |||
ae76470612 | |||
5e51beac46 | |||
8acfb8c1cf | |||
ad0ca114f5 | |||
3f3f5eacfe | |||
dd2fda54d1 | |||
7d88d83c36 | |||
5068bcd347 | |||
024a6684ce | |||
aad757df36 | |||
690eed5d58 | |||
c3d459558a | |||
fb457ce9f1 | |||
a8c93fd8d1 | |||
f23fa59ac3 | |||
e926611307 | |||
1cc4b13ba6 | |||
fdfaed005b | |||
0cf67d5e2c | |||
0fd172edc3 | |||
64f827d235 | |||
1e72485425 | |||
7097dc1cd8 | |||
8cf9b509c1 | |||
17c20d12e0 | |||
8422a33081 | |||
75418baf06 | |||
9f17c941d1 | |||
779aa33a4a | |||
7aa0f8b35d | |||
3b670016be | |||
ad5daaae95 | |||
16d0e1a0d7 | |||
4df5f19bd6 | |||
a1c024bfe2 | |||
2d72055070 | |||
331e9627d6 | |||
ed6594401c | |||
d9578cc5f4 | |||
2c6b0e3d30 | |||
b0c7abe362 | |||
346f2af2fb | |||
902fc666c2 | |||
3a1e5ee73c | |||
a083042002 | |||
ce48892173 | |||
5cfacac6c0 | |||
4758672c94 | |||
23fceb2998 | |||
e5bd78fd0c | |||
8c4b9dd08a | |||
0517a59308 | |||
ba3a68c3fa | |||
d920540021 | |||
47ecc26f28 | |||
689cd49694 | |||
b60a8a1af0 | |||
8de70cd523 | |||
25db7c716d | |||
88020b894e | |||
ee687d4e27 | |||
b318b5e88a | |||
8a25718d29 | |||
617bed91c4 | |||
9357d57a28 | |||
5d291b5b36 | |||
01c58327aa | |||
635bf55465 | |||
732985c529 | |||
9c5858e1e5 | |||
1fed499b7f | |||
d99669b3aa | |||
e548d6a5de | |||
8806dc538e | |||
f6f01ebee5 | |||
1d9ab1494f | |||
7054ec5d59 | |||
d72027e630 | |||
99fe232a21 | |||
dd2f0629d3 | |||
ffcf5c0c27 | |||
3226c01f60 | |||
54fb58755d | |||
d9b662106c | |||
53faca64c0 | |||
c23acc1513 | |||
8e4716f241 | |||
e8fca853df | |||
d6d6d97a13 | |||
e66be7c12d | |||
56a42498a5 | |||
bda718cbee | |||
13eaee5ee9 | |||
44270af88f | |||
bb680293a1 | |||
1ee75295e5 | |||
5e1c4391c6 | |||
998092f377 | |||
dea07cd49b | |||
e3f99d6985 | |||
081b63def3 | |||
3c91f3cc2f | |||
61a5250792 | |||
881637811f | |||
142ca578ec | |||
ad8ff1c2f7 | |||
8372f198db | |||
2a5448f5c1 | |||
c8695a8e62 | |||
9b2947827a | |||
bee0f67efd | |||
612345f0b2 | |||
4a86af69d1 | |||
bf31a27dca | |||
a5bdf6c3ac | |||
9258e76468 | |||
1d9b2729ef | |||
999d32fd8a | |||
642552cc08 | |||
a0d219d120 | |||
68a1abd37e | |||
bcab2b2288 | |||
b8868cb55a | |||
f29f7b20e3 | |||
ae1287b8a2 | |||
185fdb67cb | |||
0ed69ef2f7 | |||
655b6849b7 | |||
39757b00b2 | |||
64b7b485b3 | |||
f0dc2f8ebe | |||
55e1aa5857 | |||
fe45d139ad | |||
181855e7a4 | |||
5d3d6423ed | |||
6e60247026 | |||
6e416d0839 | |||
23bf1df72a | |||
413c02f272 | |||
502d324cd4 | |||
3246e5ab3a | |||
c59c5b76d8 | |||
e917b920c8 | |||
a0eeea8fe3 | |||
0f81964598 | |||
9c52219ca3 | |||
8a73f2c9df | |||
98da3a6d99 | |||
667fe596dc | |||
f3444a4edb | |||
946866319c | |||
8be0092f61 | |||
3f94832d00 | |||
41f9407024 | |||
13215960c4 | |||
9f702f5d14 | |||
992a5c33a2 | |||
ae217f9ded | |||
00631d7349 | |||
163ebcd327 | |||
a31b246924 | |||
a8ef3a0e6a | |||
b9f0c91ced | |||
966a404700 | |||
885e361bab | |||
a65f15869b | |||
1b36baad88 | |||
3273716706 | |||
2c3586a92a | |||
74a4f5b272 | |||
747a5ce7ef | |||
e3aa3fa4c6 | |||
132bd2b445 | |||
2334ad1d5b | |||
57efdff43e | |||
c5323f8d54 | |||
7f3071336b | |||
db98590985 | |||
a90ef4efec | |||
b27c03c1c4 | |||
502546f9d3 | |||
113c06fae4 | |||
73b75edc14 | |||
5337c20744 | |||
57e5923a4d | |||
9d89cbceed | |||
44f4532452 | |||
c1245308bd | |||
a1cbc45b88 | |||
90f319ebda | |||
9674af10ce | |||
5a77b6a8e5 | |||
53553fcce2 | |||
1730ef27da | |||
57a6db5df2 | |||
32fe01f128 | |||
b85d801121 | |||
daa499ab93 | |||
68fcb8522e | |||
341b94b9ff | |||
f4b9decd23 | |||
cf0c6e7f9d | |||
29703d10b2 | |||
abb0813948 | |||
2c3146314b | |||
eb34c45145 | |||
993aa87776 | |||
71043d4305 | |||
eb981c04e9 | |||
ecf934864a | |||
686eeb706b | |||
94575d2212 | |||
ca9e9601ab | |||
06f3499b6d | |||
db3d662dae | |||
dee050f338 | |||
ae00a14a35 | |||
86fdbe45e9 | |||
b04dcc18d0 | |||
1cc88ff362 | |||
3435052e27 | |||
4c3d037e58 | |||
f83790a5be | |||
11bb04419b | |||
519b02da79 | |||
fdbbbcb64c | |||
0fb4ae4fc0 | |||
181511bc8e | |||
e062412e60 | |||
bdb6d75f83 | |||
433111124f | |||
23080dbe22 | |||
05096c2a16 | |||
5559cded85 | |||
9f45e8880a | |||
dc128662da | |||
dc3fd24903 | |||
b2efa81b0d | |||
2c04d40c53 | |||
fcff9d16ff | |||
eef61f9d1e | |||
8f9f9e8397 | |||
d0e7747c7f | |||
9e45cfd02a | |||
aa66e69c15 | |||
9bf4b149aa | |||
5474973752 | |||
3e30ab57a6 | |||
9d6099b6d8 | |||
a7f667779a | |||
f71240c9a6 | |||
f8fc61e9bd | |||
a8011e3a1a | |||
e8fcf05775 | |||
cfb6fb5b30 | |||
fb68ca3c66 | |||
6af19f44e8 | |||
3291dbe8d2 | |||
efac5b0449 | |||
519155a12f | |||
603c3df1b6 | |||
05ca541a8e | |||
917884b5f5 | |||
22a2bd1de1 | |||
af4923c741 | |||
ac4401175f | |||
71c649b016 | |||
cbdb143966 | |||
8c105b0c40 | |||
8e760a0fcc | |||
6f46e4d376 | |||
bee6ddc843 | |||
a3fd931931 | |||
e2fa293c74 | |||
c4ed481ce2 | |||
e6ea3832fc | |||
9a8c0ce442 | |||
49ec66e27c | |||
2b6456168e | |||
e6cbe6ffc8 | |||
00d496086e | |||
1894ed50d1 | |||
7ad5ca03b0 | |||
b380187453 | |||
2022c53fad | |||
46b7660e2d | |||
e90b10abfd | |||
a8bd13f7e6 | |||
daf2848c4d | |||
0614637342 | |||
865ffb28af | |||
8b89d1e062 | |||
e4cf95856e | |||
74be25f656 | |||
b5d56eaec2 | |||
8bb6e91597 | |||
d72f2d9e5c | |||
781b4383d6 | |||
017bbc3748 | |||
69a5e0bc5d | |||
2579b9be26 | |||
9bfef01438 | |||
c0d4b2f6a5 | |||
529e9c3c60 | |||
504c256b3e | |||
91572c56e2 | |||
3b229b95b6 | |||
0f4b8dc794 | |||
e700f0a9e4 | |||
c85400063c | |||
dc03639cad | |||
e6a04aa5e9 | |||
1bc3830e5e | |||
93f7195429 | |||
d04e0d2a2a | |||
259eef2e68 | |||
325eab35a9 |
24
CHANGELOG.md
@ -1,3 +1,27 @@
|
||||
**1.0.0**
|
||||
- Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level
|
||||
- Removed `port` configuration option; `rtltcp_compat` takes the port number with the new connectors
|
||||
- Added support for new WSJT-X modes FST4, FST4W (only available with WSJT-X 2.3) and Q65 (only avilable with
|
||||
WSJT-X 2.4)
|
||||
- Added support for demodulating M17 digital voice signals using m17-cxx-demod
|
||||
- New reporting infrastructure, allowing WSPR and FST4W spots to be sent to wsprnet.org
|
||||
- Add some basic filtering capabilities to the map
|
||||
- New arguments to the `openwebrx` command-line to facilitate the administration of users (try `openwebrx admin`)
|
||||
- Default bandwidth changes:
|
||||
- "WFM" changed to 150kHz
|
||||
- "Packet" (APRS) changed to 12.5kHz
|
||||
- Configuration rework:
|
||||
- New: fully web-based configuration interface
|
||||
- System configuration parameters have been moved to a new, separate `openwebrx.conf` file
|
||||
- Remaining parameters are now editable in the web configuration
|
||||
- Existing `config_webrx.py` files will still be read, but changes made in the web configuration will be written to
|
||||
a new storage system
|
||||
- Added upload of avatar and panorama image via web configuration
|
||||
- New devices supported:
|
||||
- HPSDR devices (Hermes Lite 2) thanks to @jancona
|
||||
- BBRF103 / RX666 / RX888 devices supported by libsddc
|
||||
- R&S devices using the EB200 or Ammos protocols
|
||||
|
||||
**0.20.3**
|
||||
- Fix a compatibility issue with python versions <= 3.6
|
||||
|
||||
|
13
README.md
@ -11,11 +11,17 @@ It has the following features:
|
||||
- filter passband can be set from GUI
|
||||
- it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas
|
||||
- it works in Google Chrome, Chromium and Mozilla Firefox
|
||||
- currently supports RTL-SDR, HackRF, SDRplay, AirSpy, LimeSDR, PlutoSDR
|
||||
- supports a wide range of [SDR hardware](https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices)
|
||||
- Multiple SDR devices can be used simultaneously
|
||||
- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag)
|
||||
- [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN)
|
||||
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9)
|
||||
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4,
|
||||
FST4W)
|
||||
- [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets
|
||||
- [JS8Call](http://js8call.com/) support
|
||||
- [DRM](https://github.com/jketterl/openwebrx/wiki/DRM-demodulator-notes) support
|
||||
- [FreeDV](https://github.com/jketterl/openwebrx/wiki/FreeDV-demodulator-notes) support
|
||||
- M17 support based on [m17-cxx-demod](https://github.com/mobilinkd/m17-cxx-demod)
|
||||
|
||||
## Setup
|
||||
|
||||
@ -35,6 +41,9 @@ If you have trouble setting up or configuring your receiver, you have some great
|
||||
you just generally want to have some OpenWebRX-related chat, come visit us over on
|
||||
[our groups.io group](https://groups.io/g/openwebrx).
|
||||
|
||||
If you want to hang out, chat, or get in touch directly with the developers, receiver operators or users, feel free to
|
||||
drop by in [our Discord server](https://discord.gg/gnE9hPz).
|
||||
|
||||
## Usage tips
|
||||
|
||||
You can zoom the waterfall display by the mouse wheel. You can also drag the waterfall to pan across it.
|
||||
|
168
bands.json
@ -1,4 +1,24 @@
|
||||
[
|
||||
{
|
||||
"name": "2190m",
|
||||
"lower_bound": 135700,
|
||||
"upper_bound": 137800,
|
||||
"frequencies": {
|
||||
"fst4": 136000,
|
||||
"fst4w": 136000
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "630m",
|
||||
"lower_bound": 472000,
|
||||
"upper_bound": 479000,
|
||||
"frequencies": {
|
||||
"fst4": 474200,
|
||||
"fst4w": 474200
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "160m",
|
||||
"lower_bound": 1810000,
|
||||
@ -9,8 +29,11 @@
|
||||
"wspr": 1836600,
|
||||
"jt65": 1838000,
|
||||
"jt9": 1839000,
|
||||
"js8": 1842000
|
||||
}
|
||||
"js8": 1842000,
|
||||
"fst4": 1839000,
|
||||
"fst4w": 1836800
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "80m",
|
||||
@ -24,7 +47,8 @@
|
||||
"jt9": 3572000,
|
||||
"ft4": [3568000, 3575000],
|
||||
"js8": 3578000
|
||||
}
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "60m",
|
||||
@ -33,7 +57,8 @@
|
||||
"frequencies": {
|
||||
"ft8": 5357000,
|
||||
"wspr": 5364700
|
||||
}
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "40m",
|
||||
@ -47,7 +72,8 @@
|
||||
"jt9": 7078000,
|
||||
"ft4": 7047500,
|
||||
"js8": 7078000
|
||||
}
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "30m",
|
||||
@ -61,7 +87,8 @@
|
||||
"jt9": 10140000,
|
||||
"ft4": 10140000,
|
||||
"js8": 10130000
|
||||
}
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "20m",
|
||||
@ -75,7 +102,8 @@
|
||||
"jt9": 14078000,
|
||||
"ft4": 14080000,
|
||||
"js8": 14078000
|
||||
}
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "17m",
|
||||
@ -89,7 +117,8 @@
|
||||
"jt9": 18104000,
|
||||
"ft4": 18104000,
|
||||
"js8": 18104000
|
||||
}
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "15m",
|
||||
@ -103,7 +132,8 @@
|
||||
"jt9": 21078000,
|
||||
"ft4": 21140000,
|
||||
"js8": 21078000
|
||||
}
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "12m",
|
||||
@ -117,7 +147,8 @@
|
||||
"jt9": 24919000,
|
||||
"ft4": 24919000,
|
||||
"js8": 24922000
|
||||
}
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "10m",
|
||||
@ -131,7 +162,8 @@
|
||||
"jt9": 28078000,
|
||||
"ft4": 28180000,
|
||||
"js8": 28078000
|
||||
}
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "6m",
|
||||
@ -144,8 +176,10 @@
|
||||
"jt65": 50310000,
|
||||
"jt9": 50312000,
|
||||
"ft4": 50318000,
|
||||
"js8": 50318000
|
||||
}
|
||||
"js8": 50318000,
|
||||
"q65": [50211000, 50275000]
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "4m",
|
||||
@ -153,7 +187,8 @@
|
||||
"upper_bound": 70200000,
|
||||
"frequencies": {
|
||||
"wspr": 70091000
|
||||
}
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "2m",
|
||||
@ -164,110 +199,169 @@
|
||||
"ft8": 144174000,
|
||||
"ft4": 144170000,
|
||||
"jt65": 144120000,
|
||||
"packet": 144800000
|
||||
}
|
||||
"packet": 144800000,
|
||||
"q65": 144116000
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "70cm",
|
||||
"lower_bound": 430000000,
|
||||
"upper_bound": 440000000,
|
||||
"frequencies": {
|
||||
"pocsag": 439987500
|
||||
}
|
||||
"pocsag": 439987500,
|
||||
"q65": 432065000
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "23cm",
|
||||
"lower_bound": 1240000000,
|
||||
"upper_bound": 1300000000
|
||||
"upper_bound": 1300000000,
|
||||
"frequencies": {
|
||||
"q65": 1296065000
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "13cm",
|
||||
"lower_bound": 2320000000,
|
||||
"upper_bound": 2450000000
|
||||
"upper_bound": 2450000000,
|
||||
"frequencies": {
|
||||
"q65": [2301065000, 2304065000, 2320065000]
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "9cm",
|
||||
"lower_bound": 3400000000,
|
||||
"upper_bound": 3475000000
|
||||
"upper_bound": 3475000000,
|
||||
"frequencies": {
|
||||
"q65": 3400065000
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "6cm",
|
||||
"lower_bound": 5650000000,
|
||||
"upper_bound": 5850000000
|
||||
"upper_bound": 5850000000,
|
||||
"frequencies": {
|
||||
"q65": 5760200000
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "3cm",
|
||||
"lower_bound": 10000000000,
|
||||
"upper_bound": 10500000000
|
||||
"upper_bound": 10500000000,
|
||||
"frequencies": {
|
||||
"q65": 10368200000
|
||||
},
|
||||
"tags": ["hamradio"]
|
||||
},
|
||||
{
|
||||
"name": "120m Broadcast",
|
||||
"lower_bound": 2300000,
|
||||
"upper_bound": 2495000
|
||||
"upper_bound": 2495000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "90m Broadcast",
|
||||
"lower_bound": 3200000,
|
||||
"upper_bound": 3400000
|
||||
"upper_bound": 3400000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "75m Broadcast",
|
||||
"lower_bound": 3900000,
|
||||
"upper_bound": 4000000
|
||||
"upper_bound": 4000000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "60m Broadcast",
|
||||
"lower_bound": 4750000,
|
||||
"upper_bound": 4995000
|
||||
"upper_bound": 4995000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "49m Broadcast",
|
||||
"lower_bound": 5900000,
|
||||
"upper_bound": 6200000
|
||||
"upper_bound": 6200000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "41m Broadcast",
|
||||
"lower_bound": 7200000,
|
||||
"upper_bound": 7450000
|
||||
"upper_bound": 7450000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "31m Broadcast",
|
||||
"lower_bound": 9400000,
|
||||
"upper_bound": 9900000
|
||||
"upper_bound": 9900000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "25m Broadcast",
|
||||
"lower_bound": 11600000,
|
||||
"upper_bound": 12100000
|
||||
"upper_bound": 12100000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "22m Broadcast",
|
||||
"lower_bound": 13570000,
|
||||
"upper_bound": 13870000
|
||||
"upper_bound": 13870000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "19m Broadcast",
|
||||
"lower_bound": 15100000,
|
||||
"upper_bound": 15830000
|
||||
"upper_bound": 15830000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "16m Broadcast",
|
||||
"lower_bound": 17480000,
|
||||
"upper_bound": 17900000
|
||||
"upper_bound": 17900000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "15m Broadcast",
|
||||
"lower_bound": 18900000,
|
||||
"upper_bound": 19020000
|
||||
"upper_bound": 19020000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "13m Broadcast",
|
||||
"lower_bound": 21450000,
|
||||
"upper_bound": 21850000
|
||||
"upper_bound": 21850000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "11m Broadcast",
|
||||
"lower_bound": 25670000,
|
||||
"upper_bound": 26100000
|
||||
"upper_bound": 26100000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "FM Broadcast",
|
||||
"lower_bound": 87500000,
|
||||
"upper_bound": 108000000,
|
||||
"tags": ["broadcast"]
|
||||
},
|
||||
{
|
||||
"name": "11m CB",
|
||||
"lower_bound": 26965000,
|
||||
"upper_bound": 27405000,
|
||||
"frequencies": {
|
||||
"js8": 27245000
|
||||
},
|
||||
"tags": ["public"]
|
||||
},
|
||||
{
|
||||
"name": "PMR446",
|
||||
"lower_bound": 446000000,
|
||||
"upper_bound": 446200000,
|
||||
"tags": ["public"]
|
||||
}
|
||||
]
|
217
bookmarks.json
@ -1,217 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "DB0ZU",
|
||||
"frequency": 145725000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0ZM",
|
||||
"frequency": 145750000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DM0ULR",
|
||||
"frequency": 145787500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0EL",
|
||||
"frequency": 439275000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0NJ",
|
||||
"frequency": 438775000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0NJ",
|
||||
"frequency": 439437500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0UFO",
|
||||
"frequency": 438312500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0PV",
|
||||
"frequency": 438525000,
|
||||
"modulation": "ysf"
|
||||
},
|
||||
{
|
||||
"name": "DB0BZA",
|
||||
"frequency": 438412500,
|
||||
"modulation": "ysf"
|
||||
},
|
||||
{
|
||||
"name": "DB0OSH",
|
||||
"frequency": 438250000,
|
||||
"modulation": "ysf"
|
||||
},
|
||||
{
|
||||
"name": "DB0ULR",
|
||||
"frequency": 439325000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0ZU",
|
||||
"frequency": 438850000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0ISW",
|
||||
"frequency": 438650000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "Radio DARC",
|
||||
"frequency": 6070000,
|
||||
"modulation": "am"
|
||||
},
|
||||
{
|
||||
"name": "DB0TVM",
|
||||
"frequency": 439575000,
|
||||
"modulation": "dstar"
|
||||
},
|
||||
{
|
||||
"name": "DB0TVM",
|
||||
"frequency": 439800000,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0TR",
|
||||
"frequency": 438700000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0PME",
|
||||
"frequency": 439825000,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0HKN",
|
||||
"frequency": 438300000,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "OE2XHM",
|
||||
"frequency": 438825000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DM0WW",
|
||||
"frequency": 438962500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "OE7XXR",
|
||||
"frequency": 438200000,
|
||||
"modulation": "dstar"
|
||||
},
|
||||
{
|
||||
"name": "OE2XZR",
|
||||
"frequency": 439000000,
|
||||
"modulation": "dstar"
|
||||
},
|
||||
{
|
||||
"name": "DB0OAL",
|
||||
"frequency": 439912500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0AAT",
|
||||
"frequency": 439550000,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0FSG",
|
||||
"frequency": 439937500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0ULR",
|
||||
"frequency": 145575000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0RDH",
|
||||
"frequency": 145737500,
|
||||
"modulation": "dstar"
|
||||
},
|
||||
{
|
||||
"name": "DM0GAP",
|
||||
"frequency": 145612500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0XF",
|
||||
"frequency": 145600000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0TOL",
|
||||
"frequency": 145712500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0TTB",
|
||||
"frequency": 439587500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0TRS",
|
||||
"frequency": 439125000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0OAL",
|
||||
"frequency": 438937500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DM0ULR",
|
||||
"frequency": 439337500,
|
||||
"modulation": "nxdn"
|
||||
},
|
||||
{
|
||||
"name": "DB0MIR",
|
||||
"frequency": 439300000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0PM",
|
||||
"frequency": 439075000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0CP",
|
||||
"frequency": 439025000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "OE7XGR",
|
||||
"frequency": 438925000,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0TOL",
|
||||
"frequency": 438725000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0OAL",
|
||||
"frequency": 438325000,
|
||||
"modulation": "dstar"
|
||||
},
|
||||
{
|
||||
"name": "DB0ROL",
|
||||
"frequency": 439237500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0ABX",
|
||||
"frequency": 439137500,
|
||||
"modulation": "nfm"
|
||||
}
|
||||
]
|
466
config_webrx.py
@ -6,7 +6,7 @@ config_webrx: configuration options for OpenWebRX
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -32,32 +32,45 @@ config_webrx: configuration options for OpenWebRX
|
||||
and use them for running your web service with OpenWebRX.)
|
||||
"""
|
||||
|
||||
"""
|
||||
DEPRECATION notice
|
||||
|
||||
As of OpenWebRX 0.21, the configuration system has been completely overhauled.
|
||||
The configuration of OpenWebRX should now be done in the new web-based
|
||||
configuration interface exclusively.
|
||||
|
||||
Existing configurations can still be used, but their values will be migrated
|
||||
to the new storage infrastructure as soon as the web configuration is used to
|
||||
edit them.
|
||||
|
||||
The new configuration storage is not intended to be edited manually.
|
||||
"""
|
||||
|
||||
# configuration version. please only modify if you're able to perform the associated migration steps.
|
||||
version = 3
|
||||
version = 7
|
||||
|
||||
# NOTE: you can find additional information about configuring OpenWebRX in the Wiki:
|
||||
# https://github.com/jketterl/openwebrx/wiki/Configuration-guide
|
||||
|
||||
# ==== Server settings ====
|
||||
web_port = 8073
|
||||
max_clients = 20
|
||||
#max_clients = 20
|
||||
|
||||
# ==== Web GUI configuration ====
|
||||
receiver_name = "[Callsign]"
|
||||
receiver_location = "Budapest, Hungary"
|
||||
receiver_asl = 200
|
||||
receiver_admin = "example@example.com"
|
||||
receiver_gps = {"lat": 47.000000, "lon": 19.000000}
|
||||
photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
|
||||
#receiver_name = "[Callsign]"
|
||||
#receiver_location = "Budapest, Hungary"
|
||||
#receiver_asl = 200
|
||||
#receiver_admin = "example@example.com"
|
||||
#receiver_gps = {"lat": 47.000000, "lon": 19.000000}
|
||||
#photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
|
||||
# photo_desc allows you to put pretty much any HTML you like into the receiver description.
|
||||
# The lines below should give you some examples of what's possible.
|
||||
photo_desc = """
|
||||
You can add your own background photo and receiver information.<br />
|
||||
Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
|
||||
Device: Receiver Device<br />
|
||||
Antenna: Receiver Antenna<br />
|
||||
Website: <a href="http://localhost" target="_blank">http://localhost</a>
|
||||
"""
|
||||
#photo_desc = """
|
||||
#You can add your own background photo and receiver information.<br />
|
||||
#Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
|
||||
#Device: Receiver Device<br />
|
||||
#Antenna: Receiver Antenna<br />
|
||||
#Website: <a href="http://localhost" target="_blank">http://localhost</a>
|
||||
#"""
|
||||
|
||||
# ==== Public receiver listings ====
|
||||
# You can publish your receiver on online receiver directories, like https://www.receiverbook.de
|
||||
@ -65,7 +78,7 @@ Website: <a href="http://localhost" target="_blank">http://localhost</a>
|
||||
# Please note that you not share your receiver keys publicly since anyone that obtains your receiver key can take over
|
||||
# your public listing.
|
||||
# Your receiver keys should be placed into this array:
|
||||
receiver_keys = []
|
||||
#receiver_keys = []
|
||||
# If you list your receiver on multiple sites, you can place all your keys into the array above, or you can append
|
||||
# keys to the arraylike this:
|
||||
# receiver_keys += ["my-receiver-key"]
|
||||
@ -73,30 +86,29 @@ receiver_keys = []
|
||||
# If you're not sure, simply copy & paste the code you received from your listing site below this line:
|
||||
|
||||
# ==== DSP/RX settings ====
|
||||
fft_fps = 9
|
||||
fft_size = 4096 # Should be power of 2
|
||||
fft_voverlap_factor = (
|
||||
0.3 # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
|
||||
)
|
||||
#fft_fps = 9
|
||||
#fft_size = 4096 # Should be power of 2
|
||||
#fft_voverlap_factor = (
|
||||
# 0.3 # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
|
||||
#)
|
||||
|
||||
audio_compression = "adpcm" # valid values: "adpcm", "none"
|
||||
fft_compression = "adpcm" # valid values: "adpcm", "none"
|
||||
#audio_compression = "adpcm" # valid values: "adpcm", "none"
|
||||
#fft_compression = "adpcm" # valid values: "adpcm", "none"
|
||||
|
||||
# Tau setting for WFM (broadcast FM) deemphasis\
|
||||
# Quote from wikipedia https://en.wikipedia.org/wiki/FM_broadcasting#Pre-emphasis_and_de-emphasis
|
||||
# "In most of the world a 50 µs time constant is used. In the Americas and South Korea, 75 µs is used"
|
||||
# Enable one of the following lines, depending on your location:
|
||||
# wfm_deemphasis_tau = 75e-6 # for US and South Korea
|
||||
wfm_deemphasis_tau = 50e-6 # for the rest of the world
|
||||
#wfm_deemphasis_tau = 50e-6 # for the rest of the world
|
||||
|
||||
digimodes_enable = True # Decoding digimodes come with higher CPU usage.
|
||||
digimodes_fft_size = 2048
|
||||
#digimodes_fft_size = 2048
|
||||
|
||||
# determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes
|
||||
# if you're running on a Raspi (up to 3B+) you'll want to leave this on 1
|
||||
digital_voice_unvoiced_quality = 1
|
||||
#digital_voice_unvoiced_quality = 1
|
||||
# enables lookup of DMR ids using the radioid api
|
||||
digital_voice_dmr_id_lookup = True
|
||||
#digital_voice_dmr_id_lookup = True
|
||||
|
||||
"""
|
||||
Note: if you experience audio underruns while CPU usage is 100%, you can:
|
||||
@ -116,230 +128,262 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
|
||||
|
||||
# Currently supported types of sdr receivers:
|
||||
# "rtl_sdr", "rtl_sdr_soapy", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr",
|
||||
# "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote"
|
||||
#
|
||||
# In order to use rtl_sdr, you will need to install librtlsdr-dev and the connector.
|
||||
# In order to use sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the
|
||||
# connector.
|
||||
#
|
||||
# https://github.com/jketterl/owrx_connector
|
||||
#
|
||||
# In order to use Perseus HF you need to install the libperseus-sdr
|
||||
#
|
||||
# https://github.com/Microtelecom/libperseus-sdr
|
||||
#
|
||||
# and do the proper changes to the sdrs object below
|
||||
# (see also Wiki in https://github.com/jketterl/openwebrx/wiki/Sample-configuration-for-Perseus-HF-receiver).
|
||||
#
|
||||
# "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote", "hpsdr", "uhd",
|
||||
# "radioberry", "fcdpp", "rtl_tcp", "sddc", "runds"
|
||||
|
||||
sdrs = {
|
||||
"rtlsdr": {
|
||||
"name": "RTL-SDR USB Stick",
|
||||
"type": "rtl_sdr",
|
||||
"ppm": 0,
|
||||
# you can change this if you use an upconverter. formula is:
|
||||
# center_freq + lfo_offset = actual frequency on the sdr
|
||||
# "lfo_offset": 0,
|
||||
"profiles": {
|
||||
"70cm": {
|
||||
"name": "70cm Relais",
|
||||
"center_freq": 438800000,
|
||||
"rf_gain": 29,
|
||||
"samp_rate": 2400000,
|
||||
"start_freq": 439275000,
|
||||
"start_mod": "nfm",
|
||||
},
|
||||
"2m": {
|
||||
"name": "2m komplett",
|
||||
"center_freq": 145000000,
|
||||
"rf_gain": 29,
|
||||
"samp_rate": 2048000,
|
||||
"start_freq": 145725000,
|
||||
"start_mod": "nfm",
|
||||
},
|
||||
},
|
||||
},
|
||||
"airspy": {
|
||||
"name": "Airspy HF+",
|
||||
"type": "airspyhf",
|
||||
"ppm": 0,
|
||||
"rf_gain": "auto",
|
||||
"profiles": {
|
||||
"20m": {
|
||||
"name": "20m",
|
||||
"center_freq": 14150000,
|
||||
"samp_rate": 384000,
|
||||
"start_freq": 14070000,
|
||||
"start_mod": "usb",
|
||||
},
|
||||
"30m": {
|
||||
"name": "30m",
|
||||
"center_freq": 10125000,
|
||||
"samp_rate": 192000,
|
||||
"start_freq": 10142000,
|
||||
"start_mod": "usb",
|
||||
},
|
||||
"40m": {
|
||||
"name": "40m",
|
||||
"center_freq": 7100000,
|
||||
"samp_rate": 256000,
|
||||
"start_freq": 7070000,
|
||||
"start_mod": "lsb",
|
||||
},
|
||||
"80m": {
|
||||
"name": "80m",
|
||||
"center_freq": 3650000,
|
||||
"samp_rate": 384000,
|
||||
"start_freq": 3570000,
|
||||
"start_mod": "lsb",
|
||||
},
|
||||
"49m": {
|
||||
"name": "49m Broadcast",
|
||||
"center_freq": 6050000,
|
||||
"samp_rate": 384000,
|
||||
"start_freq": 6070000,
|
||||
"start_mod": "am",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sdrplay": {
|
||||
"name": "SDRPlay RSP2",
|
||||
"type": "sdrplay",
|
||||
"ppm": 0,
|
||||
"antenna": "Antenna A",
|
||||
"profiles": {
|
||||
"20m": {
|
||||
"name": "20m",
|
||||
"center_freq": 14150000,
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 14070000,
|
||||
"start_mod": "usb",
|
||||
},
|
||||
"30m": {
|
||||
"name": "30m",
|
||||
"center_freq": 10125000,
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 250000,
|
||||
"start_freq": 10142000,
|
||||
"start_mod": "usb",
|
||||
},
|
||||
"40m": {
|
||||
"name": "40m",
|
||||
"center_freq": 7100000,
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 7070000,
|
||||
"start_mod": "lsb",
|
||||
},
|
||||
"80m": {
|
||||
"name": "80m",
|
||||
"center_freq": 3650000,
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 3570000,
|
||||
"start_mod": "lsb",
|
||||
},
|
||||
"49m": {
|
||||
"name": "49m Broadcast",
|
||||
"center_freq": 6000000,
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 6070000,
|
||||
"start_mod": "am",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
# For more details on specific types, please checkout the wiki:
|
||||
# https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices
|
||||
|
||||
#sdrs = {
|
||||
# "rtlsdr": {
|
||||
# "name": "RTL-SDR USB Stick",
|
||||
# "type": "rtl_sdr",
|
||||
# "ppm": 0,
|
||||
# # you can change this if you use an upconverter. formula is:
|
||||
# # center_freq + lfo_offset = actual frequency on the sdr
|
||||
# # "lfo_offset": 0,
|
||||
# "profiles": {
|
||||
# "70cm": {
|
||||
# "name": "70cm Relais",
|
||||
# "center_freq": 438800000,
|
||||
# "rf_gain": 29,
|
||||
# "samp_rate": 2400000,
|
||||
# "start_freq": 439275000,
|
||||
# "start_mod": "nfm",
|
||||
# },
|
||||
# "2m": {
|
||||
# "name": "2m komplett",
|
||||
# "center_freq": 145000000,
|
||||
# "rf_gain": 29,
|
||||
# "samp_rate": 2048000,
|
||||
# "start_freq": 145725000,
|
||||
# "start_mod": "nfm",
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
# "airspy": {
|
||||
# "name": "Airspy HF+",
|
||||
# "type": "airspyhf",
|
||||
# "ppm": 0,
|
||||
# "rf_gain": "auto",
|
||||
# "profiles": {
|
||||
# "20m": {
|
||||
# "name": "20m",
|
||||
# "center_freq": 14150000,
|
||||
# "samp_rate": 384000,
|
||||
# "start_freq": 14070000,
|
||||
# "start_mod": "usb",
|
||||
# },
|
||||
# "30m": {
|
||||
# "name": "30m",
|
||||
# "center_freq": 10125000,
|
||||
# "samp_rate": 192000,
|
||||
# "start_freq": 10142000,
|
||||
# "start_mod": "usb",
|
||||
# },
|
||||
# "40m": {
|
||||
# "name": "40m",
|
||||
# "center_freq": 7100000,
|
||||
# "samp_rate": 256000,
|
||||
# "start_freq": 7070000,
|
||||
# "start_mod": "lsb",
|
||||
# },
|
||||
# "80m": {
|
||||
# "name": "80m",
|
||||
# "center_freq": 3650000,
|
||||
# "samp_rate": 384000,
|
||||
# "start_freq": 3570000,
|
||||
# "start_mod": "lsb",
|
||||
# },
|
||||
# "49m": {
|
||||
# "name": "49m Broadcast",
|
||||
# "center_freq": 6050000,
|
||||
# "samp_rate": 384000,
|
||||
# "start_freq": 6070000,
|
||||
# "start_mod": "am",
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
# "sdrplay": {
|
||||
# "name": "SDRPlay RSP2",
|
||||
# "type": "sdrplay",
|
||||
# "ppm": 0,
|
||||
# "antenna": "Antenna A",
|
||||
# "profiles": {
|
||||
# "20m": {
|
||||
# "name": "20m",
|
||||
# "center_freq": 14150000,
|
||||
# "rf_gain": 0,
|
||||
# "samp_rate": 500000,
|
||||
# "start_freq": 14070000,
|
||||
# "start_mod": "usb",
|
||||
# },
|
||||
# "30m": {
|
||||
# "name": "30m",
|
||||
# "center_freq": 10125000,
|
||||
# "rf_gain": 0,
|
||||
# "samp_rate": 250000,
|
||||
# "start_freq": 10142000,
|
||||
# "start_mod": "usb",
|
||||
# },
|
||||
# "40m": {
|
||||
# "name": "40m",
|
||||
# "center_freq": 7100000,
|
||||
# "rf_gain": 0,
|
||||
# "samp_rate": 500000,
|
||||
# "start_freq": 7070000,
|
||||
# "start_mod": "lsb",
|
||||
# },
|
||||
# "80m": {
|
||||
# "name": "80m",
|
||||
# "center_freq": 3650000,
|
||||
# "rf_gain": 0,
|
||||
# "samp_rate": 500000,
|
||||
# "start_freq": 3570000,
|
||||
# "start_mod": "lsb",
|
||||
# },
|
||||
# "49m": {
|
||||
# "name": "49m Broadcast",
|
||||
# "center_freq": 6000000,
|
||||
# "rf_gain": 0,
|
||||
# "samp_rate": 500000,
|
||||
# "start_freq": 6070000,
|
||||
# "start_mod": "am",
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
#}
|
||||
|
||||
# ==== Color themes ====
|
||||
|
||||
### google turbo colormap (see: https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html)
|
||||
waterfall_colors = [0x30123b, 0x311542, 0x33184a, 0x341b51, 0x351e58, 0x36215f, 0x372466, 0x38266c, 0x392973, 0x3a2c79, 0x3b2f80, 0x3c3286, 0x3d358b, 0x3e3891, 0x3e3a97, 0x3f3d9c, 0x4040a2, 0x4043a7, 0x4146ac, 0x4248b1, 0x424bb6, 0x434eba, 0x4351bf, 0x4453c3, 0x4456c7, 0x4559cb, 0x455bcf, 0x455ed3, 0x4561d7, 0x4663da, 0x4666dd, 0x4669e1, 0x466be4, 0x466ee7, 0x4671e9, 0x4673ec, 0x4676ee, 0x4678f1, 0x467bf3, 0x467df5, 0x4680f7, 0x4682f9, 0x4685fa, 0x4587fc, 0x458afd, 0x448cfe, 0x448ffe, 0x4391ff, 0x4294ff, 0x4196ff, 0x3f99ff, 0x3e9bff, 0x3d9efe, 0x3ba1fd, 0x3aa3fd, 0x38a6fb, 0x36a8fa, 0x35abf9, 0x33adf7, 0x31b0f6, 0x2fb2f4, 0x2db5f2, 0x2cb7f0, 0x2ab9ee, 0x28bcec, 0x26beea, 0x25c0e7, 0x23c3e5, 0x21c5e2, 0x20c7e0, 0x1fc9dd, 0x1dccdb, 0x1cced8, 0x1bd0d5, 0x1ad2d3, 0x19d4d0, 0x18d6cd, 0x18d8cb, 0x18dac8, 0x17dbc5, 0x17ddc3, 0x17dfc0, 0x18e0be, 0x18e2bb, 0x19e3b9, 0x1ae5b7, 0x1be6b4, 0x1de8b2, 0x1ee9af, 0x20eaad, 0x22ecaa, 0x24eda7, 0x27eea4, 0x29efa1, 0x2cf09e, 0x2ff19b, 0x32f298, 0x35f394, 0x38f491, 0x3cf58e, 0x3ff68b, 0x43f787, 0x46f884, 0x4af980, 0x4efa7d, 0x51fa79, 0x55fb76, 0x59fc73, 0x5dfc6f, 0x61fd6c, 0x65fd69, 0x69fe65, 0x6dfe62, 0x71fe5f, 0x75ff5c, 0x79ff59, 0x7dff56, 0x80ff53, 0x84ff50, 0x88ff4e, 0x8bff4b, 0x8fff49, 0x92ff46, 0x96ff44, 0x99ff42, 0x9cfe40, 0x9ffe3e, 0xa2fd3d, 0xa4fd3b, 0xa7fc3a, 0xaafc39, 0xacfb38, 0xaffa37, 0xb1f936, 0xb4f835, 0xb7f835, 0xb9f634, 0xbcf534, 0xbff434, 0xc1f334, 0xc4f233, 0xc6f033, 0xc9ef34, 0xcbee34, 0xceec34, 0xd0eb34, 0xd2e934, 0xd5e835, 0xd7e635, 0xd9e435, 0xdbe236, 0xdde136, 0xe0df37, 0xe2dd37, 0xe4db38, 0xe6d938, 0xe7d738, 0xe9d539, 0xebd339, 0xedd139, 0xeecf3a, 0xf0cd3a, 0xf1cb3a, 0xf3c93a, 0xf4c73a, 0xf5c53a, 0xf7c33a, 0xf8c13a, 0xf9bf39, 0xfabd39, 0xfaba38, 0xfbb838, 0xfcb637, 0xfcb436, 0xfdb135, 0xfdaf35, 0xfeac34, 0xfea933, 0xfea732, 0xfea431, 0xffa12f, 0xff9e2e, 0xff9c2d, 0xff992c, 0xfe962b, 0xfe932a, 0xfe9028, 0xfe8d27, 0xfd8a26, 0xfd8724, 0xfc8423, 0xfc8122, 0xfb7e20, 0xfb7b1f, 0xfa781e, 0xf9751c, 0xf8721b, 0xf86f1a, 0xf76c19, 0xf66917, 0xf56616, 0xf46315, 0xf36014, 0xf25d13, 0xf05b11, 0xef5810, 0xee550f, 0xed530e, 0xeb500e, 0xea4e0d, 0xe94b0c, 0xe7490b, 0xe6470a, 0xe4450a, 0xe34209, 0xe14009, 0xdf3e08, 0xde3c07, 0xdc3a07, 0xda3806, 0xd83606, 0xd63405, 0xd43205, 0xd23105, 0xd02f04, 0xce2d04, 0xcc2b03, 0xca2903, 0xc82803, 0xc62602, 0xc32402, 0xc12302, 0xbf2102, 0xbc1f01, 0xba1e01, 0xb71c01, 0xb41b01, 0xb21901, 0xaf1801, 0xac1601, 0xaa1501, 0xa71401, 0xa41201, 0xa11101, 0x9e1001, 0x9b0f01, 0x980d01, 0x950c01, 0x920b01, 0x8e0a01, 0x8b0901, 0x880801, 0x850701, 0x810602, 0x7e0502, 0x7a0402]
|
||||
#waterfall_scheme = "GoogleTurboWaterfall"
|
||||
|
||||
### original theme by teejez:
|
||||
#waterfall_colors = [0x000000, 0x0000FF, 0x00FFFF, 0x00FF00, 0xFFFF00, 0xFF0000, 0xFF00FF, 0xFFFFFF]
|
||||
#waterfall_scheme = "TeejeezWaterfall"
|
||||
|
||||
### old theme by HA7ILM:
|
||||
#waterfall_colors = [0x000000, 0x2e6893, 0x69a5d0, 0x214b69, 0x9dc4e0, 0xfff775, 0xff8a8a, 0xb20000]
|
||||
# waterfall_min_level = -115 #in dB
|
||||
# waterfall_max_level = 0
|
||||
# waterfall_auto_level_margin = {"min": 20, "max": 30}
|
||||
#waterfall_scheme = "Ha7ilmWaterfall"
|
||||
##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
|
||||
|
||||
waterfall_min_level = -88 # in dB
|
||||
waterfall_max_level = -20
|
||||
waterfall_auto_level_margin = {"min": 3, "max": 10, "min_range": 50}
|
||||
### custom waterfall schemes can be configured like this:
|
||||
#waterfall_scheme = "CustomWaterfall"
|
||||
#waterfall_colors = [0x0000FF, 0x00FF00, 0xFF0000]
|
||||
|
||||
### Waterfall calibration
|
||||
#waterfall_levels = {"min": -88, "max": -20} # in dB
|
||||
|
||||
#waterfall_auto_levels = {"min": 3, "max": 10}
|
||||
#waterfall_auto_min_range = 50
|
||||
|
||||
# Note: When the auto waterfall level button is clicked, the following happens:
|
||||
# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin["min"]]
|
||||
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin["max"]]
|
||||
# [waterfall_levels.min] = [current_min_power_level] - [waterfall_auto_levels["min"]]
|
||||
# [waterfall_levels.max] = [current_max_power_level] + [waterfall_auto_levels["max"]]
|
||||
#
|
||||
# ___|________________________________________|____________________________________|________________________________________|___> signal power
|
||||
# \_waterfall_auto_level_margin["min"]_/ |__ current_min_power_level | \_waterfall_auto_level_margin["max"]_/
|
||||
# current_max_power_level __|
|
||||
# ___|__________________________________|____________________________________|__________________________________|___> signal power
|
||||
# \_waterfall_auto_levels["min"]_/ |__ current_min_power_level | \_waterfall_auto_levels["max"]_/
|
||||
# current_max_power_level __|
|
||||
|
||||
# === Experimental settings ===
|
||||
# Warning! The settings below are very experimental.
|
||||
csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr.
|
||||
csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes.
|
||||
csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
|
||||
# This setting allows you to modify the precision of the frequency displays in OpenWebRX.
|
||||
# Set this to exponent of 10 to select the most precise digit in Hz you'd like to see
|
||||
# examples:
|
||||
# a value of 2 selects 10^2 = 100Hz tuning precision (default):
|
||||
#tuning_precision = 2
|
||||
# a value of 1 selects 10^1 = 10Hz tuning precision:
|
||||
#tuning_precision = 1
|
||||
|
||||
nmux_memory = 50 # in megabytes. This sets the approximate size of the circular buffer used by nmux.
|
||||
# This setting tells the auto-squelch the offset to add to the current signal level to use as the new squelch level.
|
||||
# Lowering this setting will give you a more sensitive squelch, but it may also cause unwanted squelch openings when
|
||||
# using the auto squelch.
|
||||
#squelch_auto_margin = 10 # in dB
|
||||
|
||||
google_maps_api_key = ""
|
||||
#google_maps_api_key = ""
|
||||
|
||||
# how long should positions be visible on the map?
|
||||
# they will start fading out after half of that
|
||||
# in seconds; default: 2 hours
|
||||
map_position_retention_time = 2 * 60 * 60
|
||||
#map_position_retention_time = 2 * 60 * 60
|
||||
|
||||
# decoder queue configuration
|
||||
# due to the nature of some operating modes (ft8, ft8, jt9, jt65, wspr and js8), the data is recorded for a given amount
|
||||
# of time (6 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
|
||||
# to mitigate this, the recordings will be queued and processed in sequence.
|
||||
# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
|
||||
decoding_queue_workers = 2
|
||||
#decoding_queue_workers = 2
|
||||
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up
|
||||
# if you are running background services, make sure this number is high enough to accept the task influx during peaks
|
||||
# i.e. this should be higher than the number of decoding services running at the same time
|
||||
decoding_queue_length = 10
|
||||
#decoding_queue_length = 10
|
||||
|
||||
# wsjt decoding depth will allow more results, but will also consume more cpu
|
||||
wsjt_decoding_depth = 3
|
||||
#wsjt_decoding_depth = 3
|
||||
# can also be set for each mode separately
|
||||
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
|
||||
wsjt_decoding_depths = {"jt65": 1}
|
||||
#wsjt_decoding_depths = {"jt65": 1}
|
||||
|
||||
# FST4 can be transmitted in different intervals. This setting determines which intervals will be decoded.
|
||||
# available values (in seconds): 15, 30, 60, 120, 300, 900, 1800
|
||||
#fst4_enabled_intervals = [15, 30]
|
||||
|
||||
# FST4W can be transmitted in different intervals. This setting determines which intervals will be decoded.
|
||||
# available values (in seconds): 120, 300, 900, 1800
|
||||
#fst4w_enabled_intervals = [120, 300]
|
||||
|
||||
# Q65 allows many combinations of intervals and submodes. This setting determines which combinations will be decoded.
|
||||
# Please use the mode letter followed by the decode interval in seconds to specify the combinations. For example:
|
||||
#q65_enabled_combinations = ["A30", "E120", "C60"]
|
||||
|
||||
# JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled.
|
||||
js8_enabled_profiles = ["normal", "slow"]
|
||||
#js8_enabled_profiles = ["normal", "slow"]
|
||||
# JS8 decoding depth; higher value will get more results, but will also consume more cpu
|
||||
js8_decoding_depth = 3
|
||||
#js8_decoding_depth = 3
|
||||
|
||||
temporary_directory = "/tmp"
|
||||
|
||||
services_enabled = False
|
||||
services_decoders = ["ft8", "ft4", "wspr", "packet"]
|
||||
# Enable background service for decoding digital data. You can find more information at:
|
||||
# https://github.com/jketterl/openwebrx/wiki/Background-decoding
|
||||
#services_enabled = False
|
||||
#services_decoders = ["ft8", "ft4", "wspr", "packet"]
|
||||
|
||||
# === aprs igate settings ===
|
||||
# if you want to share your APRS decodes with the aprs network, configure these settings accordingly
|
||||
aprs_callsign = "N0CALL"
|
||||
aprs_igate_enabled = False
|
||||
aprs_igate_server = "euro.aprs2.net"
|
||||
aprs_igate_password = ""
|
||||
# If you want to share your APRS decodes with the aprs network, configure these settings accordingly.
|
||||
# Make sure that you have set services_enabled to true and customize services_decoders to your needs.
|
||||
#aprs_callsign = "N0CALL"
|
||||
#aprs_igate_enabled = False
|
||||
#aprs_igate_server = "euro.aprs2.net"
|
||||
#aprs_igate_password = ""
|
||||
# beacon uses the receiver_gps setting, so if you enable this, make sure the location is correct there
|
||||
aprs_igate_beacon = False
|
||||
#aprs_igate_beacon = False
|
||||
|
||||
# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols)
|
||||
aprs_symbols_path = "/opt/aprs-symbols/png"
|
||||
# Uncomment the following to customize gateway beacon details reported to the aprs network
|
||||
# Plese see Dire Wolf's documentation on PBEACON configuration for complete details:
|
||||
# https://github.com/wb2osz/direwolf/raw/master/doc/User-Guide.pdf
|
||||
|
||||
# === PSK Reporter setting ===
|
||||
# Symbol in its two-character form as specified by the APRS spec at http://www.aprs.org/symbols/symbols-new.txt
|
||||
# Default: Receive only IGate (do not send msgs back to RF)
|
||||
# aprs_igate_symbol = "R&"
|
||||
|
||||
# Custom comment about igate
|
||||
# Default: OpenWebRX APRS gateway
|
||||
# aprs_igate_comment = "OpenWebRX APRS gateway"
|
||||
|
||||
# Antenna Height and Gain details
|
||||
# Unspecified by default
|
||||
# Antenna height above average terrain (HAAT) in meters
|
||||
# aprs_igate_height = "5"
|
||||
# Antenna gain in dBi
|
||||
# aprs_igate_gain = "0"
|
||||
# Antenna direction (N, NE, E, SE, S, SW, W, NW). Omnidirectional by default
|
||||
# aprs_igate_dir = "NE"
|
||||
|
||||
# === PSK Reporter settings ===
|
||||
# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info
|
||||
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator
|
||||
pskreporter_enabled = False
|
||||
pskreporter_callsign = "N0CALL"
|
||||
#pskreporter_enabled = False
|
||||
#pskreporter_callsign = "N0CALL"
|
||||
# optional antenna information, uncomment to enable
|
||||
#pskreporter_antenna_information = "Dipole"
|
||||
|
||||
# === Web admin settings ===
|
||||
# this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote
|
||||
# changes to the receiver settings. enable for testing in controlled environment only.
|
||||
# webadmin_enabled = False
|
||||
# === WSPRNet reporting settings
|
||||
# enable this if you want to upload WSPR spots to wsprnet.ort
|
||||
# in addition to these settings also make sure that receiver_gps contains your correct location
|
||||
#wsprnet_enabled = False
|
||||
#wsprnet_callsign = "N0CALL"
|
||||
|
@ -4,7 +4,7 @@ OpenWebRX csdr plugin: do the signal processing with csdr
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -28,10 +28,10 @@ import threading
|
||||
import math
|
||||
from functools import partial
|
||||
|
||||
from owrx.kiss import KissClient, DirewolfConfig
|
||||
from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile
|
||||
from owrx.js8 import Js8Profiles
|
||||
from owrx.audio import AudioChopper
|
||||
from csdr.output import Output
|
||||
|
||||
from owrx.kiss import KissClient, DirewolfConfig, DirewolfConfigSubscriber
|
||||
from owrx.audio.chopper import AudioChopper
|
||||
|
||||
from csdr.pipe import Pipe
|
||||
|
||||
@ -40,40 +40,8 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class output(object):
|
||||
def send_output(self, t, read_fn):
|
||||
if not self.supports_type(t):
|
||||
# TODO rewrite the output mechanism in a way that avoids producing unnecessary data
|
||||
logger.warning("dumping output of type %s since it is not supported.", t)
|
||||
threading.Thread(target=self.pump(read_fn, lambda x: None), name="csdr_pump_thread").start()
|
||||
return
|
||||
self.receive_output(t, read_fn)
|
||||
|
||||
def receive_output(self, t, read_fn):
|
||||
pass
|
||||
|
||||
def pump(self, read, write):
|
||||
def copy():
|
||||
run = True
|
||||
while run:
|
||||
data = None
|
||||
try:
|
||||
data = read()
|
||||
except ValueError:
|
||||
pass
|
||||
if data is None or (isinstance(data, bytes) and len(data) == 0):
|
||||
run = False
|
||||
else:
|
||||
write(data)
|
||||
|
||||
return copy
|
||||
|
||||
def supports_type(self, t):
|
||||
return True
|
||||
|
||||
|
||||
class dsp(object):
|
||||
def __init__(self, output):
|
||||
class Dsp(DirewolfConfigSubscriber):
|
||||
def __init__(self, output: Output):
|
||||
self.samp_rate = 250000
|
||||
self.output_rate = 11025
|
||||
self.hd_output_rate = 44100
|
||||
@ -95,9 +63,6 @@ class dsp(object):
|
||||
self.decimation = None
|
||||
self.last_decimation = None
|
||||
self.nc_port = None
|
||||
self.csdr_dynamic_bufsize = False
|
||||
self.csdr_print_bufsizes = False
|
||||
self.csdr_through = False
|
||||
self.squelch_level = -150
|
||||
self.fft_averages = 50
|
||||
self.wfm_deemphasis_tau = 50e-6
|
||||
@ -130,7 +95,7 @@ class dsp(object):
|
||||
|
||||
self.is_service = False
|
||||
self.direwolf_config = None
|
||||
self.direwolf_port = None
|
||||
self.direwolf_config_path = None
|
||||
self.process = None
|
||||
|
||||
def set_service(self, flag=True):
|
||||
@ -142,10 +107,6 @@ class dsp(object):
|
||||
|
||||
def chain(self, which):
|
||||
chain = ["nc -v 127.0.0.1 {nc_port}"]
|
||||
if self.csdr_dynamic_bufsize:
|
||||
chain += ["csdr setbuf {start_bufsize}"]
|
||||
if self.csdr_through:
|
||||
chain += ["csdr through"]
|
||||
if which == "fft":
|
||||
chain += [
|
||||
"csdr fft_cc {fft_size} {fft_block_size}",
|
||||
@ -198,16 +159,13 @@ class dsp(object):
|
||||
"csdr limit_ff",
|
||||
]
|
||||
chain += last_decimation_block
|
||||
chain += [
|
||||
"csdr deemphasis_wfm_ff {audio_rate} {wfm_deemphasis_tau}",
|
||||
"csdr convert_f_s16"
|
||||
]
|
||||
chain += ["csdr deemphasis_wfm_ff {audio_rate} {wfm_deemphasis_tau}", "csdr convert_f_s16"]
|
||||
elif self.isDigitalVoice(which):
|
||||
chain += ["csdr fmdemod_quadri_cf", "dc_block "]
|
||||
chain += ["csdr fmdemod_quadri_cf"]
|
||||
chain += last_decimation_block
|
||||
# dsd modes
|
||||
if which in ["dstar", "nxdn"]:
|
||||
chain += ["csdr limit_ff", "csdr convert_f_s16"]
|
||||
chain += ["dc_block", "csdr limit_ff", "csdr convert_f_s16"]
|
||||
if which == "dstar":
|
||||
chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "]
|
||||
elif which == "nxdn":
|
||||
@ -217,9 +175,19 @@ class dsp(object):
|
||||
"CSDR_FIXED_BUFSIZE=32 csdr agc_s16 --max 30 --initial 3",
|
||||
"sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
|
||||
]
|
||||
# m17
|
||||
elif which == "m17":
|
||||
chain += [
|
||||
"dc_block",
|
||||
"csdr limit_ff",
|
||||
"csdr convert_f_s16",
|
||||
"m17-demod",
|
||||
"CSDR_FIXED_BUFSIZE=32 csdr agc_s16 --max 30 --initial 3",
|
||||
"sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
|
||||
]
|
||||
# digiham modes
|
||||
else:
|
||||
chain += ["rrc_filter", "gfsk_demodulator"]
|
||||
chain += ["dc_block", "rrc_filter", "gfsk_demodulator"]
|
||||
if which == "dmr":
|
||||
chain += [
|
||||
"dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}",
|
||||
@ -301,7 +269,7 @@ class dsp(object):
|
||||
chain += ["csdr realpart_cf"]
|
||||
if self.last_decimation != 1.0:
|
||||
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
||||
return chain + ["csdr limit_ff", "csdr convert_f_s16"]
|
||||
return chain + ["csdr agc_ff", "csdr convert_f_s16"]
|
||||
elif which == "packet":
|
||||
chain += ["csdr fmdemod_quadri_cf"]
|
||||
if self.last_decimation != 1.0:
|
||||
@ -374,14 +342,10 @@ class dsp(object):
|
||||
if_samp_rate=self.if_samp_rate(),
|
||||
last_decimation=self.last_decimation,
|
||||
audio_rate=self.get_audio_rate(),
|
||||
direwolf_config=self.direwolf_config,
|
||||
direwolf_config=self.direwolf_config_path,
|
||||
)
|
||||
|
||||
logger.debug("secondary command (demod) = %s", secondary_command_demod)
|
||||
my_env = os.environ.copy()
|
||||
# if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
|
||||
if self.csdr_print_bufsizes:
|
||||
my_env["CSDR_PRINT_BUFSIZES"] = "1"
|
||||
if self.output.supports_type("secondary_fft"):
|
||||
secondary_command_fft = " | ".join(self.secondary_chain("fft"))
|
||||
secondary_command_fft = secondary_command_fft.format(
|
||||
@ -394,7 +358,7 @@ class dsp(object):
|
||||
logger.debug("secondary command (fft) = %s", secondary_command_fft)
|
||||
|
||||
self.secondary_process_fft = subprocess.Popen(
|
||||
secondary_command_fft, stdout=subprocess.PIPE, shell=True, start_new_session=True, env=my_env
|
||||
secondary_command_fft, stdout=subprocess.PIPE, shell=True, start_new_session=True
|
||||
)
|
||||
self.output.send_output(
|
||||
"secondary_fft",
|
||||
@ -406,34 +370,18 @@ class dsp(object):
|
||||
# it would block if not read. by piping it to devnull, we avoid a potential pitfall here.
|
||||
secondary_output = subprocess.DEVNULL if self.isPacket() else subprocess.PIPE
|
||||
self.secondary_process_demod = subprocess.Popen(
|
||||
secondary_command_demod, stdout=secondary_output, shell=True, start_new_session=True, env=my_env
|
||||
secondary_command_demod, stdout=secondary_output, shell=True, start_new_session=True
|
||||
)
|
||||
self.secondary_processes_running = True
|
||||
|
||||
if self.isWsjtMode():
|
||||
smd = self.get_secondary_demodulator()
|
||||
chopper_profile = None
|
||||
if smd == "ft8":
|
||||
chopper_profile = Ft8Profile()
|
||||
elif smd == "wspr":
|
||||
chopper_profile = WsprProfile()
|
||||
elif smd == "jt65":
|
||||
chopper_profile = Jt65Profile()
|
||||
elif smd == "jt9":
|
||||
chopper_profile = Jt9Profile()
|
||||
elif smd == "ft4":
|
||||
chopper_profile = Ft4Profile()
|
||||
if chopper_profile is not None:
|
||||
chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile)
|
||||
chopper.start()
|
||||
self.output.send_output("wsjt_demod", chopper.read)
|
||||
elif self.isJs8():
|
||||
chopper = AudioChopper(self, self.secondary_process_demod.stdout, *Js8Profiles.getEnabledProfiles())
|
||||
chopper.start()
|
||||
self.output.send_output("js8_demod", chopper.read)
|
||||
if self.isWsjtMode() or self.isJs8():
|
||||
chopper = AudioChopper(self, self.get_secondary_demodulator())
|
||||
chopper.send_output("audio", self.secondary_process_demod.stdout.read)
|
||||
output_type = "js8_demod" if self.isJs8() else "wsjt_demod"
|
||||
self.output.send_output(output_type, chopper.read)
|
||||
elif self.isPacket():
|
||||
# we best get the ax25 packets from the kiss socket
|
||||
kiss = KissClient(self.direwolf_port)
|
||||
kiss = KissClient(self.direwolf_config.getPort())
|
||||
self.output.send_output("packet_demod", kiss.read)
|
||||
elif self.isPocsag():
|
||||
self.output.send_output("pocsag_demod", self.secondary_process_demod.stdout.readline)
|
||||
@ -447,7 +395,9 @@ class dsp(object):
|
||||
def set_secondary_offset_freq(self, value):
|
||||
self.secondary_offset_freq = value
|
||||
if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"):
|
||||
self.pipes["secondary_shift_pipe"].write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
|
||||
self.pipes["secondary_shift_pipe"].write(
|
||||
"%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())
|
||||
)
|
||||
|
||||
def stop_secondary_demodulator(self):
|
||||
if not self.secondary_processes_running:
|
||||
@ -478,11 +428,16 @@ class dsp(object):
|
||||
return self.secondary_demodulator
|
||||
|
||||
def set_secondary_fft_size(self, secondary_fft_size):
|
||||
# to change this, restart is required
|
||||
if self.secondary_fft_size == secondary_fft_size:
|
||||
return
|
||||
self.secondary_fft_size = secondary_fft_size
|
||||
self.restart()
|
||||
|
||||
def set_audio_compression(self, what):
|
||||
if self.audio_compression == what:
|
||||
return
|
||||
self.audio_compression = what
|
||||
self.restart()
|
||||
|
||||
def get_audio_bytes_to_read(self):
|
||||
# desired latency: 5ms
|
||||
@ -494,7 +449,10 @@ class dsp(object):
|
||||
return int(base)
|
||||
|
||||
def set_fft_compression(self, what):
|
||||
if self.fft_compression == what:
|
||||
return
|
||||
self.fft_compression = what
|
||||
self.restart()
|
||||
|
||||
def get_fft_bytes_to_read(self):
|
||||
if self.fft_compression == "none":
|
||||
@ -515,25 +473,22 @@ class dsp(object):
|
||||
self.restart()
|
||||
|
||||
def calculate_decimation(self):
|
||||
(self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate())
|
||||
(self.decimation, self.last_decimation) = self.get_decimation(self.samp_rate, self.get_audio_rate())
|
||||
|
||||
def get_decimation(self, input_rate, output_rate):
|
||||
if output_rate <= 0:
|
||||
raise ValueError("invalid output rate: {rate}".format(rate=output_rate))
|
||||
decimation = 1
|
||||
correction = 1
|
||||
target_rate = output_rate
|
||||
# wideband fm has a much higher frequency deviation (75kHz).
|
||||
# we cannot cover this if we immediately decimate to the sample rate the audio will have later on, so we need
|
||||
# to compensate here.
|
||||
# the factor of 5 is by experimentation only, with a minimum audio rate of 36kHz (enforced by the client)
|
||||
# this allows us to cover at least +/- 80kHz of frequency spectrum (may be higher, but that's the worst case).
|
||||
# the correction factor is automatically compensated for by the secondary decimation stage, which comes
|
||||
# after the demodulator.
|
||||
if self.get_demodulator() == "wfm":
|
||||
correction = 5
|
||||
while input_rate / (decimation + 1) >= output_rate * correction:
|
||||
if self.get_demodulator() == "wfm" and output_rate < 200000:
|
||||
target_rate = 200000
|
||||
while input_rate / (decimation + 1) >= target_rate:
|
||||
decimation += 1
|
||||
fraction = float(input_rate / decimation) / output_rate
|
||||
intermediate_rate = input_rate / decimation
|
||||
return decimation, fraction, intermediate_rate
|
||||
return decimation, fraction
|
||||
|
||||
def if_samp_rate(self):
|
||||
return self.samp_rate / self.decimation
|
||||
@ -561,14 +516,14 @@ class dsp(object):
|
||||
def isDigitalVoice(self, demodulator=None):
|
||||
if demodulator is None:
|
||||
demodulator = self.get_demodulator()
|
||||
return demodulator in ["dmr", "dstar", "nxdn", "ysf"]
|
||||
return demodulator in ["dmr", "dstar", "nxdn", "ysf", "m17"]
|
||||
|
||||
def isWsjtMode(self, demodulator=None):
|
||||
if demodulator is None:
|
||||
demodulator = self.get_secondary_demodulator()
|
||||
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
|
||||
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]
|
||||
|
||||
def isJs8(self, demodulator = None):
|
||||
def isJs8(self, demodulator=None):
|
||||
if demodulator is None:
|
||||
demodulator = self.get_secondary_demodulator()
|
||||
return demodulator == "js8"
|
||||
@ -625,6 +580,8 @@ class dsp(object):
|
||||
return self.demodulator
|
||||
|
||||
def set_fft_size(self, fft_size):
|
||||
if self.fft_size == fft_size:
|
||||
return
|
||||
self.fft_size = fft_size
|
||||
self.restart()
|
||||
|
||||
@ -656,6 +613,9 @@ class dsp(object):
|
||||
def get_operating_freq(self):
|
||||
return self.center_freq + self.offset_freq
|
||||
|
||||
def set_bandpass(self, bandpass):
|
||||
self.set_bpf(bandpass.low_cut, bandpass.high_cut)
|
||||
|
||||
def set_bpf(self, low_cut, high_cut):
|
||||
self.low_cut = low_cut
|
||||
self.high_cut = high_cut
|
||||
@ -673,7 +633,11 @@ class dsp(object):
|
||||
def set_squelch_level(self, squelch_level):
|
||||
self.squelch_level = squelch_level
|
||||
# no squelch required on digital voice modes
|
||||
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV() else self.squelch_level
|
||||
actual_squelch = (
|
||||
-150
|
||||
if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV()
|
||||
else self.squelch_level
|
||||
)
|
||||
if self.running:
|
||||
self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
|
||||
|
||||
@ -724,27 +688,34 @@ class dsp(object):
|
||||
|
||||
def try_create_configs(self, command):
|
||||
if "{direwolf_config}" in command:
|
||||
self.direwolf_config = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
|
||||
self.direwolf_config_path = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
|
||||
tmp_dir=self.temporary_directory, myid=id(self)
|
||||
)
|
||||
self.direwolf_port = KissClient.getFreePort()
|
||||
file = open(self.direwolf_config, "w")
|
||||
file.write(DirewolfConfig().getConfig(self.direwolf_port, self.is_service))
|
||||
self.direwolf_config = DirewolfConfig()
|
||||
self.direwolf_config.wire(self)
|
||||
file = open(self.direwolf_config_path, "w")
|
||||
file.write(self.direwolf_config.getConfig(self.is_service))
|
||||
file.close()
|
||||
else:
|
||||
self.direwolf_config = None
|
||||
self.direwolf_port = None
|
||||
self.direwolf_config_path = None
|
||||
|
||||
def try_delete_configs(self):
|
||||
if self.direwolf_config:
|
||||
if self.direwolf_config is not None:
|
||||
self.direwolf_config.unwire(self)
|
||||
self.direwolf_config = None
|
||||
if self.direwolf_config_path is not None:
|
||||
try:
|
||||
os.unlink(self.direwolf_config)
|
||||
os.unlink(self.direwolf_config_path)
|
||||
except FileNotFoundError:
|
||||
# result suits our expectations. fine :)
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("try_delete_configs()")
|
||||
self.direwolf_config = None
|
||||
self.direwolf_config_path = None
|
||||
|
||||
def onConfigChanged(self):
|
||||
self.restart()
|
||||
|
||||
def start(self):
|
||||
with self.modification_lock:
|
||||
@ -795,14 +766,9 @@ class dsp(object):
|
||||
)
|
||||
|
||||
logger.debug("Command = %s", command)
|
||||
my_env = os.environ.copy()
|
||||
if self.csdr_dynamic_bufsize:
|
||||
my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
|
||||
if self.csdr_print_bufsizes:
|
||||
my_env["CSDR_PRINT_BUFSIZES"] = "1"
|
||||
|
||||
out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
|
||||
self.process = subprocess.Popen(command, stdout=out, shell=True, start_new_session=True, env=my_env)
|
||||
self.process = subprocess.Popen(command, stdout=out, shell=True, start_new_session=True)
|
||||
|
||||
def watch_thread():
|
||||
rc = self.process.wait()
|
||||
@ -826,6 +792,7 @@ class dsp(object):
|
||||
self.start_secondary_demodulator()
|
||||
|
||||
if self.has_pipe("smeter_pipe"):
|
||||
|
||||
def read_smeter():
|
||||
raw = self.pipes["smeter_pipe"].readline()
|
||||
if len(raw) == 0:
|
||||
@ -835,6 +802,7 @@ class dsp(object):
|
||||
|
||||
self.output.send_output("smeter", read_smeter)
|
||||
if self.has_pipe("meta_pipe"):
|
||||
|
||||
def read_meta():
|
||||
raw = self.pipes["meta_pipe"].readline()
|
||||
if len(raw) == 0:
|
||||
@ -844,10 +812,6 @@ class dsp(object):
|
||||
|
||||
self.output.send_output("meta", read_meta)
|
||||
|
||||
if self.csdr_dynamic_bufsize:
|
||||
self.process.stdout.read(8) # dummy read to skip bufsize & preamble
|
||||
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
|
||||
|
||||
def stop(self):
|
||||
with self.modification_lock:
|
||||
self.running = False
|
||||
@ -863,12 +827,10 @@ class dsp(object):
|
||||
self.stop_secondary_demodulator()
|
||||
|
||||
self.try_delete_pipes(self.pipe_names)
|
||||
self.try_delete_configs()
|
||||
|
||||
def restart(self):
|
||||
if not self.running:
|
||||
return
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def __del__(self):
|
||||
self.stop()
|
36
csdr/output.py
Normal file
@ -0,0 +1,36 @@
|
||||
import threading
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Output(object):
|
||||
def send_output(self, t, read_fn):
|
||||
if not self.supports_type(t):
|
||||
# TODO rewrite the output mechanism in a way that avoids producing unnecessary data
|
||||
logger.warning("dumping output of type %s since it is not supported.", t)
|
||||
threading.Thread(target=self.pump(read_fn, lambda x: None), name="csdr_pump_thread").start()
|
||||
return
|
||||
self.receive_output(t, read_fn)
|
||||
|
||||
def receive_output(self, t, read_fn):
|
||||
pass
|
||||
|
||||
def pump(self, read, write):
|
||||
def copy():
|
||||
run = True
|
||||
while run:
|
||||
data = None
|
||||
try:
|
||||
data = read()
|
||||
except ValueError:
|
||||
pass
|
||||
if data is None or (isinstance(data, bytes) and len(data) == 0):
|
||||
run = False
|
||||
else:
|
||||
write(data)
|
||||
|
||||
return copy
|
||||
|
||||
def supports_type(self, t):
|
||||
return True
|
@ -42,6 +42,7 @@ class Pipe(object):
|
||||
immediately here), resulting in empty reads until data is available. This is handled specially in the
|
||||
ReadingPipe class.
|
||||
"""
|
||||
|
||||
def opener(path, flags):
|
||||
fd = os.open(path, flags | os.O_NONBLOCK)
|
||||
os.set_blocking(fd, True)
|
||||
@ -88,7 +89,7 @@ class WritingPipe(Pipe):
|
||||
except OSError as error:
|
||||
# ENXIO = FIFO has not been opened for reading
|
||||
if error.errno == 6:
|
||||
time.sleep(.1)
|
||||
time.sleep(0.1)
|
||||
retries += 1
|
||||
else:
|
||||
raise
|
||||
|
34
debian/changelog
vendored
@ -1,3 +1,37 @@
|
||||
openwebrx (1.0.0) buster hirsute; urgency=low
|
||||
* Introduced `squelch_auto_margin` config option that allows configuring the
|
||||
auto squelch level
|
||||
* Removed `port` configuration option; `rtltcp_compat` takes the port number
|
||||
with the new connectors
|
||||
* Added support for new WSJT-X modes FST4, FST4W (only available with WSJT-X
|
||||
2.3) and Q65 (only available with WSJT-X 2.4)
|
||||
* Added support for demodulating M17 digital voice signals using
|
||||
m17-cxx-demod
|
||||
* New reporting infrastructure, allowing WSPR and FST4W spots to be sent to
|
||||
wsprnet.org
|
||||
* Add some basic filtering capabilities to the map
|
||||
* New arguments to the `openwebrx` command-line to facilitate the
|
||||
administration of users (try `openwebrx admin`)
|
||||
* New command-line tool `openwebrx-admin` that facilitates the
|
||||
administration of users
|
||||
* Default bandwidth changes:
|
||||
- "WFM" changed to 150kHz
|
||||
- "Packet" (APRS) changed to 12.5kHz
|
||||
* Configuration rework:
|
||||
- New: fully web-based configuration interface
|
||||
- System configuration parameters have been moved to a new, separate
|
||||
`openwebrx.conf` file
|
||||
- Remaining parameters are now editable in the web configuration
|
||||
- Existing `config_webrx.py` files will still be read, but changes made in
|
||||
the web configuration will be written to a new storage system
|
||||
- Added upload of avatar and panorama image via web configuration
|
||||
* New devices supported:
|
||||
- HPSDR devices (Hermes Lite 2) thanks to @jancona
|
||||
- BBRF103 / RX666 / RX888 devices supported by libsddc
|
||||
- R&S devices using the EB200 or Ammos protocols
|
||||
|
||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Thu, 06 May 2021 17:22:00 +0000
|
||||
|
||||
openwebrx (0.20.3) buster focal; urgency=low
|
||||
|
||||
* Fix a compatibility issue with python versions <= 3.6
|
||||
|
4
debian/control
vendored
@ -10,7 +10,7 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git
|
||||
|
||||
Package: openwebrx
|
||||
Architecture: all
|
||||
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.17), netcat, owrx-connector (>= 0.3), python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends}
|
||||
Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools
|
||||
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.17), netcat, owrx-connector (>= 0.4), soapysdr-tools, python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends}
|
||||
Recommends: digiham (>= 0.4), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, runds-connector, hpsdrconnector, aprs-symbols, m17-demod, js8call
|
||||
Description: multi-user web sdr
|
||||
Open source, multi-user SDR receiver with a web interface
|
8
debian/openwebrx.config
vendored
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/sh -e
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
db_get openwebrx/admin_user_configured
|
||||
if [ "${1:-}" = "reconfigure" ] || [ "${RET}" != true ]; then
|
||||
db_input high openwebrx/admin_user_password || true
|
||||
db_go
|
||||
fi
|
1
debian/openwebrx.dirs
vendored
Normal file
@ -0,0 +1 @@
|
||||
/etc/openwebrx/openwebrx.conf.d
|
4
debian/openwebrx.install
vendored
@ -1,5 +1,3 @@
|
||||
config_webrx.py etc/openwebrx/
|
||||
bands.json etc/openwebrx/
|
||||
bookmarks.json etc/openwebrx/
|
||||
users.json etc/openwebrx/
|
||||
openwebrx.conf etc/openwebrx/
|
||||
systemd/openwebrx.service lib/systemd/system/
|
59
debian/openwebrx.postinst
vendored
Executable file
@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OWRX_USER="openwebrx"
|
||||
OWRX_DATADIR="/var/lib/openwebrx"
|
||||
OWRX_USERS_FILE="${OWRX_DATADIR}/users.json"
|
||||
OWRX_SETTINGS_FILE="${OWRX_DATADIR}/settings.json"
|
||||
OWRX_BOOKMARKS_FILE="${OWRX_DATADIR}/bookmarks.json"
|
||||
|
||||
case "$1" in
|
||||
configure|reconfigure)
|
||||
adduser --system --group --no-create-home --home /nonexistent --quiet "${OWRX_USER}"
|
||||
usermod -aG plugdev openwebrx
|
||||
|
||||
# create OpenWebRX data directory and set the correct permissions
|
||||
if [ ! -d "${OWRX_DATADIR}" ] && [ ! -L "${OWRX_DATADIR}" ]; then mkdir "${OWRX_DATADIR}"; fi
|
||||
chown "${OWRX_USER}". ${OWRX_DATADIR}
|
||||
|
||||
# create empty config files now to avoid permission problems later
|
||||
if [ ! -e "${OWRX_USERS_FILE}" ]; then
|
||||
echo "[]" > "${OWRX_USERS_FILE}"
|
||||
chown "${OWRX_USER}". "${OWRX_USERS_FILE}"
|
||||
chmod 0600 "${OWRX_USERS_FILE}"
|
||||
fi
|
||||
|
||||
if [ ! -e "${OWRX_SETTINGS_FILE}" ]; then
|
||||
echo "{}" > "${OWRX_SETTINGS_FILE}"
|
||||
chown "${OWRX_USER}". "${OWRX_SETTINGS_FILE}"
|
||||
fi
|
||||
|
||||
if [ ! -e "${OWRX_BOOKMARKS_FILE}" ]; then
|
||||
touch "${OWRX_BOOKMARKS_FILE}"
|
||||
chown "${OWRX_USER}". "${OWRX_BOOKMARKS_FILE}"
|
||||
fi
|
||||
|
||||
db_get openwebrx/admin_user_password
|
||||
if [ ! -z "${RET}" ]; then
|
||||
if ! openwebrx admin --silent hasuser admin; then
|
||||
# create initial openwebrx user
|
||||
OWRX_PASSWORD="${RET}" openwebrx admin --noninteractive adduser admin
|
||||
else
|
||||
# change existing user's password
|
||||
OWRX_PASSWORD="${RET}" openwebrx admin --noninteractive resetpassword admin
|
||||
fi
|
||||
fi
|
||||
# remove password from debconf database
|
||||
db_unregister openwebrx/admin_user_password
|
||||
# set a marker that admin is configured to avoid future questions
|
||||
db_set openwebrx/admin_user_configured true
|
||||
;;
|
||||
*)
|
||||
echo "postinst called with unknown argument '$1'" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
8
debian/openwebrx.postrm
vendored
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
if [ "$1" = purge ] && [ -e /usr/share/debconf/confmodule ]; then
|
||||
. /usr/share/debconf/confmodule
|
||||
db_purge
|
||||
fi
|
||||
|
||||
#DEBHELPER#
|
23
debian/openwebrx.templates
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
Template: openwebrx/admin_user_password
|
||||
Type: password
|
||||
Description: OpenWebRX "admin" user password:
|
||||
The system can create a user for the OpenWebRX web configuration interface for
|
||||
you. Using this user, you will be able to log into the "settings" area of
|
||||
OpenWebRX to configure your receiver conveniently through your browser.
|
||||
.
|
||||
The name of the created user will be "admin".
|
||||
.
|
||||
If you do not wish to create a web admin user right now, you can leave this
|
||||
empty for now. You can return to this prompt at a later time by running the
|
||||
command "sudo dpkg-reconfigure openwebrx".
|
||||
.
|
||||
You can also use the "openwebrx admin" command to create, delete or manage
|
||||
existing users. More information is available in by running the command
|
||||
"openwebrx admin --help".
|
||||
|
||||
Template: openwebrx/admin_user_configured
|
||||
Type: boolean
|
||||
Default: false
|
||||
Description: OpenWebRX "admin" user previously configured?
|
||||
Marker used internally by the config scripts to remember if an admin user has
|
||||
been created.
|
7
debian/postinst
vendored
@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
adduser --system --group --no-create-home --home /nonexistant openwebrx
|
||||
usermod -aG plugdev openwebrx
|
||||
|
||||
#DEBHELPER#
|
@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
ARCH=$(uname -m)
|
||||
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-redpitaya openwebrx-rtltcp openwebrx-full openwebrx"
|
||||
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-full openwebrx"
|
||||
ALL_ARCHS="x86_64 armv7l aarch64"
|
||||
TAG=${TAG:-"latest"}
|
||||
ARCHTAG="$TAG-$ARCH"
|
||||
|
@ -4,6 +4,7 @@ COPY docker/files/js8call/js8call-hamlib.patch \
|
||||
docker/files/wsjtx/wsjtx.patch \
|
||||
docker/files/wsjtx/wsjtx-hamlib.patch \
|
||||
docker/files/dream/dream.patch \
|
||||
docker/files/direwolf/direwolf-hamlib.patch \
|
||||
docker/scripts/install-dependencies.sh /
|
||||
RUN /install-dependencies.sh && \
|
||||
rm /install-dependencies.sh && \
|
||||
@ -17,7 +18,9 @@ ENTRYPOINT ["/init"]
|
||||
WORKDIR /opt/openwebrx
|
||||
|
||||
VOLUME /etc/openwebrx
|
||||
VOLUME /var/lib/openwebrx
|
||||
|
||||
CMD [ "/opt/openwebrx/docker/scripts/run.sh" ]
|
||||
ENV S6_CMD_ARG0="/opt/openwebrx/docker/scripts/run.sh"
|
||||
CMD []
|
||||
|
||||
EXPOSE 8073
|
||||
|
@ -18,8 +18,9 @@ RUN /install-dependencies-rtlsdr.sh &&\
|
||||
/install-dependencies-fcdpp.sh &&\
|
||||
/install-dependencies-radioberry.sh &&\
|
||||
/install-dependencies-uhd.sh &&\
|
||||
/install-dependencies-redpitaya.sh &&\
|
||||
/install-dependencies-hpsdr.sh &&\
|
||||
/install-connectors.sh &&\
|
||||
/install-dependencies-runds.sh &&\
|
||||
rm /install-dependencies-*.sh &&\
|
||||
rm /install-lib.*.patch && \
|
||||
rm /install-connectors.sh
|
||||
|
9
docker/Dockerfiles/Dockerfile-hpsdr
Normal file
@ -0,0 +1,9 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-base:$ARCHTAG
|
||||
|
||||
COPY docker/scripts/install-dependencies-hpsdr.sh /
|
||||
|
||||
RUN /install-dependencies-hpsdr.sh &&\
|
||||
rm /install-dependencies-hpsdr.sh
|
||||
|
||||
COPY . /opt/openwebrx
|
@ -1,8 +0,0 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
COPY docker/scripts/install-dependencies-redpitaya.sh /
|
||||
RUN /install-dependencies-redpitaya.sh &&\
|
||||
rm /install-dependencies-redpitaya.sh
|
||||
|
||||
COPY . /opt/openwebrx
|
12
docker/Dockerfiles/Dockerfile-runds
Normal file
@ -0,0 +1,12 @@
|
||||
ARG ARCHTAG
|
||||
FROM openwebrx-base:$ARCHTAG
|
||||
|
||||
COPY docker/scripts/install-connectors.sh \
|
||||
docker/scripts/install-dependencies-runds.sh /
|
||||
|
||||
RUN /install-connectors.sh &&\
|
||||
rm /install-connectors.sh && \
|
||||
/install-dependencies-runds.sh && \
|
||||
rm /install-dependencies-runds.sh
|
||||
|
||||
COPY . /opt/openwebrx
|
@ -1,5 +0,0 @@
|
||||
ARCH=$(uname -m)
|
||||
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-redpitaya openwebrx-rtltcp openwebrx-full openwebrx"
|
||||
ALL_ARCHS="x86_64 armv7l aarch64"
|
||||
TAG=${TAG:-"latest"}
|
||||
ARCHTAG="$TAG-$ARCH"
|
20
docker/files/direwolf/direwolf-hamlib.patch
Normal file
@ -0,0 +1,20 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 9e710f5..da90b43 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -257,13 +257,8 @@ else()
|
||||
set(GPSD_LIBRARIES "")
|
||||
endif()
|
||||
|
||||
-find_package(hamlib)
|
||||
-if(HAMLIB_FOUND)
|
||||
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_HAMLIB")
|
||||
-else()
|
||||
- set(HAMLIB_INCLUDE_DIRS "")
|
||||
- set(HAMLIB_LIBRARIES "")
|
||||
-endif()
|
||||
+set(HAMLIB_INCLUDE_DIRS "")
|
||||
+set(HAMLIB_LIBRARIES "")
|
||||
|
||||
if(LINUX)
|
||||
find_package(ALSA REQUIRED)
|
@ -1,18 +1,17 @@
|
||||
--- CMakeLists.txt.orig 2020-07-21 20:59:55.982026645 +0200
|
||||
+++ CMakeLists.txt 2020-07-21 21:01:25.444836112 +0200
|
||||
@@ -80,24 +80,6 @@
|
||||
--- CMakeLists.txt.orig 2021-03-30 15:28:36.956587995 +0200
|
||||
+++ CMakeLists.txt 2021-03-30 15:29:45.719326832 +0200
|
||||
@@ -106,24 +106,6 @@
|
||||
|
||||
include (ExternalProject)
|
||||
|
||||
-
|
||||
-#
|
||||
#
|
||||
-# build and install hamlib locally so it can be referenced by the
|
||||
-# WSJT-X build
|
||||
-#
|
||||
-ExternalProject_Add (hamlib
|
||||
- GIT_REPOSITORY ${hamlib_repo}
|
||||
- GIT_TAG ${hamlib_TAG}
|
||||
- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/${__hamlib_upstream}
|
||||
- GIT_SHALLOW False
|
||||
- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/${__hamlib_upstream}.tar.gz
|
||||
- URL_HASH MD5=${hamlib_md5sum}
|
||||
- #UPDATE_COMMAND ${CMAKE_COMMAND} -E env "[ -f ./bootstrap ] && ./bootstrap"
|
||||
- PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch
|
||||
@ -22,10 +21,11 @@
|
||||
- STEP_TARGETS update install
|
||||
- )
|
||||
-
|
||||
#
|
||||
-#
|
||||
# custom target to make a hamlib source tarball
|
||||
#
|
||||
@@ -136,7 +118,6 @@
|
||||
add_custom_target (hamlib_sources
|
||||
@@ -161,7 +143,6 @@
|
||||
# build and optionally install WSJT-X using the hamlib package built
|
||||
# above
|
||||
#
|
||||
@ -33,11 +33,18 @@
|
||||
ExternalProject_Add (wsjtx
|
||||
GIT_REPOSITORY ${wsjtx_repo}
|
||||
GIT_TAG ${WSJTX_TAG}
|
||||
@@ -160,7 +141,6 @@
|
||||
@@ -186,14 +167,8 @@
|
||||
DEPENDEES build
|
||||
)
|
||||
|
||||
-set_target_properties (hamlib PROPERTIES EXCLUDE_FROM_ALL 1)
|
||||
set_target_properties (wsjtx PROPERTIES EXCLUDE_FROM_ALL 1)
|
||||
|
||||
add_dependencies (wsjtx-configure hamlib-install)
|
||||
-add_dependencies (wsjtx-configure hamlib-install)
|
||||
-add_dependencies (wsjtx-build hamlib-install)
|
||||
-add_dependencies (wsjtx-install hamlib-install)
|
||||
-add_dependencies (wsjtx-package hamlib-install)
|
||||
-
|
||||
# export traditional targets
|
||||
add_custom_target (build ALL DEPENDS wsjtx-build)
|
||||
add_custom_target (install DEPENDS wsjtx-install)
|
||||
|
@ -1,6 +1,6 @@
|
||||
diff -ur wsjtx-orig/CMake/Modules/Findhamlib.cmake wsjtx/CMake/Modules/Findhamlib.cmake
|
||||
--- wsjtx-orig/CMake/Modules/Findhamlib.cmake 2020-07-21 21:10:43.124810140 +0200
|
||||
+++ wsjtx/CMake/Modules/Findhamlib.cmake 2020-07-21 21:11:03.368019114 +0200
|
||||
--- wsjtx-orig/CMake/Modules/Findhamlib.cmake 2021-02-01 20:38:00.947536514 +0100
|
||||
+++ wsjtx/CMake/Modules/Findhamlib.cmake 2021-02-01 20:39:06.273680932 +0100
|
||||
@@ -85,4 +85,4 @@
|
||||
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
|
||||
# TRUE if all listed variables are TRUE
|
||||
@ -8,9 +8,18 @@ diff -ur wsjtx-orig/CMake/Modules/Findhamlib.cmake wsjtx/CMake/Modules/Findhamli
|
||||
-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
|
||||
+find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
|
||||
diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
--- wsjtx-orig/CMakeLists.txt 2020-07-21 21:10:43.124810140 +0200
|
||||
+++ wsjtx/CMakeLists.txt 2020-07-21 22:14:04.454639589 +0200
|
||||
@@ -871,7 +871,7 @@
|
||||
--- wsjtx-orig/CMakeLists.txt 2021-02-01 20:38:00.947536514 +0100
|
||||
+++ wsjtx/CMakeLists.txt 2021-02-01 23:02:22.503027275 +0100
|
||||
@@ -122,7 +122,7 @@
|
||||
option (WSJT_QDEBUG_TO_FILE "Redirect Qt debuging messages to a trace file.")
|
||||
option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON)
|
||||
option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.")
|
||||
-option (WSJT_GENERATE_DOCS "Generate documentation files." ON)
|
||||
+option (WSJT_GENERATE_DOCS "Generate documentation files.")
|
||||
option (WSJT_RIG_NONE_CAN_SPLIT "Allow split operation with \"None\" as rig.")
|
||||
option (WSJT_TRACE_UDP "Debugging option that turns on UDP message protocol diagnostics.")
|
||||
option (WSJT_BUILD_UTILS "Build simulators and code demonstrators." ON)
|
||||
@@ -856,7 +856,7 @@
|
||||
#
|
||||
# libhamlib setup
|
||||
#
|
||||
@ -19,31 +28,37 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
find_package (hamlib 3 REQUIRED)
|
||||
find_program (RIGCTL_EXE rigctl)
|
||||
find_program (RIGCTLD_EXE rigctld)
|
||||
@@ -1348,53 +1348,10 @@
|
||||
|
||||
endif(WSJT_BUILD_UTILS)
|
||||
@@ -1376,60 +1376,6 @@
|
||||
target_link_libraries (jt9 wsjt_fort wsjt_cxx fort_qt)
|
||||
endif (${OPENMP_FOUND} OR APPLE)
|
||||
|
||||
-# build the main application
|
||||
-generate_version_info (wsjtx_VERSION_RESOURCES
|
||||
- NAME wsjtx
|
||||
- BUNDLE ${PROJECT_BUNDLE_NAME}
|
||||
- ICON ${WSJTX_ICON_FILE}
|
||||
- )
|
||||
-
|
||||
-add_executable (wsjtx MACOSX_BUNDLE
|
||||
- ${wsjtx_CXXSRCS}
|
||||
- ${wsjtx_GENUISRCS}
|
||||
- wsjtx.rc
|
||||
- ${WSJTX_ICON_FILE}
|
||||
- ${wsjtx_RESOURCES_RCC}
|
||||
- ${wsjtx_VERSION_RESOURCES}
|
||||
- )
|
||||
-
|
||||
if (WSJT_CREATE_WINMAIN)
|
||||
set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON)
|
||||
endif (WSJT_CREATE_WINMAIN)
|
||||
|
||||
-if (WSJT_CREATE_WINMAIN)
|
||||
- set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON)
|
||||
-endif (WSJT_CREATE_WINMAIN)
|
||||
-
|
||||
-set_target_properties (wsjtx PROPERTIES
|
||||
- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in"
|
||||
- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}"
|
||||
- MACOSX_BUNDLE_INFO_STRING "${PROJECT_DESCRIPTION}"
|
||||
- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}"
|
||||
- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION}
|
||||
- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}"
|
||||
- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}"
|
||||
- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
|
||||
- MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}
|
||||
- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}"
|
||||
- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${SCS_VERSION_STR}"
|
||||
- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_BUNDLE_NAME}"
|
||||
- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
|
||||
- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
|
||||
- MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx"
|
||||
@ -51,9 +66,9 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
-
|
||||
-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS})
|
||||
-if (APPLE)
|
||||
- target_link_libraries (wsjtx Qt5::SerialPort wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
|
||||
- target_link_libraries (wsjtx wsjt_fort)
|
||||
-else ()
|
||||
- target_link_libraries (wsjtx Qt5::SerialPort wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
|
||||
- target_link_libraries (wsjtx wsjt_fort_omp)
|
||||
- if (OpenMP_C_FLAGS)
|
||||
- set_target_properties (wsjtx PROPERTIES
|
||||
- COMPILE_FLAGS "${OpenMP_C_FLAGS}"
|
||||
@ -65,15 +80,16 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
- )
|
||||
- if (WIN32)
|
||||
- set_target_properties (wsjtx PROPERTIES
|
||||
- LINK_FLAGS -Wl,--stack,16777216
|
||||
- LINK_FLAGS -Wl,--stack,0x1000000,--heap,0x20000000
|
||||
- )
|
||||
- endif ()
|
||||
-endif ()
|
||||
-target_link_libraries (wsjtx Qt5::SerialPort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES} ${LIBM_LIBRARIES})
|
||||
-
|
||||
# make a library for WSJT-X UDP servers
|
||||
# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
|
||||
add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
|
||||
@@ -1437,24 +1394,9 @@
|
||||
@@ -1492,24 +1438,9 @@
|
||||
set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
|
||||
endif (WSJT_CREATE_WINMAIN)
|
||||
|
||||
@ -98,21 +114,21 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
|
||||
# install (TARGETS wsjtx_udp EXPORT udp
|
||||
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
@@ -1473,12 +1415,7 @@
|
||||
@@ -1528,12 +1459,7 @@
|
||||
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
|
||||
# )
|
||||
|
||||
-install (TARGETS udp_daemon message_aggregator
|
||||
-install (TARGETS udp_daemon message_aggregator wsjtx_app_version
|
||||
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||
- BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||
- )
|
||||
-
|
||||
-install (TARGETS jt9 wsprd fmtave fcal fmeasure
|
||||
+install (TARGETS jt9 wsprd
|
||||
+install (TARGETS wsjtx_app_version jt9 wsprd
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||
)
|
||||
@@ -1491,39 +1428,6 @@
|
||||
@@ -1546,38 +1472,6 @@
|
||||
)
|
||||
endif(WSJT_BUILD_UTILS)
|
||||
|
||||
@ -143,13 +159,25 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||
- AUTHORS
|
||||
- THANKS
|
||||
- NEWS
|
||||
- INSTALL
|
||||
- BUGS
|
||||
- DESTINATION ${CMAKE_INSTALL_DOCDIR}
|
||||
- #COMPONENT runtime
|
||||
- )
|
||||
-
|
||||
install (FILES
|
||||
contrib/Ephemeris/JPLEPH
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
|
||||
Only in wsjtx: .idea
|
||||
cty.dat
|
||||
cty.dat_copyright.txt
|
||||
@@ -1586,13 +1480,6 @@
|
||||
#COMPONENT runtime
|
||||
)
|
||||
|
||||
-install (DIRECTORY
|
||||
- example_log_configurations
|
||||
- DESTINATION ${CMAKE_INSTALL_DOCDIR}
|
||||
- FILES_MATCHING REGEX "^.*[^~]$"
|
||||
- #COMPONENT runtime
|
||||
- )
|
||||
-
|
||||
#
|
||||
# Mac installer files
|
||||
#
|
||||
|
@ -24,7 +24,7 @@ apt-get update
|
||||
apt-get -y install --no-install-recommends $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/jketterl/owrx_connector.git
|
||||
cmakebuild owrx_connector 0.3.0
|
||||
cmakebuild owrx_connector 0.4.0
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
46
docker/scripts/install-dependencies-hpsdr.sh
Executable file
@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
BUILD_PACKAGES="git wget gcc libc6-dev"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $BUILD_PACKAGES
|
||||
|
||||
pushd /tmp
|
||||
|
||||
ARCH=$(uname -m)
|
||||
GOVERSION=1.15.5
|
||||
|
||||
case ${ARCH} in
|
||||
x86_64)
|
||||
PACKAGE=go${GOVERSION}.linux-amd64.tar.gz
|
||||
;;
|
||||
armv*)
|
||||
PACKAGE=go${GOVERSION}.linux-armv6l.tar.gz
|
||||
;;
|
||||
aarch64)
|
||||
PACKAGE=go${GOVERSION}.linux-arm64.tar.gz
|
||||
;;
|
||||
esac
|
||||
|
||||
wget https://golang.org/dl/${PACKAGE}
|
||||
tar xfz $PACKAGE
|
||||
|
||||
git clone https://github.com/jancona/hpsdrconnector.git
|
||||
pushd hpsdrconnector
|
||||
git checkout v0.4.2
|
||||
/tmp/go/bin/go build
|
||||
install -m 0755 hpsdrconnector /usr/local/bin
|
||||
|
||||
popd
|
||||
|
||||
rm -rf hpsdrconnector
|
||||
rm -rf go
|
||||
rm $PACKAGE
|
||||
|
||||
popd
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -euxo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
function cmakebuild() {
|
||||
@ -19,14 +19,14 @@ function cmakebuild() {
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES=""
|
||||
BUILD_PACKAGES="git cmake make gcc g++"
|
||||
BUILD_PACKAGES="git cmake make gcc g++ pkg-config"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/pothosware/SoapyRedPitaya.git
|
||||
cmakebuild SoapyRedPitaya soapy-redpitaya-0.1.1
|
||||
git clone https://github.com/jketterl/runds_connector.git
|
||||
cmakebuild runds_connector 0.1.0
|
||||
|
||||
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
@ -18,8 +18,8 @@ function cmakebuild() {
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates libqt5gui5 libqt5sql5 libqt5printsupport5 libpulse0 libfaad2 libopus0"
|
||||
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-default libfaad-dev libopus-dev"
|
||||
STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates libqt5gui5 libqt5sql5 libqt5printsupport5 libpulse0 libfaad2 libopus0 libboost-program-options1.67.0 libboost-log1.67.0"
|
||||
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-default libfaad-dev libopus-dev libgtest-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev"
|
||||
apt-get update
|
||||
apt-get -y install auto-apt-proxy
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
@ -60,7 +60,7 @@ rm /js8call-hamlib.patch
|
||||
CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_HAMLIB_THREE" cmakebuild ${JS8CALL_DIR}
|
||||
rm ${JS8CALL_TGZ}
|
||||
|
||||
WSJT_DIR=wsjtx-2.2.2
|
||||
WSJT_DIR=wsjtx-2.3.1
|
||||
WSJT_TGZ=${WSJT_DIR}.tgz
|
||||
wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
|
||||
tar xfz ${WSJT_TGZ}
|
||||
@ -69,13 +69,17 @@ mv /wsjtx.patch ${WSJT_DIR}
|
||||
cmakebuild ${WSJT_DIR}
|
||||
rm ${WSJT_TGZ}
|
||||
|
||||
git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git
|
||||
git clone --depth 1 -b 1.6 https://github.com/wb2osz/direwolf.git
|
||||
cd direwolf
|
||||
# hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need.
|
||||
# by setting enable_hamlib we prevent direwolf from linking to it, and it can be stripped at the end of the script.
|
||||
make enable_hamlib=
|
||||
# this patch prevents direwolf from linking to it, and it can be stripped at the end of the script.
|
||||
patch -Np1 < /direwolf-hamlib.patch
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install
|
||||
cd ..
|
||||
cd ../..
|
||||
rm -rf direwolf
|
||||
# strip lots of generic documentation that will never be read inside a docker container
|
||||
rm /usr/local/share/doc/direwolf/*.pdf
|
||||
@ -106,9 +110,15 @@ popd
|
||||
rm -rf dream
|
||||
rm dream-2.1.1-svn808.tar.gz
|
||||
|
||||
git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols
|
||||
pushd /opt/aprs-symbols
|
||||
git clone https://github.com/mobilinkd/m17-cxx-demod.git
|
||||
# latest master as of 2021-04-20
|
||||
cmakebuild m17-cxx-demod c1d954fd5e5c53d28a2524e99484f832f9dcb826
|
||||
|
||||
git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols
|
||||
pushd /usr/share/aprs-symbols
|
||||
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
|
||||
# remove unused files (including git meta information)
|
||||
rm -rf .git aprs-symbols.ai aprs-sym-export.js
|
||||
popd
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
|
@ -41,7 +41,7 @@ cd ..
|
||||
rm -rf csdr
|
||||
|
||||
git clone https://github.com/jketterl/digiham.git
|
||||
cmakebuild digiham 0.3.0
|
||||
cmakebuild digiham 0.4.0
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
@ -1,19 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p /etc/openwebrx/
|
||||
mkdir -p /etc/openwebrx/openwebrx.conf.d
|
||||
mkdir -p /var/lib/openwebrx
|
||||
mkdir -p /tmp/openwebrx/
|
||||
if [[ ! -f /etc/openwebrx/config_webrx.py ]] ; then
|
||||
sed 's/temporary_directory = "\/tmp"/temporary_directory = "\/tmp\/openwebrx"/' < "/opt/openwebrx/config_webrx.py" > "/etc/openwebrx/config_webrx.py"
|
||||
if [[ ! -f /etc/openwebrx/openwebrx.conf.d/20-temporary-directory.conf ]] ; then
|
||||
cat << EOF > /etc/openwebrx/openwebrx.conf.d/20-temporary-directory.conf
|
||||
[core]
|
||||
temporary_directory = /tmp/openwebrx
|
||||
EOF
|
||||
fi
|
||||
if [[ ! -f /etc/openwebrx/bands.json ]] ; then
|
||||
cp bands.json /etc/openwebrx/
|
||||
fi
|
||||
if [[ ! -f /etc/openwebrx/bookmarks.json ]] ; then
|
||||
cp bookmarks.json /etc/openwebrx/
|
||||
if [[ ! -f /etc/openwebrx/openwebrx.conf ]] ; then
|
||||
cp openwebrx.conf /etc/openwebrx/
|
||||
fi
|
||||
if [[ ! -f /etc/openwebrx/users.json ]] ; then
|
||||
cp users.json /etc/openwebrx/
|
||||
if [[ ! -z "${OPENWEBRX_ADMIN_USER:-}" ]] && [[ ! -z "${OPENWEBRX_ADMIN_PASSWORD:-}" ]] ; then
|
||||
if ! python3 openwebrx.py admin --silent hasuser "${OPENWEBRX_ADMIN_USER}" ; then
|
||||
OWRX_PASSWORD="${OPENWEBRX_ADMIN_PASSWORD}" python3 openwebrx.py admin --noninteractive adduser "${OPENWEBRX_ADMIN_USER}"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
@ -1,14 +1,162 @@
|
||||
@import url("openwebrx-header.css");
|
||||
@import url("openwebrx-globals.css");
|
||||
|
||||
html, body {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #222;
|
||||
z-index: 2;
|
||||
padding: 10px;
|
||||
text-align: right;
|
||||
border-top: 1px solid #444;
|
||||
}
|
||||
|
||||
.row .map-input {
|
||||
margin: 15px 15px 0;
|
||||
}
|
||||
|
||||
.device {
|
||||
margin-top: 20px;
|
||||
.settings-section h3 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 1em 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.matrix {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.q65-matrix {
|
||||
grid-template-columns: repeat(5, auto);
|
||||
}
|
||||
|
||||
.imageupload .image-container {
|
||||
max-width: 100%;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.imageupload img.webrx-top-photo {
|
||||
max-height: 350px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.settings-grid > div {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-grid .btn {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
padding: 20px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.tab-body {
|
||||
overflow: auto;
|
||||
border: 1px solid #444;
|
||||
border-top: none;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.tab-body .form-group {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.bookmarks table .frequency, .bookmark-list table .frequency {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bookmarks table input, .bookmarks table select {
|
||||
width: initial;
|
||||
text-align: inherit;
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.bookmark-list table .form-check-input {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.actions .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wsjt-decoding-depths-table {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wsjt-decoding-depths-table td:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.sdr-device-list .list-group-item,
|
||||
.sdr-profile-list .list-group-item {
|
||||
background: initial;
|
||||
}
|
||||
|
||||
.sdr-device-list .sdr-profile-list {
|
||||
max-height: 20rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.removable-group.removable, .add-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.removable-group.removable .removable-item, .add-group .add-group-select {
|
||||
flex: 1 0 auto;
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
.removable-group.removable .option-remove-button, .add-group .option-add-button {
|
||||
flex: 0 0 70px;
|
||||
}
|
||||
|
||||
.option-add-button, .option-remove-button {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.scheduler-static-time-inputs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.scheduler-static-time-inputs > * {
|
||||
flex: 0 0 auto;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.scheduler-static-time-inputs > select {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.imageupload.is-invalid ~ .invalid-feedback {
|
||||
display: block;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
@import url("openwebrx-header.css");
|
||||
@import url("openwebrx-globals.css");
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 50px 0;
|
||||
}
|
@ -1,6 +1,16 @@
|
||||
@import url("openwebrx-header.css");
|
||||
@import url("openwebrx-globals.css");
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
|
@ -6,10 +6,6 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#webrx-top-container {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.openwebrx-map {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
@ -25,10 +21,18 @@ ul {
|
||||
padding-inline-start: 25px;
|
||||
}
|
||||
|
||||
/* don't show the filter in it's initial position */
|
||||
.openwebrx-map-legend {
|
||||
display: none;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* show it as soon as google maps has moved it to its container */
|
||||
.openwebrx-map .openwebrx-map-legend {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.openwebrx-map-legend ul {
|
||||
@ -36,6 +40,15 @@ ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.openwebrx-map-legend ul li {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.openwebrx-map-legend ul li.disabled {
|
||||
opacity: .3;
|
||||
filter: grayscale(70%);
|
||||
}
|
||||
|
||||
.openwebrx-map-legend li.square .illustration {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
|
@ -1,32 +1,36 @@
|
||||
#webrx-top-container
|
||||
{
|
||||
.webrx-top-container {
|
||||
position: relative;
|
||||
z-index:1000;
|
||||
background-color: #575757;
|
||||
}
|
||||
|
||||
#webrx-top-photo
|
||||
{
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
background-image: url(../gfx/openwebrx-top-photo.jpg);
|
||||
background-position-x: center;
|
||||
background-position-y: top;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
|
||||
#webrx-top-photo-clip
|
||||
{
|
||||
min-height: 67px;
|
||||
max-height: 67px;
|
||||
height: 350px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.webrx-top-bar-parts
|
||||
{
|
||||
.openwebrx-description-container {
|
||||
transition-property: height, opacity;
|
||||
transition-duration: 1s;
|
||||
transition-timing-function: ease-out;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
/* originally, top-bar + description was 350px */
|
||||
max-height: 283px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.openwebrx-description-container.expanded {
|
||||
opacity: 1;
|
||||
height: 283px;
|
||||
}
|
||||
|
||||
.webrx-top-bar {
|
||||
height:67px;
|
||||
}
|
||||
|
||||
#webrx-top-bar
|
||||
{
|
||||
background: rgba(128, 128, 128, 0.15);
|
||||
margin:0;
|
||||
padding:0;
|
||||
@ -37,34 +41,30 @@
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#webrx-tob-container, #webrx-top-container * {
|
||||
.webrx-top-bar > * {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.webrx-top-container, .webrx-top-container * {
|
||||
line-height: initial;
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
#webrx-top-container img {
|
||||
vertical-align: initial;
|
||||
}
|
||||
|
||||
#webrx-top-logo
|
||||
{
|
||||
.webrx-top-logo {
|
||||
padding: 12px;
|
||||
float: left;
|
||||
/* overwritten by media queries */
|
||||
display: none;
|
||||
}
|
||||
|
||||
#webrx-rx-avatar
|
||||
{
|
||||
.webrx-rx-avatar {
|
||||
background-color: rgba(154, 154, 154, .5);
|
||||
float: left;
|
||||
margin: 7px;
|
||||
|
||||
cursor:pointer;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
padding: 4px;
|
||||
@ -72,88 +72,79 @@
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
#webrx-rx-texts {
|
||||
float: left;
|
||||
padding: 10px;
|
||||
.webrx-rx-texts {
|
||||
/* minimum layout width */
|
||||
width: 0;
|
||||
/* will be getting wider with flex */
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
#webrx-rx-texts div {
|
||||
.webrx-rx-texts div, .webrx-rx-texts h1 {
|
||||
margin: 0 10px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#webrx-rx-title
|
||||
{
|
||||
white-space:nowrap;
|
||||
overflow: hidden;
|
||||
cursor:pointer;
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
color: #909090;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.webrx-rx-title {
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
font-size: 11pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#webrx-rx-desc
|
||||
{
|
||||
white-space:nowrap;
|
||||
overflow: hidden;
|
||||
cursor:pointer;
|
||||
.webrx-rx-desc {
|
||||
font-size: 10pt;
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
#webrx-rx-desc a
|
||||
{
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow
|
||||
{
|
||||
cursor:pointer;
|
||||
.openwebrx-rx-details-arrow {
|
||||
position: absolute;
|
||||
left: 470px;
|
||||
top: 55px;
|
||||
}
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
#openwebrx-rx-details-arrow a
|
||||
{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons .button {
|
||||
.openwebrx-main-buttons .button {
|
||||
display: block;
|
||||
width: 55px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons .button img {
|
||||
.openwebrx-main-buttons .button[data-toggle-panel] {
|
||||
/* will be enabled by javascript if the panel is present in the DOM */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.openwebrx-main-buttons .button img {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons a {
|
||||
.openwebrx-main-buttons a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons .button:hover
|
||||
{
|
||||
.openwebrx-main-buttons .button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons .button:active
|
||||
{
|
||||
.openwebrx-main-buttons .button:active {
|
||||
background-color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
|
||||
#openwebrx-main-buttons
|
||||
{
|
||||
.openwebrx-main-buttons {
|
||||
padding: 5px 15px;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
float: right;
|
||||
margin:0;
|
||||
color: white;
|
||||
text-shadow: 0px 0px 4px #000000;
|
||||
@ -162,23 +153,17 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-title
|
||||
{
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 78px;
|
||||
color: White;
|
||||
.webrx-rx-photo-title {
|
||||
margin: 10px 15px;
|
||||
color: white;
|
||||
font-size: 16pt;
|
||||
text-shadow: 1px 1px 4px #444;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-desc
|
||||
{
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 109px;
|
||||
color: White;
|
||||
.webrx-rx-photo-desc {
|
||||
margin: 10px 15px;
|
||||
color: white;
|
||||
font-size: 10pt;
|
||||
font-weight: bold;
|
||||
text-shadow: 0px 0px 6px #444;
|
||||
@ -186,12 +171,41 @@
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-desc a
|
||||
{
|
||||
.webrx-rx-photo-desc a {
|
||||
color: #5ca8ff;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.openwebrx-photo-trigger {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
* Responsive stuff
|
||||
*/
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.webrx-rx-texts {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.webrx-top-logo {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Sprites (images)
|
||||
*/
|
||||
|
||||
.sprite-panel-status {
|
||||
background-position: 0 0;
|
||||
width: 44px;
|
||||
@ -222,13 +236,13 @@
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.sprite-rx-details-arrow-down {
|
||||
.openwebrx-rx-details-arrow--down .sprite-rx-details-arrow {
|
||||
background-position: 0 -65px;
|
||||
width: 43px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.sprite-rx-details-arrow-up {
|
||||
.openwebrx-rx-details-arrow--up .sprite-rx-details-arrow {
|
||||
background-position: -43px -65px;
|
||||
width: 43px;
|
||||
height: 12px;
|
||||
|
@ -3,7 +3,7 @@
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -36,15 +36,14 @@ input
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
input[type=range]
|
||||
{
|
||||
input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
margin: 0 0;
|
||||
background: transparent;
|
||||
background: transparent !important;
|
||||
--track-background: #B6B6B6;
|
||||
}
|
||||
input[type=range]:focus
|
||||
{
|
||||
|
||||
input[type=range]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@ -297,11 +296,13 @@ input[type=range]:disabled {
|
||||
#webrx-canvas-container canvas
|
||||
{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border-style: none;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#openwebrx-log-scroll
|
||||
@ -336,12 +337,58 @@ input[type=range]:disabled {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.webrx-actual-freq > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.webrx-actual-freq .input-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.webrx-actual-freq .input-group > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.webrx-actual-freq .input-group input {
|
||||
flex: 1 0 auto;
|
||||
margin-right: 0;
|
||||
border-right: 1px solid #373737;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.webrx-actual-freq .input-group input::-webkit-outer-spin-button,
|
||||
.webrx-actual-freq .input-group input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.input-group > :not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > :not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-group :first-child {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.input-group :last-child {
|
||||
padding-right: 5px
|
||||
}
|
||||
|
||||
.webrx-actual-freq .input-group input, .webrx-actual-freq .input-group select {
|
||||
outline: none;
|
||||
font-size: 16pt;
|
||||
}
|
||||
|
||||
.webrx-actual-freq input {
|
||||
font-family: 'roboto-mono';
|
||||
width: 0;
|
||||
@ -355,7 +402,17 @@ input[type=range]:disabled {
|
||||
.webrx-actual-freq, .webrx-actual-freq input {
|
||||
font-size: 16pt;
|
||||
font-family: 'roboto-mono';
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.webrx-actual-freq .digit {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.webrx-actual-freq .digit:hover {
|
||||
color: #FFFF50;
|
||||
border-radius: 5px;
|
||||
background: -webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) );
|
||||
background: -moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
|
||||
}
|
||||
|
||||
.webrx-mouse-freq {
|
||||
@ -544,21 +601,35 @@ img.openwebrx-mirror-img
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
overflow: hidden;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.openwebrx-progressbar-bar
|
||||
{
|
||||
.openwebrx-progressbar-bar {
|
||||
background-color: #00aba6;
|
||||
border-radius: 5px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transition-property: transform, background-color;
|
||||
transition-duration: 1s;
|
||||
transition-timing-function: ease-in-out;
|
||||
transform: translate(-100%) translateZ(0);
|
||||
will-change: transform, background-color;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.openwebrx-progressbar--over .openwebrx-progressbar-bar {
|
||||
background-color: #ff6262;
|
||||
}
|
||||
|
||||
.openwebrx-progressbar-text
|
||||
{
|
||||
position: absolute;
|
||||
left:0px;
|
||||
top:4px;
|
||||
width: inherit;
|
||||
left:50%;
|
||||
top:50%;
|
||||
transform: translate(-50%, -50%);
|
||||
white-space: nowrap;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#openwebrx-panel-status
|
||||
@ -583,6 +654,7 @@ img.openwebrx-mirror-img
|
||||
#openwebrx-panel-receiver .frequencies-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-receiver .frequencies {
|
||||
@ -658,8 +730,7 @@ img.openwebrx-mirror-img
|
||||
}
|
||||
}
|
||||
|
||||
#openwebrx-smeter-outer
|
||||
{
|
||||
#openwebrx-smeter {
|
||||
border-color: #888;
|
||||
border-style: solid;
|
||||
border-width: 0px;
|
||||
@ -667,16 +738,20 @@ img.openwebrx-mirror-img
|
||||
height: 7px;
|
||||
background-color: #373737;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#openwebrx-smeter-bar
|
||||
{
|
||||
transition: all 0.2s linear;
|
||||
width: 0px;
|
||||
height: 7px;
|
||||
|
||||
.openwebrx-smeter-bar {
|
||||
transition-property: transform;
|
||||
transition-duration: 0.2s;
|
||||
transition-timing-function: linear;
|
||||
will-change: transform;
|
||||
transform: translate(-100%) translateZ(0);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to top, #ff5939 , #961700);
|
||||
position: absolute;
|
||||
margin: 0; padding: 0; left: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@ -746,11 +821,14 @@ img.openwebrx-mirror-img
|
||||
#openwebrx-digimode-canvas-container canvas
|
||||
{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
pointer-events: none;
|
||||
transition: width 500ms, left 500ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.openwebrx-panel select,
|
||||
.openwebrx-panel input,
|
||||
.openwebrx-dialog select,
|
||||
.openwebrx-dialog input {
|
||||
border-radius: 5px;
|
||||
@ -759,11 +837,26 @@ img.openwebrx-mirror-img
|
||||
font-weight: normal;
|
||||
font-size: 13pt;
|
||||
margin-right: 1px;
|
||||
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) );
|
||||
background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
|
||||
background:linear-gradient(#373737, #4F4F4F);
|
||||
border-color: transparent;
|
||||
border-width: 0px;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
@supports(-moz-appearance: none) {
|
||||
.openwebrx-panel select,
|
||||
.openwebrx-dialog select {
|
||||
-moz-appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%20%20xmlns%3Av%3D%22https%3A%2F%2Fvecta.io%2Fnano%22%3E%3Cpath%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8s-1.9-9.2-5.5-12.8z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E'),
|
||||
linear-gradient(#373737, #4F4F4F);
|
||||
background-repeat: no-repeat, repeat;
|
||||
background-position: right .3em top 50%, 0 0;
|
||||
background-size: .65em auto, 100%;
|
||||
}
|
||||
|
||||
.openwebrx-panel .input-group select,
|
||||
.openwebrx-dialog .input-group select {
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.openwebrx-panel select option,
|
||||
@ -886,20 +979,36 @@ img.openwebrx-mirror-img
|
||||
border-color: Red;
|
||||
}
|
||||
|
||||
.openwebrx-meta-panel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
/* compatibility with iOS 14.2 */
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot {
|
||||
flex: 1;
|
||||
width: 145px;
|
||||
height: 196px;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
|
||||
background-color: #676767;
|
||||
padding: 2px 0;
|
||||
color: #333;
|
||||
|
||||
text-align: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot > * {
|
||||
flex: 0;
|
||||
flex-basis: 1.2em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before {
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
@ -910,8 +1019,6 @@ img.openwebrx-mirror-img
|
||||
display: block;
|
||||
content: "";
|
||||
background-image: url("../gfx/openwebrx-mute.png");
|
||||
width:100%;
|
||||
height:133px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
@ -939,27 +1046,44 @@ img.openwebrx-mirror-img
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot .openwebrx-meta-user-image {
|
||||
width:100%;
|
||||
height:133px;
|
||||
flex: 1;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.active .openwebrx-meta-user-image {
|
||||
.openwebrx-meta-slot.active.direct .openwebrx-meta-user-image,
|
||||
#openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image {
|
||||
background-image: url("../gfx/openwebrx-directcall.png");
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.active .openwebrx-meta-user-image.group {
|
||||
.openwebrx-meta-slot.active.group .openwebrx-meta-user-image {
|
||||
background-image: url("../gfx/openwebrx-groupcall.png");
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.group .openwebrx-dmr-target:not(:empty):before {
|
||||
content: "Talkgroup: ";
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.direct .openwebrx-dmr-target:not(:empty):before {
|
||||
content: "Direct: ";
|
||||
}
|
||||
|
||||
.openwebrx-dmr-timeslot-panel * {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.openwebrx-ysf-mode:not(:empty):before {
|
||||
content: "Mode: ";
|
||||
}
|
||||
|
||||
.openwebrx-ysf-up:not(:empty):before {
|
||||
content: "Up: ";
|
||||
}
|
||||
|
||||
.openwebrx-ysf-down:not(:empty):before {
|
||||
content: "Down: ";
|
||||
}
|
||||
|
||||
.openwebrx-maps-pin {
|
||||
@ -974,6 +1098,7 @@ img.openwebrx-mirror-img
|
||||
|
||||
.openwebrx-message-panel {
|
||||
height: 180px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.openwebrx-message-panel tbody {
|
||||
@ -1139,6 +1264,9 @@ img.openwebrx-mirror-img
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
|
||||
@ -1146,7 +1274,10 @@ img.openwebrx-mirror-img
|
||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel
|
||||
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-select-channel
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
@ -1158,7 +1289,10 @@ img.openwebrx-mirror-img
|
||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container
|
||||
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container
|
||||
{
|
||||
height: 200px;
|
||||
margin: -10px;
|
||||
@ -1205,12 +1339,12 @@ img.openwebrx-mirror-img
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
#openwebrx-mute-on .sprite-speaker {
|
||||
background-position: -117px -38px;
|
||||
.openwebrx-mute-button .sprite-speaker {
|
||||
background-position: -103px -38px;
|
||||
}
|
||||
|
||||
#openwebrx-mute-off .sprite-speaker {
|
||||
background-position: -103px -38px;
|
||||
.openwebrx-mute-button.muted .sprite-speaker {
|
||||
background-position: -117px -38px;
|
||||
}
|
||||
|
||||
.sprite-squelch {
|
||||
|
@ -3,7 +3,6 @@
|
||||
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
|
||||
<link rel="stylesheet" href="static/css/features.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script>
|
||||
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
||||
<script src="static/lib/Header.js"></script>
|
||||
@ -11,6 +10,7 @@
|
||||
</HEAD><BODY>
|
||||
${header}
|
||||
<div class="container">
|
||||
${breadcrumb}
|
||||
<h1>OpenWebRX Feature Report</h1>
|
||||
<table class="features table">
|
||||
<tr>
|
||||
@ -20,5 +20,6 @@
|
||||
<th>Available</th>
|
||||
</tr>
|
||||
</table>
|
||||
${breadcrumb}
|
||||
</div>
|
||||
</BODY></HTML>
|
@ -13,8 +13,7 @@ $(function(){
|
||||
});
|
||||
$table.append(
|
||||
'<tr>' +
|
||||
'<td colspan=2>' + name + '</td>' +
|
||||
'<td>' + converter.makeHtml(details.description) + '</td>' +
|
||||
'<td colspan=3>' + name + '</td>' +
|
||||
'<td>' + (details.available ? 'YES' : 'NO') + '</td>' +
|
||||
'</tr>' +
|
||||
requirements.join("")
|
||||
|
@ -1,20 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenWebRX Settings</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
|
||||
<script src="https://unpkg.com/location-picker/dist/location-picker.min.js"></script>
|
||||
<script src="compiled/settings.js"></script>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
${header}
|
||||
<div class="container">
|
||||
<div class="col-12">
|
||||
<h1>General settings</h1>
|
||||
</div>
|
||||
${sections}
|
||||
</div>
|
||||
</body>
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 25 KiB |
1
htdocs/gfx/openwebrx-play-button.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="700" height="700" xmlns="http://www.w3.org/2000/svg"><g class="layer"><circle cx="350" cy="350" r="330" stroke="#fff" stroke-width="36" fill="none"/><path d="M195 211v278l366-139-366-139z" fill="#fff"/></g></svg>
|
After Width: | Height: | Size: 224 B |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 6.7 KiB |
@ -1,26 +1,22 @@
|
||||
<div id="webrx-top-container">
|
||||
<div id="webrx-top-photo-clip">
|
||||
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo" alt="Receiver panorama"/>
|
||||
<div id="webrx-top-bar" class="webrx-top-bar-parts">
|
||||
<a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a>
|
||||
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
|
||||
<div id="webrx-rx-texts">
|
||||
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
|
||||
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
|
||||
</div>
|
||||
<div id="openwebrx-rx-details-arrow">
|
||||
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger" style="display: none;"><span class="sprite sprite-rx-details-arrow-up"></span></a>
|
||||
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><span class="sprite sprite-rx-details-arrow-down"></span></a>
|
||||
</div>
|
||||
<section id="openwebrx-main-buttons">
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-status"><span class="sprite sprite-panel-status"></span><br/>Status</div>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-log"><span class="sprite sprite-panel-log"></span><br/>Log</div>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><span class="sprite sprite-panel-receiver"></span><br/>Receiver</div>
|
||||
<a class="button" href="map" target="openwebrx-map"><span class="sprite sprite-panel-map"></span><br/>Map</a>
|
||||
${settingslink}
|
||||
</section>
|
||||
<div class="webrx-top-container">
|
||||
<div class="webrx-top-bar">
|
||||
<a href="https://www.openwebrx.de/" target="_blank"><img src="${document_root}static/gfx/openwebrx-top-logo.png" class="webrx-top-logo" alt="OpenWebRX Logo"/></a>
|
||||
<img class="webrx-rx-avatar openwebrx-photo-trigger" src="${document_root}static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
|
||||
<div class="webrx-rx-texts openwebrx-photo-trigger">
|
||||
<h1 class="webrx-rx-title">${receiver_name}</h1>
|
||||
<div class="webrx-rx-desc">${receiver_location} | Loc: ${locator}, ASL: ${receiver_asl} m</div>
|
||||
</div>
|
||||
<div id="webrx-rx-photo-title"></div>
|
||||
<div id="webrx-rx-photo-desc"></div>
|
||||
<section class="openwebrx-main-buttons">
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-status"><span class="sprite sprite-panel-status"></span><br/>Status</div>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-log"><span class="sprite sprite-panel-log"></span><br/>Log</div>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><span class="sprite sprite-panel-receiver"></span><br/>Receiver</div>
|
||||
<a class="button" href="${document_root}map" target="openwebrx-map"><span class="sprite sprite-panel-map"></span><br/>Map</a>
|
||||
<a class="button" href="${document_root}settings" target="openwebrx-settings"><span class="sprite sprite-panel-settings"></span><br/>Settings</a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="openwebrx-description-container">
|
||||
<div class="webrx-rx-photo-title">${photo_title}</div>
|
||||
<div class="webrx-rx-photo-desc">${photo_desc}</div>
|
||||
</div>
|
||||
<a class="openwebrx-rx-details-arrow openwebrx-rx-details-arrow--down openwebrx-photo-trigger"><span class="sprite sprite-rx-details-arrow"></span></a>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -28,6 +28,8 @@
|
||||
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
|
||||
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="theme-color" content="#222" />
|
||||
</head>
|
||||
<body onload="openwebrx_init();">
|
||||
<div id="webrx-page-container">
|
||||
@ -56,67 +58,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message">
|
||||
<thead><tr>
|
||||
<th>UTC</th>
|
||||
<th class="decimal">dB</th>
|
||||
<th class="decimal">DT</th>
|
||||
<th class="decimal freq">Freq</th>
|
||||
<th class="message">Message</th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message">
|
||||
<thead><tr>
|
||||
<th>UTC</th>
|
||||
<th class="decimal freq">Freq</th>
|
||||
<th class="message">Message</th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message">
|
||||
<thead><tr>
|
||||
<th>UTC</th>
|
||||
<th class="callsign">Callsign</th>
|
||||
<th class="coord">Coord</th>
|
||||
<th class="message">Comment</th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message">
|
||||
<thead><tr>
|
||||
<th class="address">Address</th>
|
||||
<th class="message">Message</th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" style="display: none;" data-panel-name="metadata-ysf">
|
||||
<div class="openwebrx-meta-frame">
|
||||
<div class="openwebrx-meta-slot">
|
||||
<div class="openwebrx-ysf-mode openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-ysf-source openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-ysf-up openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-ysf-down openwebrx-meta-autoclear"></div>
|
||||
</div>
|
||||
<div class="openwebrx-meta-slot">
|
||||
<div class="openwebrx-ysf-mode"></div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-ysf-source"><span class="location"></span><span class="callsign"></span></div>
|
||||
<div class="openwebrx-ysf-up"></div>
|
||||
<div class="openwebrx-ysf-down"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" style="display: none;" data-panel-name="metadata-dmr">
|
||||
<div class="openwebrx-meta-frame">
|
||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||
<div class="openwebrx-dmr-slot">Timeslot 1</div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
|
||||
</div>
|
||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||
<div class="openwebrx-dmr-slot">Timeslot 2</div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
|
||||
</div>
|
||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||
<div class="openwebrx-dmr-slot">Timeslot 1</div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-dmr-id"></div>
|
||||
<div class="openwebrx-dmr-name"></div>
|
||||
<div class="openwebrx-dmr-target"></div>
|
||||
</div>
|
||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||
<div class="openwebrx-dmr-slot">Timeslot 2</div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-dmr-id"></div>
|
||||
<div class="openwebrx-dmr-name"></div>
|
||||
<div class="openwebrx-dmr-target"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" style="width: 619px;">
|
||||
@ -158,13 +126,13 @@
|
||||
</div>
|
||||
<div class="openwebrx-modes openwebrx-panel-line"></div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><span class="sprite sprite-speaker openwebrx-sliderbtn-img"></span></div>
|
||||
<div title="Mute on/off" class="openwebrx-button openwebrx-mute-button" onclick="toggleMute();"><span class="sprite sprite-speaker openwebrx-sliderbtn-img"></span></div>
|
||||
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
|
||||
<div title="Auto-adjust waterfall colors (right-click for continuous)" id="openwebrx-waterfall-colors-auto" class="openwebrx-button"><span class="sprite sprite-waterfall-auto openwebrx-sliderbtn-img"></span></div>
|
||||
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div title="Auto-set squelch level" class="openwebrx-squelch-default openwebrx-button"><span class="sprite sprite-squelch openwebrx-sliderbtn-img"></span></div>
|
||||
<div title="Auto-set squelch level" class="openwebrx-squelch-auto openwebrx-button"><span class="sprite sprite-squelch openwebrx-sliderbtn-img"></span></div>
|
||||
<input title="Squelch" class="openwebrx-squelch-slider openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1">
|
||||
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><span class="sprite sprite-waterfall-default openwebrx-sliderbtn-img"></span></div>
|
||||
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
|
||||
@ -177,20 +145,14 @@
|
||||
<div id="openwebrx-smeter-db">0 dB</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div id="openwebrx-smeter-outer">
|
||||
<div id="openwebrx-smeter-bar"></div>
|
||||
<div id="openwebrx-smeter">
|
||||
<div class="openwebrx-smeter-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="openwebrx-autoplay-overlay" class="openwebrx-overlay" style="display:none;">
|
||||
<div class="overlay-content">
|
||||
<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" />
|
||||
<div>Start OpenWebRX</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="openwebrx-error-overlay" class="openwebrx-overlay" style="display:none;">
|
||||
<div class="overlay-content">
|
||||
<div>This receiver is currently unavailable due to technical issues.</div>
|
||||
|
@ -6,15 +6,15 @@ function AudioEngine(maxBufferLength, audioReporter) {
|
||||
this.audioReporter = audioReporter;
|
||||
this.initStats();
|
||||
this.resetStats();
|
||||
var ctx = window.AudioContext || window.webkitAudioContext;
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onStartCallbacks = [];
|
||||
|
||||
this.started = false;
|
||||
this.audioContext = new ctx();
|
||||
this.audioContext = this.buildAudioContext();
|
||||
if (!this.audioContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
var me = this;
|
||||
this.audioContext.onstatechange = function() {
|
||||
if (me.audioContext.state !== 'running') return;
|
||||
@ -31,6 +31,38 @@ function AudioEngine(maxBufferLength, audioReporter) {
|
||||
this.maxBufferSize = maxBufferLength * this.getSampleRate();
|
||||
}
|
||||
|
||||
AudioEngine.prototype.buildAudioContext = function() {
|
||||
var ctxClass = window.AudioContext || window.webkitAudioContext;
|
||||
if (!ctxClass) {
|
||||
return;
|
||||
}
|
||||
|
||||
// known good sample rates
|
||||
var goodRates = [48000, 44100, 96000]
|
||||
|
||||
// let the browser chose the sample rate, if it is good, use it
|
||||
var ctx = new ctxClass({latencyHint: 'playback'});
|
||||
if (goodRates.indexOf(ctx.sampleRate) >= 0) {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// if that didn't work, try if any of the good rates work
|
||||
if (goodRates.some(function(sr) {
|
||||
try {
|
||||
ctx = new ctxClass({sampleRate: sr, latencyHint: 'playback'});
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}, this)) {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// fallback: let the browser decide
|
||||
// this may cause playback problems down the line
|
||||
return new ctxClass({latencyHint: 'playback'});
|
||||
}
|
||||
|
||||
AudioEngine.prototype.resume = function(){
|
||||
this.audioContext.resume();
|
||||
}
|
||||
@ -193,6 +225,7 @@ AudioEngine.prototype.resetStats = function() {
|
||||
};
|
||||
|
||||
AudioEngine.prototype.setupResampling = function() { //both at the server and the client
|
||||
var targetRate = this.audioContext.sampleRate;
|
||||
var audio_params = this.findRate(8000, 12000);
|
||||
if (!audio_params) {
|
||||
this.resamplingFactor = 0;
|
||||
|
@ -145,21 +145,3 @@ BookmarkBar.prototype.getDemodulatorPanel = function() {
|
||||
BookmarkBar.prototype.getDemodulator = function() {
|
||||
return this.getDemodulatorPanel().getDemodulator();
|
||||
};
|
||||
|
||||
BookmarkLocalStorage = function(){
|
||||
};
|
||||
|
||||
BookmarkLocalStorage.prototype.getBookmarks = function(){
|
||||
return JSON.parse(window.localStorage.getItem("bookmarks")) || [];
|
||||
};
|
||||
|
||||
BookmarkLocalStorage.prototype.setBookmarks = function(bookmarks){
|
||||
window.localStorage.setItem("bookmarks", JSON.stringify(bookmarks));
|
||||
};
|
||||
|
||||
BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
|
||||
if (data.id) data = data.id;
|
||||
var bookmarks = this.getBookmarks();
|
||||
bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
|
||||
this.setBookmarks(bookmarks);
|
||||
};
|
||||
|
17
htdocs/lib/BookmarkLocalStorage.js
Normal file
@ -0,0 +1,17 @@
|
||||
BookmarkLocalStorage = function(){
|
||||
};
|
||||
|
||||
BookmarkLocalStorage.prototype.getBookmarks = function(){
|
||||
return JSON.parse(window.localStorage.getItem("bookmarks")) || [];
|
||||
};
|
||||
|
||||
BookmarkLocalStorage.prototype.setBookmarks = function(bookmarks){
|
||||
window.localStorage.setItem("bookmarks", JSON.stringify(bookmarks));
|
||||
};
|
||||
|
||||
BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
|
||||
if (data.id) data = data.id;
|
||||
var bookmarks = this.getBookmarks();
|
||||
bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
|
||||
this.setBookmarks(bookmarks);
|
||||
};
|
@ -5,12 +5,14 @@ function Filter(demodulator) {
|
||||
|
||||
Filter.prototype.getLimits = function() {
|
||||
var max_bw;
|
||||
if (this.demodulator.get_secondary_demod() === 'pocsag') {
|
||||
if (['pocsag', 'packet'].indexOf(this.demodulator.get_secondary_demod()) >= 0) {
|
||||
max_bw = 12500;
|
||||
} else if (['dmr', 'dstar', 'nxdn', 'ysf', 'm17'].indexOf(this.demodulator.get_modulation()) >= 0) {
|
||||
max_bw = 6250;
|
||||
} else if (this.demodulator.get_modulation() === 'wfm') {
|
||||
max_bw = 80000;
|
||||
} else if (this.demodulator.get_modulation() === 'drm') {
|
||||
max_bw = 100000;
|
||||
} else if (this.demodulator.get_modulation() === 'drm') {
|
||||
max_bw = 50000;
|
||||
} else {
|
||||
max_bw = (audioEngine.getOutputRate() / 2) - 1;
|
||||
}
|
||||
@ -234,7 +236,7 @@ Demodulator.prototype.emit = function(event, params) {
|
||||
};
|
||||
|
||||
Demodulator.prototype.set_offset_frequency = function(to_what) {
|
||||
if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
|
||||
if (typeof(to_what) == 'undefined' || to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
|
||||
to_what = Math.round(to_what);
|
||||
if (this.offset_frequency === to_what) {
|
||||
return;
|
||||
|
@ -3,6 +3,8 @@ function DemodulatorPanel(el) {
|
||||
self.el = el;
|
||||
self.demodulator = null;
|
||||
self.mode = null;
|
||||
self.squelchMargin = 10;
|
||||
self.initialParams = {};
|
||||
|
||||
var displayEl = el.find('.webrx-actual-freq')
|
||||
this.tuneableFrequencyDisplay = displayEl.tuneableFrequencyDisplay();
|
||||
@ -10,6 +12,8 @@ function DemodulatorPanel(el) {
|
||||
self.getDemodulator().set_offset_frequency(freq - self.center_freq);
|
||||
});
|
||||
|
||||
this.mouseFrequencyDisplay = el.find('.webrx-mouse-freq').frequencyDisplay();
|
||||
|
||||
Modes.registerModePanel(this);
|
||||
el.on('click', '.openwebrx-demodulator-button', function() {
|
||||
var modulation = $(this).data('modulation');
|
||||
@ -27,9 +31,9 @@ function DemodulatorPanel(el) {
|
||||
self.setMode(value);
|
||||
}
|
||||
});
|
||||
el.on('click', '.openwebrx-squelch-default', function() {
|
||||
el.on('click', '.openwebrx-squelch-auto', function() {
|
||||
if (!self.squelchAvailable()) return;
|
||||
el.find('.openwebrx-squelch-slider').val(getLogSmeterValue(smeter_level) + 10);
|
||||
el.find('.openwebrx-squelch-slider').val(getLogSmeterValue(smeter_level) + self.getSquelchMargin());
|
||||
self.updateSquelch();
|
||||
});
|
||||
el.on('change', '.openwebrx-squelch-slider', function() {
|
||||
@ -154,17 +158,20 @@ DemodulatorPanel.prototype.updatePanels = function() {
|
||||
var modulation = this.getDemodulator().get_secondary_demod();
|
||||
$('#openwebrx-panel-digimodes').attr('data-mode', modulation);
|
||||
toggle_panel("openwebrx-panel-digimodes", !!modulation);
|
||||
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(modulation) >= 0);
|
||||
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65"].indexOf(modulation) >= 0);
|
||||
toggle_panel("openwebrx-panel-js8-message", modulation == "js8");
|
||||
toggle_panel("openwebrx-panel-packet-message", modulation === "packet");
|
||||
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");
|
||||
|
||||
modulation = this.getDemodulator().get_modulation();
|
||||
var showing = 'openwebrx-panel-metadata-' + modulation;
|
||||
$(".openwebrx-meta-panel").each(function (_, p) {
|
||||
var metaPanels = $(".openwebrx-meta-panel");
|
||||
metaPanels.each(function (_, p) {
|
||||
toggle_panel(p.id, p.id === showing);
|
||||
});
|
||||
clear_metadata();
|
||||
metaPanels.metaPanel().each(function() {
|
||||
this.clear();
|
||||
});
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.getDemodulator = function() {
|
||||
@ -177,7 +184,7 @@ DemodulatorPanel.prototype.collectParams = function() {
|
||||
squelch_level: -150,
|
||||
mod: 'nfm'
|
||||
}
|
||||
return $.extend(new Object(), defaults, this.initialParams || {}, this.transformHashParams(this.parseHash()));
|
||||
return $.extend(new Object(), defaults, this.validateInitialParams(this.initialParams), this.transformHashParams(this.parseHash()));
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.startDemodulator = function() {
|
||||
@ -203,7 +210,11 @@ DemodulatorPanel.prototype._apply = function(params) {
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.setInitialParams = function(params) {
|
||||
this.initialParams = params;
|
||||
$.extend(this.initialParams, params);
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.resetInitialParams = function() {
|
||||
this.initialParams = {};
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.onHashChange = function() {
|
||||
@ -247,7 +258,7 @@ DemodulatorPanel.prototype.updateButtons = function() {
|
||||
}
|
||||
var squelch_disabled = !this.squelchAvailable();
|
||||
this.el.find('.openwebrx-squelch-slider').prop('disabled', squelch_disabled);
|
||||
this.el.find('.openwebrx-squelch-default')[squelch_disabled ? 'addClass' : 'removeClass']('disabled');
|
||||
this.el.find('.openwebrx-squelch-auto')[squelch_disabled ? 'addClass' : 'removeClass']('disabled');
|
||||
}
|
||||
|
||||
DemodulatorPanel.prototype.setCenterFrequency = function(center_freq) {
|
||||
@ -283,7 +294,7 @@ DemodulatorPanel.prototype.validateHash = function(params) {
|
||||
var self = this;
|
||||
params = Object.keys(params).filter(function(key) {
|
||||
if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') {
|
||||
return params.freq && Math.abs(params.freq - self.center_freq) < bandwidth / 2;
|
||||
return params.freq && Math.abs(params.freq - self.center_freq) <= bandwidth / 2;
|
||||
}
|
||||
return true;
|
||||
}).reduce(function(p, key) {
|
||||
@ -299,6 +310,17 @@ DemodulatorPanel.prototype.validateHash = function(params) {
|
||||
return params;
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.validateInitialParams = function(params) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(params).filter(function(a) {
|
||||
if (a[0] == "offset_frequency") {
|
||||
return Math.abs(a[1]) <= bandwidth / 2;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.updateHash = function() {
|
||||
var demod = this.getDemodulator();
|
||||
if (!demod) return;
|
||||
@ -322,6 +344,24 @@ DemodulatorPanel.prototype.updateSquelch = function() {
|
||||
if (demod) demod.setSquelch(sliderValue);
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.setSquelchMargin = function(margin) {
|
||||
if (typeof(margin) === 'undefined' || this.squelchMargin == margin) return;
|
||||
this.squelchMargin = margin;
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.getSquelchMargin = function() {
|
||||
return this.squelchMargin;
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.setMouseFrequency = function(freq) {
|
||||
this.mouseFrequencyDisplay.setFrequency(freq);
|
||||
};
|
||||
|
||||
DemodulatorPanel.prototype.setTuningPrecision = function(precision) {
|
||||
this.tuneableFrequencyDisplay.setTuningPrecision(precision);
|
||||
this.mouseFrequencyDisplay.setTuningPrecision(precision);
|
||||
};
|
||||
|
||||
$.fn.demodulatorPanel = function(){
|
||||
if (!this.data('panel')) {
|
||||
this.data('panel', new DemodulatorPanel(this));
|
||||
|
@ -1,6 +1,14 @@
|
||||
function FrequencyDisplay(element) {
|
||||
this.suffixes = {
|
||||
'': 0,
|
||||
'k': 3,
|
||||
'M': 6,
|
||||
'G': 9,
|
||||
'T': 12
|
||||
};
|
||||
this.element = $(element);
|
||||
this.digits = [];
|
||||
this.precision = 2;
|
||||
this.setupElements();
|
||||
this.setFrequency(0);
|
||||
}
|
||||
@ -8,13 +16,31 @@ function FrequencyDisplay(element) {
|
||||
FrequencyDisplay.prototype.setupElements = function() {
|
||||
this.displayContainer = $('<div>');
|
||||
this.digitContainer = $('<span>');
|
||||
this.displayContainer.html([this.digitContainer, $('<span> MHz</span>')]);
|
||||
this.unitContainer = $('<span> Hz</span>');
|
||||
this.displayContainer.html([this.digitContainer, this.unitContainer]);
|
||||
this.element.html(this.displayContainer);
|
||||
};
|
||||
|
||||
FrequencyDisplay.prototype.getSuffix = function() {
|
||||
var me = this;
|
||||
return Object.keys(me.suffixes).filter(function(key){
|
||||
return me.suffixes[key] == me.exponent;
|
||||
})[0] || "";
|
||||
};
|
||||
|
||||
FrequencyDisplay.prototype.setFrequency = function(freq) {
|
||||
this.frequency = freq;
|
||||
var formatted = (freq / 1e6).toLocaleString(undefined, {maximumFractionDigits: 4, minimumFractionDigits: 4});
|
||||
if (this.frequency === 0 || Number.isNaN(this.frequency)) {
|
||||
this.exponent = 0
|
||||
} else {
|
||||
this.exponent = Math.floor(Math.log10(this.frequency) / 3) * 3;
|
||||
}
|
||||
|
||||
var digits = Math.max(0, this.exponent - this.precision);
|
||||
var formatted = (freq / 10 ** this.exponent).toLocaleString(
|
||||
undefined,
|
||||
{maximumFractionDigits: digits, minimumFractionDigits: digits}
|
||||
);
|
||||
var children = this.digitContainer.children();
|
||||
for (var i = 0; i < formatted.length; i++) {
|
||||
if (!this.digits[i]) {
|
||||
@ -32,6 +58,13 @@ FrequencyDisplay.prototype.setFrequency = function(freq) {
|
||||
while (this.digits.length > formatted.length) {
|
||||
this.digits.pop().remove();
|
||||
}
|
||||
this.unitContainer.text(' ' + this.getSuffix() + 'Hz');
|
||||
};
|
||||
|
||||
FrequencyDisplay.prototype.setTuningPrecision = function(precision) {
|
||||
if (typeof(precision) == 'undefined') return;
|
||||
this.precision = precision;
|
||||
this.setFrequency(this.frequency);
|
||||
};
|
||||
|
||||
function TuneableFrequencyDisplay(element) {
|
||||
@ -43,22 +76,28 @@ TuneableFrequencyDisplay.prototype = new FrequencyDisplay();
|
||||
|
||||
TuneableFrequencyDisplay.prototype.setupElements = function() {
|
||||
FrequencyDisplay.prototype.setupElements.call(this);
|
||||
this.input = $('<input>');
|
||||
this.input.hide();
|
||||
this.element.append(this.input);
|
||||
this.input = $('<input type="number" step="any">');
|
||||
this.suffixInput = $('<select tabindex="-1">');
|
||||
this.suffixInput.append($.map(this.suffixes, function(e, p) {
|
||||
return $('<option value="' + e + '">' + p + 'Hz</option>');
|
||||
}));
|
||||
this.inputGroup = $('<div class="input-group">');
|
||||
this.inputGroup.append([this.input, this.suffixInput]);
|
||||
this.inputGroup.hide();
|
||||
this.element.append(this.inputGroup);
|
||||
};
|
||||
|
||||
TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
||||
var me = this;
|
||||
|
||||
me.element.on('wheel', function(e){
|
||||
me.displayContainer.on('wheel', function(e){
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var index = me.digitContainer.find('.digit').index(e.target);
|
||||
if (index < 0) return;
|
||||
|
||||
var delta = 10 ** (Math.floor(Math.max(6, Math.log10(me.frequency))) - index);
|
||||
var delta = 10 ** (Math.floor(Math.max(me.exponent, Math.log10(me.frequency))) - index);
|
||||
if (e.originalEvent.deltaY > 0) delta *= -1;
|
||||
var newFrequency = me.frequency + delta;
|
||||
|
||||
@ -66,26 +105,64 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
||||
});
|
||||
|
||||
var submit = function(){
|
||||
var freq = parseInt(me.input.val());
|
||||
var exponent = parseInt(me.suffixInput.val());
|
||||
var freq = parseFloat(me.input.val()) * 10 ** exponent;
|
||||
if (!isNaN(freq)) {
|
||||
me.element.trigger('frequencychange', freq);
|
||||
}
|
||||
me.input.hide();
|
||||
me.inputGroup.hide();
|
||||
me.displayContainer.show();
|
||||
};
|
||||
me.input.on('blur', submit).on('keyup', function(e){
|
||||
$inputs = $.merge($(), me.input);
|
||||
$inputs = $.merge($inputs, me.suffixInput);
|
||||
$('body').on('click', function(e) {
|
||||
if (!me.input.is(':visible')) return;
|
||||
if ($.contains(me.element[0], e.target)) return;
|
||||
submit();
|
||||
});
|
||||
$inputs.on('blur', function(e){
|
||||
if ($inputs.toArray().indexOf(e.relatedTarget) >= 0) {
|
||||
return;
|
||||
}
|
||||
submit();
|
||||
});
|
||||
me.input.on('keydown', function(e){
|
||||
if (e.keyCode == 13) return submit();
|
||||
if (e.keyCode == 27) {
|
||||
me.input.hide();
|
||||
me.inputGroup.hide();
|
||||
me.displayContainer.show();
|
||||
return;
|
||||
}
|
||||
var c = String.fromCharCode(e.which);
|
||||
Object.entries(me.suffixes).forEach(function(e) {
|
||||
if (e[0].toUpperCase() == c) {
|
||||
me.suffixInput.val(e[1]);
|
||||
return submit();
|
||||
}
|
||||
})
|
||||
});
|
||||
me.input.on('click', function(e){
|
||||
var currentExponent;
|
||||
me.suffixInput.on('change', function() {
|
||||
var newExponent = me.suffixInput.val();
|
||||
delta = currentExponent - newExponent;
|
||||
if (delta >= 0) {
|
||||
me.input.val(parseFloat(me.input.val()) * 10 ** delta);
|
||||
} else {
|
||||
// should not be necessary to handle this separately, but floating point precision in javascript
|
||||
// does not handle this well otherwise
|
||||
me.input.val(parseFloat(me.input.val()) / 10 ** -delta);
|
||||
}
|
||||
currentExponent = newExponent;
|
||||
me.input.focus();
|
||||
});
|
||||
$inputs.on('click', function(e){
|
||||
e.stopPropagation();
|
||||
});
|
||||
me.element.on('click', function(){
|
||||
me.input.val(me.frequency);
|
||||
me.input.show();
|
||||
currentExponent = me.exponent;
|
||||
me.input.val(me.frequency / 10 ** me.exponent);
|
||||
me.suffixInput.val(me.exponent);
|
||||
me.inputGroup.show();
|
||||
me.displayContainer.hide();
|
||||
me.input.focus();
|
||||
});
|
||||
|
@ -1,20 +1,23 @@
|
||||
function Header(el) {
|
||||
this.el = el;
|
||||
|
||||
this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () {
|
||||
var $buttons = this.el.find('.openwebrx-main-buttons').find('[data-toggle-panel]').filter(function(){
|
||||
// ignore buttons when the corresponding panel is not in the DOM
|
||||
return $('#' + $(this).data('toggle-panel'))[0];
|
||||
});
|
||||
|
||||
$buttons.css({display: 'block'}).click(function () {
|
||||
toggle_panel($(this).data('toggle-panel'));
|
||||
});
|
||||
|
||||
this.init_rx_photo();
|
||||
this.download_details();
|
||||
};
|
||||
|
||||
Header.prototype.setDetails = function(details) {
|
||||
this.el.find('#webrx-rx-title').html(details['receiver_name']);
|
||||
var query = encodeURIComponent(details['receiver_gps']['lat'] + ',' + details['receiver_gps']['lon']);
|
||||
this.el.find('#webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m, <a href="https://www.google.com/maps/search/?api=1&query=' + query + '" target="_blank">[maps]</a>');
|
||||
this.el.find('#webrx-rx-photo-title').html(details['photo_title']);
|
||||
this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']);
|
||||
this.el.find('.webrx-rx-title').html(details['receiver_name']);
|
||||
this.el.find('.webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m');
|
||||
this.el.find('.webrx-rx-photo-title').html(details['photo_title']);
|
||||
this.el.find('.webrx-rx-photo-desc').html(details['photo_desc']);
|
||||
};
|
||||
|
||||
Header.prototype.init_rx_photo = function() {
|
||||
@ -26,25 +29,19 @@ Header.prototype.init_rx_photo = function() {
|
||||
}
|
||||
});
|
||||
|
||||
$('#webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this));
|
||||
$('.webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this));
|
||||
};
|
||||
|
||||
Header.prototype.close_rx_photo = function() {
|
||||
this.rx_photo_state = 0;
|
||||
this.el.find("#webrx-rx-photo-desc").animate({opacity: 0});
|
||||
this.el.find("#webrx-rx-photo-title").animate({opacity: 0});
|
||||
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'});
|
||||
this.el.find("#openwebrx-rx-details-arrow-down").show();
|
||||
this.el.find("#openwebrx-rx-details-arrow-up").hide();
|
||||
this.el.find('.openwebrx-description-container').removeClass('expanded');
|
||||
this.el.find(".openwebrx-rx-details-arrow").removeClass('openwebrx-rx-details-arrow--up').addClass('openwebrx-rx-details-arrow--down');
|
||||
}
|
||||
|
||||
Header.prototype.open_rx_photo = function() {
|
||||
this.rx_photo_state = 1;
|
||||
this.el.find("#webrx-rx-photo-desc").animate({opacity: 1});
|
||||
this.el.find("#webrx-rx-photo-title").animate({opacity: 1});
|
||||
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 350}, {duration: 1000, easing: 'easeOutCubic'});
|
||||
this.el.find("#openwebrx-rx-details-arrow-down").hide();
|
||||
this.el.find("#openwebrx-rx-details-arrow-up").show();
|
||||
this.el.find('.openwebrx-description-container').addClass('expanded');
|
||||
this.el.find(".openwebrx-rx-details-arrow").removeClass('openwebrx-rx-details-arrow--down').addClass('openwebrx-rx-details-arrow--up');
|
||||
}
|
||||
|
||||
Header.prototype.toggle_rx_photo = function(ev) {
|
||||
@ -58,13 +55,6 @@ Header.prototype.toggle_rx_photo = function(ev) {
|
||||
}
|
||||
};
|
||||
|
||||
Header.prototype.download_details = function() {
|
||||
var self = this;
|
||||
$.ajax('api/receiverdetails').done(function(data){
|
||||
self.setDetails(data);
|
||||
});
|
||||
};
|
||||
|
||||
$.fn.header = function() {
|
||||
if (!this.data('header')) {
|
||||
this.data('header', new Header(this));
|
||||
@ -73,5 +63,5 @@ $.fn.header = function() {
|
||||
};
|
||||
|
||||
$(function(){
|
||||
$('#webrx-top-container').header();
|
||||
$('.webrx-top-container').header();
|
||||
});
|
||||
|
@ -100,7 +100,13 @@ Js8Thread.prototype.purgeOldMessages = function() {
|
||||
return this.messages.length;
|
||||
};
|
||||
|
||||
Js8Thread.prototype.purge = function() {
|
||||
this.message = [];
|
||||
this.el.remove();
|
||||
};
|
||||
|
||||
Js8Threader = function(el){
|
||||
MessagePanel.call(this, el);
|
||||
this.threads = [];
|
||||
this.tbody = $(el).find('tbody');
|
||||
var me = this;
|
||||
@ -109,6 +115,28 @@ Js8Threader = function(el){
|
||||
}, 15000);
|
||||
};
|
||||
|
||||
Js8Threader.prototype = new MessagePanel();
|
||||
|
||||
Js8Threader.prototype.render = function() {
|
||||
$(this.el).append($(
|
||||
'<table>' +
|
||||
'<thead><tr>' +
|
||||
'<th>UTC</th>' +
|
||||
'<th class="decimal freq">Freq</th>' +
|
||||
'<th class="message">Message</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>'
|
||||
));
|
||||
};
|
||||
|
||||
Js8Threader.prototype.clearMessages = function() {
|
||||
this.threads.forEach(function(t) {
|
||||
t.purge();
|
||||
});
|
||||
this.threads = [];
|
||||
};
|
||||
|
||||
Js8Threader.prototype.purgeOldMessages = function() {
|
||||
this.threads = this.threads.filter(function(t) {
|
||||
return t.purgeOldMessages();
|
||||
|
247
htdocs/lib/MessagePanel.js
Normal file
@ -0,0 +1,247 @@
|
||||
function MessagePanel(el) {
|
||||
this.el = el;
|
||||
this.render();
|
||||
this.initClearButton();
|
||||
}
|
||||
|
||||
MessagePanel.prototype.render = function() {
|
||||
};
|
||||
|
||||
MessagePanel.prototype.pushMessage = function(message) {
|
||||
};
|
||||
|
||||
// automatic clearing is not enabled by default. call this method from the constructor to enable
|
||||
MessagePanel.prototype.initClearTimer = function() {
|
||||
var me = this;
|
||||
if (me.removalInterval) clearInterval(me.removalInterval);
|
||||
me.removalInterval = setInterval(function () {
|
||||
me.clearMessages(1000);
|
||||
}, 15000);
|
||||
};
|
||||
|
||||
MessagePanel.prototype.clearMessages = function(toRemain) {
|
||||
var $elements = $(this.el).find('tbody tr');
|
||||
// limit to 1000 entries in the list since browsers get laggy at some point
|
||||
var toRemove = $elements.length - toRemain;
|
||||
if (toRemove <= 0) return;
|
||||
$elements.slice(0, toRemove).remove();
|
||||
};
|
||||
|
||||
MessagePanel.prototype.initClearButton = function() {
|
||||
var me = this;
|
||||
me.clearButton = $(
|
||||
'<div class="openwebrx-button">Clear</div>'
|
||||
);
|
||||
me.clearButton.css({
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
right: '10px'
|
||||
});
|
||||
me.clearButton.on('click', function() {
|
||||
me.clearMessages(0);
|
||||
});
|
||||
$(me.el).append(me.clearButton);
|
||||
};
|
||||
|
||||
function WsjtMessagePanel(el) {
|
||||
MessagePanel.call(this, el);
|
||||
this.initClearTimer();
|
||||
}
|
||||
|
||||
WsjtMessagePanel.prototype = new MessagePanel();
|
||||
|
||||
WsjtMessagePanel.prototype.render = function() {
|
||||
$(this.el).append($(
|
||||
'<table>' +
|
||||
'<thead><tr>' +
|
||||
'<th>UTC</th>' +
|
||||
'<th class="decimal">dB</th>' +
|
||||
'<th class="decimal">DT</th>' +
|
||||
'<th class="decimal freq">Freq</th>' +
|
||||
'<th class="message">Message</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>'
|
||||
));
|
||||
};
|
||||
|
||||
WsjtMessagePanel.prototype.pushMessage = function(msg) {
|
||||
var $b = $(this.el).find('tbody');
|
||||
var t = new Date(msg['timestamp']);
|
||||
var pad = function (i) {
|
||||
return ('' + i).padStart(2, "0");
|
||||
};
|
||||
var linkedmsg = msg['msg'];
|
||||
var matches;
|
||||
|
||||
var html_escape = function(input) {
|
||||
return $('<div/>').text(input).html()
|
||||
};
|
||||
|
||||
if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65'].indexOf(msg['mode']) >= 0) {
|
||||
matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/);
|
||||
if (matches && matches[2] !== 'RR73') {
|
||||
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
|
||||
} else {
|
||||
linkedmsg = html_escape(linkedmsg);
|
||||
}
|
||||
} else if (['WSPR', 'FST4W'].indexOf(msg['mode']) >= 0) {
|
||||
matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/);
|
||||
if (matches) {
|
||||
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + html_escape(matches[3]);
|
||||
} else {
|
||||
linkedmsg = html_escape(linkedmsg);
|
||||
}
|
||||
}
|
||||
$b.append($(
|
||||
'<tr data-timestamp="' + msg['timestamp'] + '">' +
|
||||
'<td>' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '</td>' +
|
||||
'<td class="decimal">' + msg['db'] + '</td>' +
|
||||
'<td class="decimal">' + msg['dt'] + '</td>' +
|
||||
'<td class="decimal freq">' + msg['freq'] + '</td>' +
|
||||
'<td class="message">' + linkedmsg + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
$b.scrollTop($b[0].scrollHeight);
|
||||
}
|
||||
|
||||
$.fn.wsjtMessagePanel = function(){
|
||||
if (!this.data('panel')) {
|
||||
this.data('panel', new WsjtMessagePanel(this));
|
||||
};
|
||||
return this.data('panel');
|
||||
};
|
||||
|
||||
function PacketMessagePanel(el) {
|
||||
MessagePanel.call(this, el);
|
||||
this.initClearTimer();
|
||||
}
|
||||
|
||||
PacketMessagePanel.prototype = new MessagePanel();
|
||||
|
||||
PacketMessagePanel.prototype.render = function() {
|
||||
$(this.el).append($(
|
||||
'<table>' +
|
||||
'<thead><tr>' +
|
||||
'<th>UTC</th>' +
|
||||
'<th class="callsign">Callsign</th>' +
|
||||
'<th class="coord">Coord</th>' +
|
||||
'<th class="message">Comment</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>'
|
||||
));
|
||||
};
|
||||
|
||||
PacketMessagePanel.prototype.pushMessage = function(msg) {
|
||||
var $b = $(this.el).find('tbody');
|
||||
var pad = function (i) {
|
||||
return ('' + i).padStart(2, "0");
|
||||
};
|
||||
|
||||
if (msg.type && msg.type === 'thirdparty' && msg.data) {
|
||||
msg = msg.data;
|
||||
}
|
||||
var source = msg.source;
|
||||
if (msg.type) {
|
||||
if (msg.type === 'item') {
|
||||
source = msg.item;
|
||||
}
|
||||
if (msg.type === 'object') {
|
||||
source = msg.object;
|
||||
}
|
||||
}
|
||||
|
||||
var timestamp = '';
|
||||
if (msg.timestamp) {
|
||||
var t = new Date(msg.timestamp);
|
||||
timestamp = pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds())
|
||||
}
|
||||
|
||||
var link = '';
|
||||
var classes = [];
|
||||
var styles = {};
|
||||
var overlay = '';
|
||||
var stylesToString = function (s) {
|
||||
return $.map(s, function (value, key) {
|
||||
return key + ':' + value + ';'
|
||||
}).join('')
|
||||
};
|
||||
if (msg.symbol) {
|
||||
classes.push('aprs-symbol');
|
||||
classes.push('aprs-symboltable-' + (msg.symbol.table === '/' ? 'normal' : 'alternate'));
|
||||
styles['background-position-x'] = -(msg.symbol.index % 16) * 15 + 'px';
|
||||
styles['background-position-y'] = -Math.floor(msg.symbol.index / 16) * 15 + 'px';
|
||||
if (msg.symbol.table !== '/' && msg.symbol.table !== '\\') {
|
||||
var s = {};
|
||||
s['background-position-x'] = -(msg.symbol.tableindex % 16) * 15 + 'px';
|
||||
s['background-position-y'] = -Math.floor(msg.symbol.tableindex / 16) * 15 + 'px';
|
||||
overlay = '<div class="aprs-symbol aprs-symboltable-overlay" style="' + stylesToString(s) + '"></div>';
|
||||
}
|
||||
} else if (msg.lat && msg.lon) {
|
||||
classes.push('openwebrx-maps-pin');
|
||||
}
|
||||
var attrs = [
|
||||
'class="' + classes.join(' ') + '"',
|
||||
'style="' + stylesToString(styles) + '"'
|
||||
].join(' ');
|
||||
if (msg.lat && msg.lon) {
|
||||
link = '<a ' + attrs + ' href="map?callsign=' + encodeURIComponent(source) + '" target="openwebrx-map">' + overlay + '</a>';
|
||||
} else {
|
||||
link = '<div ' + attrs + '>' + overlay + '</div>'
|
||||
}
|
||||
|
||||
$b.append($(
|
||||
'<tr>' +
|
||||
'<td>' + timestamp + '</td>' +
|
||||
'<td class="callsign">' + source + '</td>' +
|
||||
'<td class="coord">' + link + '</td>' +
|
||||
'<td class="message">' + (msg.comment || msg.message || '') + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
$b.scrollTop($b[0].scrollHeight);
|
||||
};
|
||||
|
||||
$.fn.packetMessagePanel = function() {
|
||||
if (!this.data('panel')) {
|
||||
this.data('panel', new PacketMessagePanel(this));
|
||||
};
|
||||
return this.data('panel');
|
||||
};
|
||||
|
||||
PocsagMessagePanel = function(el) {
|
||||
MessagePanel.call(this, el);
|
||||
this.initClearTimer();
|
||||
}
|
||||
|
||||
PocsagMessagePanel.prototype = new MessagePanel();
|
||||
|
||||
PocsagMessagePanel.prototype.render = function() {
|
||||
$(this.el).append($(
|
||||
'<table>' +
|
||||
'<thead><tr>' +
|
||||
'<th class="address">Address</th>' +
|
||||
'<th class="message">Message</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>'
|
||||
));
|
||||
};
|
||||
|
||||
PocsagMessagePanel.prototype.pushMessage = function(msg) {
|
||||
var $b = $(this.el).find('tbody');
|
||||
$b.append($(
|
||||
'<tr>' +
|
||||
'<td class="address">' + msg.address + '</td>' +
|
||||
'<td class="message">' + msg.message + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
$b.scrollTop($b[0].scrollHeight);
|
||||
};
|
||||
|
||||
$.fn.pocsagMessagePanel = function() {
|
||||
if (!this.data('panel')) {
|
||||
this.data('panel', new PocsagMessagePanel(this));
|
||||
};
|
||||
return this.data('panel');
|
||||
};
|
180
htdocs/lib/MetaPanel.js
Normal file
@ -0,0 +1,180 @@
|
||||
function MetaPanel(el) {
|
||||
this.el = el;
|
||||
this.modes = [];
|
||||
}
|
||||
|
||||
MetaPanel.prototype.update = function(data) {
|
||||
};
|
||||
|
||||
MetaPanel.prototype.isSupported = function(data) {
|
||||
return this.modes.includes(data.protocol);
|
||||
};
|
||||
|
||||
MetaPanel.prototype.clear = function() {
|
||||
this.el.find(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
|
||||
};
|
||||
|
||||
function DmrMetaSlot(el) {
|
||||
this.el = $(el);
|
||||
this.clear();
|
||||
}
|
||||
|
||||
DmrMetaSlot.prototype.update = function(data) {
|
||||
this.el[data['sync'] ? "addClass" : "removeClass"]("sync");
|
||||
if (data['sync'] && data['sync'] === "voice") {
|
||||
this.setId(data['additional'] && data['additional']['callsign'] || data['source']);
|
||||
this.setName(data['additional'] && data['additional']['fname']);
|
||||
this.setMode(['group', 'direct'].includes(data['type']) ? data['type'] : undefined);
|
||||
this.setTarget(data['target']);
|
||||
this.el.addClass("active");
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
};
|
||||
|
||||
DmrMetaSlot.prototype.setId = function(id) {
|
||||
if (this.id === id) return;
|
||||
this.id = id;
|
||||
this.el.find('.openwebrx-dmr-id').text(id || '');
|
||||
}
|
||||
|
||||
DmrMetaSlot.prototype.setName = function(name) {
|
||||
if (this.name === name) return;
|
||||
this.name = name;
|
||||
this.el.find('.openwebrx-dmr-name').text(name || '');
|
||||
};
|
||||
|
||||
DmrMetaSlot.prototype.setMode = function(mode) {
|
||||
if (this.mode === mode) return;
|
||||
this.mode = mode;
|
||||
var classes = ['group', 'direct'].filter(function(c){
|
||||
return c !== mode;
|
||||
});
|
||||
this.el.removeClass(classes.join(' ')).addClass(mode);
|
||||
}
|
||||
|
||||
DmrMetaSlot.prototype.setTarget = function(target) {
|
||||
if (this.target === target) return;
|
||||
this.target = target;
|
||||
this.el.find('.openwebrx-dmr-target').text(target || '');
|
||||
}
|
||||
|
||||
DmrMetaSlot.prototype.clear = function() {
|
||||
this.setId();
|
||||
this.setName();
|
||||
this.setMode();
|
||||
this.setTarget();
|
||||
this.el.removeClass("active");
|
||||
};
|
||||
|
||||
function DmrMetaPanel(el) {
|
||||
MetaPanel.call(this, el);
|
||||
this.modes = ['DMR'];
|
||||
this.slots = this.el.find('.openwebrx-meta-slot').toArray().map(function(el){
|
||||
return new DmrMetaSlot(el);
|
||||
});
|
||||
}
|
||||
|
||||
DmrMetaPanel.prototype = new MetaPanel();
|
||||
|
||||
DmrMetaPanel.prototype.update = function(data) {
|
||||
if (!this.isSupported(data)) return;
|
||||
if (data['slot']) {
|
||||
var slot = this.slots[data['slot']];
|
||||
slot.update(data);
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
DmrMetaPanel.prototype.clear = function() {
|
||||
MetaPanel.prototype.clear.call(this);
|
||||
this.el.find(".openwebrx-dmr-timeslot-panel").removeClass("muted");
|
||||
this.slots.forEach(function(slot) {
|
||||
slot.clear();
|
||||
});
|
||||
};
|
||||
|
||||
function YsfMetaPanel(el) {
|
||||
MetaPanel.call(this, el);
|
||||
this.modes = ['YSF'];
|
||||
this.clear();
|
||||
}
|
||||
|
||||
YsfMetaPanel.prototype = new MetaPanel();
|
||||
|
||||
YsfMetaPanel.prototype.update = function(data) {
|
||||
if (!this.isSupported(data)) return;
|
||||
this.setMode(data['mode']);
|
||||
|
||||
if (data['mode'] && data['mode'] !== "") {
|
||||
this.setSource(data['source']);
|
||||
this.setLocation(data['lat'], data['lon'], data['source']);
|
||||
this.setUp(data['up']);
|
||||
this.setDown(data['down']);
|
||||
this.el.find(".openwebrx-meta-slot").addClass("active");
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
};
|
||||
|
||||
YsfMetaPanel.prototype.clear = function() {
|
||||
MetaPanel.prototype.clear.call(this);
|
||||
this.setMode();
|
||||
this.setSource();
|
||||
this.setLocation();
|
||||
this.setUp();
|
||||
this.setDown();
|
||||
};
|
||||
|
||||
YsfMetaPanel.prototype.setMode = function(mode) {
|
||||
if (this.mode === mode) return;
|
||||
this.mode = mode;
|
||||
this.el.find('.openwebrx-ysf-mode').text(mode || '');
|
||||
};
|
||||
|
||||
YsfMetaPanel.prototype.setSource = function(source) {
|
||||
if (this.source === source) return;
|
||||
this.source = source;
|
||||
this.el.find('.openwebrx-ysf-source .callsign').text(source || '');
|
||||
};
|
||||
|
||||
YsfMetaPanel.prototype.setLocation = function(lat, lon, callsign) {
|
||||
var hasLocation = lat && lon && callsign && callsign != '';
|
||||
if (hasLocation === this.hasLocation && this.callsign === callsign) return;
|
||||
this.hasLocation = hasLocation; this.callsign = callsign;
|
||||
var html = '';
|
||||
if (hasLocation) {
|
||||
html = '<a class="openwebrx-maps-pin" href="map?callsign=' + encodeURIComponent(callsign) + '" target="_blank"></a>';
|
||||
}
|
||||
this.el.find('.openwebrx-ysf-source .location').html(html);
|
||||
};
|
||||
|
||||
YsfMetaPanel.prototype.setUp = function(up) {
|
||||
if (this.up === up) return;
|
||||
this.up = up;
|
||||
this.el.find('.openwebrx-ysf-up').text(up || '');
|
||||
};
|
||||
|
||||
YsfMetaPanel.prototype.setDown = function(down) {
|
||||
if (this.down === down) return;
|
||||
this.down = down;
|
||||
this.el.find('.openwebrx-ysf-down').text(down || '');
|
||||
}
|
||||
|
||||
MetaPanel.types = {
|
||||
dmr: DmrMetaPanel,
|
||||
ysf: YsfMetaPanel
|
||||
};
|
||||
|
||||
$.fn.metaPanel = function() {
|
||||
return this.map(function() {
|
||||
var $self = $(this);
|
||||
if (!$self.data('metapanel')) {
|
||||
var matches = /^openwebrx-panel-metadata-([a-z]+)$/.exec($self.prop('id'));
|
||||
var constructor = matches && MetaPanel.types[matches[1]] || MetaPanel;
|
||||
$self.data('metapanel', new constructor($self));
|
||||
}
|
||||
return $self.data('metapanel');
|
||||
});
|
||||
};
|
@ -3,7 +3,6 @@ ProgressBar = function(el) {
|
||||
this.$innerText = $('<span class="openwebrx-progressbar-text">' + this.getDefaultText() + '</span>');
|
||||
this.$innerBar = $('<div class="openwebrx-progressbar-bar"></div>');
|
||||
this.$el.empty().append(this.$innerText, this.$innerBar);
|
||||
this.$innerBar.css('width', '0%');
|
||||
};
|
||||
|
||||
ProgressBar.prototype.getDefaultText = function() {
|
||||
@ -19,7 +18,7 @@ ProgressBar.prototype.set = function(val, text, over) {
|
||||
ProgressBar.prototype.setValue = function(val) {
|
||||
if (val < 0) val = 0;
|
||||
if (val > 1) val = 1;
|
||||
this.$innerBar.stop().animate({width: val * 100 + '%'}, 700);
|
||||
this.$innerBar.css({transform: 'translate(' + ((val - 1) * 100) + '%) translateZ(0)'});
|
||||
};
|
||||
|
||||
ProgressBar.prototype.setText = function(text) {
|
||||
@ -27,7 +26,7 @@ ProgressBar.prototype.setText = function(text) {
|
||||
};
|
||||
|
||||
ProgressBar.prototype.setOver = function(over) {
|
||||
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
|
||||
this.$el[over ? 'addClass' : 'removeClass']('openwebrx-progressbar--over');
|
||||
};
|
||||
|
||||
AudioBufferProgressBar = function(el) {
|
||||
|
6
htdocs/lib/bootstrap.bundle.min.js
vendored
Normal file
3
htdocs/lib/jquery.nanoscroller.min.js
vendored
Normal file
2
htdocs/lib/location-picker.min.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/* Taken from https://github.com/cyphercodes/location-picker under GPLv3 license */
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.locationPicker=t()}(this,function(){"use strict";return function(e,t){void 0===t&&(t={});var n=t.insertAt;if(e&&"undefined"!=typeof document){var o=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style");i.type="text/css","top"===n&&o.firstChild?o.insertBefore(i,o.firstChild):o.appendChild(i),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(document.createTextNode(e))}}('.location-picker .centerMarker{position:absolute;background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAABWCAYAAACEsWWHAAAGLElEQVR4AdXcA2xsXReH8f3Ztm3btm2br23btm3btm2rd257b835vVnJStL0sj3Tds6TrOTkzN5r/5925qgorQRPwQewDHbHebgXDQxlNXLfeTlmmZzzlNJOpMyXsA86TJ2O7PGl6DnbQr/CncYx1tNj4JzTde+0la5V/6/x+x+Z85Ov6/jWZ0TFduyL12JMjI05E4iev5pxQXwFV0lGOx7Xf/xRulb5n45vflrH1z4xqYo5MTd6RC9JrvGVmRB6C04dLzR/yw10fP2TGbJyRa/oOVHw1Fh7uqS+jAY0e3uaPXvupOM7n48w01LRO9aItSDX/nKrpf6FYRi85AJzfvy1WHxGKtaKNZPhyNIKoadjZ0nfIfvFYrNSsXYiMz2tilg00Bweas7fZO1YYFYrMkQWKVfl7ScadS7z12jcFhVZxsn9ayoHimHI71RbVWRKhiPrZA7pjZn+TFX4zDWW6lSAU/PoFw3auiJjcuqSryjyPJWH9LauyBhZEXxlcdd+VyFOjDGxFhVZk6vCoUwEv5KXSXlFUYuKrJE5+dXCvlt3Ia7TYkKtKjIndxr/XcOXIMzzgrZWFZkje/Kl8WL7QP/xR7Z0wWs/9X5HvutNdnjdq2z6qpdFxXbsi9daulZkT/YZ/zbsQNwTtWSRuz7/YXu9+bU2ePlLFlcxJsa2ZM3InnSEU8EH5J1v3iRWqhs+9QGbv+plS5TKirExp/K6kT0ckg8ULIO4RW/Jd2q81GTkWvGdC4dkmYLdoXvHLSs3zrffVCrmVl4/HJLdC86r/vmKA8X7ImCVqnxACYfkvIL7EE+OKjWNI11FsehRKUM4JPcVNBCPxSo1jcN4RbHoUSlDOCSNgiHEM79KTeMcVVEselTKEA7JUMEA4pqr9mLhkAwUPAqNX36n9m/Fxi+/K3mk4GaY+/df1/7gEQ7JzQUnwPwN16h8XTjbh/twSE4oWBf6jjio9ifocEjWLfgeDN14be0vqcIh+V7BCzFobKw556ffqO1FcGQPh3AJpxLgROjebrPa3rZE9uTEEgT4IwzffENtbzQje/LH8WLPRQPycXatKjInjXAp48GGMHjx+bUTi8zJhmUieAX60ez8359rIxVZ0czsrygLA9uM+6zVoiJrsnVZFHg55sP8jddqe6nImMyP7GVxYA0Yffyxtn4iHNkiY7J6WRJ4Fu6G3n13a1uxyJbcHZnL0oDvQHNwsJmPDNqqIlNkg8haJgOOgKGrL287sciUHFEmC16FTpi/2bptIxVZks7IWKYC/grN+fOa+bBnVisyRBaIbKUKOBsGzjp11sUiQ3J2qQreij7oWn3ZWZOKtZO+yFRaAVaAsSceb3Z874szLhVrxtoQWUqrwFNxeeWfoVX/2dflkaW0ErwPQ2h2Lvf3GZOKtdDMtd9XpgOsByMP3q/j25+bdqlYI9ZK1ivTBZ6JW6D3kH2nXSzWSG6Jtct0gs+iaWSkOfcvv5g2qehtdLSJqM+WmQC7wND110ybWPROdikzBV6Ih2H+5uu1XCp6Jg/HWmUmwa+gOa+rOedHX22ZVPSKngh+VWYDnAP9Jx3TMrHolZxTZgu8B8PxBHbun39eWSp6RC8MR+8ym2AbGLzo3Mpi0SPZpsw2eCm6oPP/f5myVMxNuqJnaQewKgzdMPXDf8xNVi3tAp6Nh2DemstPWirmJNHj2aWdwDIwfNstkxaLOckypd3Ac/AEdP73T0stFWOTJ6JHaUewBgycecpSi8XYZI3SruBlGGwODy/VT0djTIzFYMwt7QwOhZ69d16iWIxJDi3tDr5CPh/5xqcWKRWvxRgEX6mDWP5GOF0r/muRYl0r/ltyV8wpdQCbQ98xhy1SLF5LNi91AV+A0ccfXaRYvJZ8oU5iT0cXNP7wExOlYl/SFWNLncARMH+L9U0Ui33JEaVuYGXoP/lYE8ViX7JyHcW+uahrx9iXfLOOYq+Bse75JorFvuQ1pY6gE8b/kV1sJ52lruCuib+9GtvJXXUWu2ri72fFdnJVncXOhvgXFykmtpOz6yx2MMzfdB0pJraTg+sstiwM33mbxm9/qPG7H4ntZNk6iz0Hd5hA7ntOqTN4OfbDo1mx/fIyzTwJJedUPgRWtocAAAAASUVORK5CYII=") no-repeat;background-size:100%;top:50%;left:50%;z-index:1;margin-left:-14px;margin-top:-43px;height:44px;width:28px;cursor:pointer}'),function(){function e(e,t,n){void 0===t&&(t={}),void 0===n&&(n={});var o={setCurrentPosition:!0};Object.assign(o,t);var i={center:new google.maps.LatLng(o.lat?o.lat:34.4346,o.lng?o.lng:35.8362),zoom:15};Object.assign(i,n),e instanceof HTMLElement?this.element=e:this.element=document.getElementById(e),this.map=new google.maps.Map(this.element,i);var r=document.createElement("div");r.classList.add("centerMarker"),this.element&&(this.element.classList.add("location-picker"),this.element.children[0].appendChild(r)),!o.setCurrentPosition||o.lat||o.lng||this.setCurrentPosition()}return e.prototype.getMarkerPosition=function(){var e=this.map.getCenter();return{lat:e.lat(),lng:e.lng()}},e.prototype.setLocation=function(e,t){this.map.setCenter(new google.maps.LatLng(e,t))},e.prototype.setCurrentPosition=function(){var e=this;navigator.geolocation?navigator.geolocation.getCurrentPosition(function(t){var n={lat:t.coords.latitude,lng:t.coords.longitude};e.map.setCenter(n)},function(){console.log("Could not determine your location...")}):console.log("Your browser does not support Geolocation.")},e}()});
|
@ -30,7 +30,8 @@ var nite = {
|
||||
fillOpacity: 0.1,
|
||||
strokeOpacity: 0,
|
||||
clickable: false,
|
||||
editable: false
|
||||
editable: false,
|
||||
zIndex: 1
|
||||
});
|
||||
this.marker_twilight_nautical = new google.maps.Circle({
|
||||
map: this.map,
|
||||
@ -40,7 +41,8 @@ var nite = {
|
||||
fillOpacity: 0.1,
|
||||
strokeOpacity: 0,
|
||||
clickable: false,
|
||||
editable: false
|
||||
editable: false,
|
||||
zIndex: 1
|
||||
});
|
||||
this.marker_twilight_astronomical = new google.maps.Circle({
|
||||
map: this.map,
|
||||
@ -50,7 +52,8 @@ var nite = {
|
||||
fillOpacity: 0.1,
|
||||
strokeOpacity: 0,
|
||||
clickable: false,
|
||||
editable: false
|
||||
editable: false,
|
||||
zIndex: 1
|
||||
});
|
||||
this.marker_night = new google.maps.Circle({
|
||||
map: this.map,
|
||||
@ -60,7 +63,8 @@ var nite = {
|
||||
fillOpacity: 0.1,
|
||||
strokeOpacity: 0,
|
||||
clickable: false,
|
||||
editable: false
|
||||
editable: false,
|
||||
zIndex: 1
|
||||
});
|
||||
},
|
||||
getShadowRadiusFromAngle: function(angle) {
|
||||
|
402
htdocs/lib/settings/BookmarkTable.js
Normal file
@ -0,0 +1,402 @@
|
||||
function Editor(table) {
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
Editor.prototype.getInputHtml = function() {
|
||||
return '<input>';
|
||||
}
|
||||
|
||||
Editor.prototype.render = function(el) {
|
||||
this.input = $(this.getInputHtml());
|
||||
el.append(this.input);
|
||||
this.setupEvents();
|
||||
};
|
||||
|
||||
Editor.prototype.setupEvents = function() {
|
||||
var me = this;
|
||||
this.input.on('blur', function() { me.submit(); }).on('change', function() { me.submit(); }).on('keydown', function(e){
|
||||
if (e.keyCode == 13) return me.submit();
|
||||
if (e.keyCode == 27) return me.cancel();
|
||||
});
|
||||
};
|
||||
|
||||
Editor.prototype.submit = function() {
|
||||
if (!this.onSubmit) return;
|
||||
var submit = this.onSubmit;
|
||||
delete this.onSubmit;
|
||||
submit();
|
||||
};
|
||||
|
||||
Editor.prototype.cancel = function() {
|
||||
if (!this.onCancel) return;
|
||||
var cancel = this.onCancel;
|
||||
delete this.onCancel;
|
||||
cancel();
|
||||
};
|
||||
|
||||
Editor.prototype.focus = function() {
|
||||
this.input.focus();
|
||||
};
|
||||
|
||||
Editor.prototype.disable = function(flag) {
|
||||
this.input.prop('disabled', flag);
|
||||
};
|
||||
|
||||
Editor.prototype.setValue = function(value) {
|
||||
this.input.val(value);
|
||||
};
|
||||
|
||||
Editor.prototype.getValue = function() {
|
||||
return this.input.val();
|
||||
};
|
||||
|
||||
Editor.prototype.getHtml = function() {
|
||||
return this.getValue();
|
||||
};
|
||||
|
||||
function NameEditor(table) {
|
||||
Editor.call(this, table);
|
||||
}
|
||||
|
||||
NameEditor.prototype = new Editor();
|
||||
|
||||
NameEditor.prototype.getInputHtml = function() {
|
||||
return '<input class="form-control form-control-sm" type="text">';
|
||||
}
|
||||
|
||||
function FrequencyEditor(table) {
|
||||
Editor.call(this, table);
|
||||
}
|
||||
|
||||
FrequencyEditor.suffixes = {
|
||||
'': 0,
|
||||
'K': 3,
|
||||
'M': 6,
|
||||
'G': 9,
|
||||
'T': 12
|
||||
};
|
||||
|
||||
FrequencyEditor.prototype = new Editor();
|
||||
|
||||
FrequencyEditor.prototype.getInputHtml = function() {
|
||||
return '<div class="input-group input-group-sm exponential-input" name="frequency">' +
|
||||
'<input class="form-control form-control-sm" type="number" step="1">' +
|
||||
'<div class="input-group-append">' +
|
||||
'<select class="input-group-text exponent" tabindex="-1">' +
|
||||
$.map(FrequencyEditor.suffixes, function(v, k) {
|
||||
// fix lowercase "kHz"
|
||||
if (k === "K") k = "k";
|
||||
return '<option value="' + v + '">' + k + 'Hz</option>';
|
||||
}).join('') +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
};
|
||||
|
||||
FrequencyEditor.prototype.render = function(el) {
|
||||
this.input = $(this.getInputHtml());
|
||||
el.append(this.input);
|
||||
this.freqInput = el.find('input');
|
||||
this.expInput = el.find('select');
|
||||
this.setupEvents();
|
||||
};
|
||||
|
||||
FrequencyEditor.prototype.setupEvents = function() {
|
||||
var me = this;
|
||||
var inputs = [this.freqInput, this.expInput].map(function(i) { return i[0]; });
|
||||
inputs.forEach(function(input) {
|
||||
$(input).on('blur', function(e){
|
||||
if (inputs.indexOf(e.relatedTarget) >= 0) {
|
||||
return;
|
||||
}
|
||||
me.submit();
|
||||
});
|
||||
});
|
||||
|
||||
var me = this;
|
||||
this.freqInput.on('keydown', function(e){
|
||||
if (e.keyCode == 13) return me.submit();
|
||||
if (e.keyCode == 27) return me.cancel();
|
||||
var c = String.fromCharCode(e.which);
|
||||
if (c in FrequencyEditor.suffixes) {
|
||||
me.expInput.val(FrequencyEditor.suffixes[c]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FrequencyEditor.prototype.getValue = function() {
|
||||
var frequency = parseFloat(this.freqInput.val());
|
||||
var exp = parseInt(this.expInput.val());
|
||||
return Math.floor(frequency * 10 ** exp);
|
||||
};
|
||||
|
||||
FrequencyEditor.prototype.setValue = function(value) {
|
||||
var value = parseFloat(value);
|
||||
var exp = 0;
|
||||
if (!Number.isNaN(value) && value > 0) {
|
||||
exp = Math.floor(Math.log10(value) / 3) * 3;
|
||||
}
|
||||
this.freqInput.val(value / 10 ** exp);
|
||||
this.expInput.val(exp);
|
||||
};
|
||||
|
||||
FrequencyEditor.prototype.focus = function() {
|
||||
this.freqInput.focus();
|
||||
};
|
||||
|
||||
var renderFrequency = function(freq) {
|
||||
var exp = 0;
|
||||
if (!Number.isNaN(freq)) {
|
||||
exp = Math.floor(Math.log10(freq) / 3) * 3;
|
||||
}
|
||||
var frequency = freq / 10 ** exp;
|
||||
var suffix = Object.entries(FrequencyEditor.suffixes).find(function(e) {
|
||||
return e[1] == exp;
|
||||
});
|
||||
if (!suffix) {
|
||||
return freq + ' Hz';
|
||||
}
|
||||
// fix lowercase 'kHz'
|
||||
suffix = suffix[0] == 'K' ? 'k' : suffix[0];
|
||||
var expString = suffix[0] + 'Hz';
|
||||
return frequency + ' ' + expString;
|
||||
}
|
||||
|
||||
FrequencyEditor.prototype.getHtml = function() {
|
||||
return renderFrequency(this.getValue());
|
||||
};
|
||||
|
||||
function ModulationEditor(table) {
|
||||
Editor.call(this, table);
|
||||
this.modes = table.data('modes');
|
||||
}
|
||||
|
||||
ModulationEditor.prototype = new Editor();
|
||||
|
||||
ModulationEditor.prototype.getInputHtml = function() {
|
||||
return '<select class="form-control form-control-sm">' +
|
||||
$.map(this.modes, function(name, modulation) {
|
||||
return '<option value="' + modulation + '">' + name + '</option>';
|
||||
}).join('') +
|
||||
'</select>';
|
||||
};
|
||||
|
||||
ModulationEditor.prototype.getHtml = function() {
|
||||
var $option = this.input.find('option:selected')
|
||||
return $option.html();
|
||||
};
|
||||
|
||||
$.fn.bookmarktable = function() {
|
||||
var editors = {
|
||||
name: NameEditor,
|
||||
frequency: FrequencyEditor,
|
||||
modulation: ModulationEditor
|
||||
};
|
||||
|
||||
$.each(this, function(){
|
||||
var $table = $(this).find('table');
|
||||
|
||||
$table.on('dblclick', 'td', function(e) {
|
||||
var $cell = $(e.target);
|
||||
var html = $cell.html();
|
||||
|
||||
var $row = $cell.parents('tr');
|
||||
var name = $cell.data('editor');
|
||||
var EditorCls = editors[name];
|
||||
if (!EditorCls) return;
|
||||
|
||||
var editor = new EditorCls($table);
|
||||
editor.render($cell.html(''));
|
||||
editor.setValue($cell.data('value'));
|
||||
editor.focus();
|
||||
|
||||
editor.onSubmit = function() {
|
||||
editor.disable(true);
|
||||
$.ajax(document.location.href + "/" + $row.data('id'), {
|
||||
data: JSON.stringify(Object.fromEntries([[name, editor.getValue()]])),
|
||||
contentType: 'application/json',
|
||||
method: 'POST'
|
||||
}).done(function(){
|
||||
$cell.data('value', editor.getValue());
|
||||
$cell.html(editor.getHtml());
|
||||
});
|
||||
};
|
||||
|
||||
editor.onCancel = function() {
|
||||
$cell.html(html);
|
||||
};
|
||||
});
|
||||
|
||||
var $modal = $('#deleteModal').modal({show:false});
|
||||
|
||||
$modal.on('hidden.bs.modal', function() {
|
||||
var $row = $modal.data('row');
|
||||
if (!$row) return;
|
||||
$row.find('.bookmark-delete').prop('disabled', false);
|
||||
$modal.removeData('row');
|
||||
});
|
||||
|
||||
$modal.on('click', '.confirm', function() {
|
||||
var $row = $modal.data('row');
|
||||
if (!$row) return;
|
||||
$.ajax(document.location.href + "/" + $row.data('id'), {
|
||||
data: "{}",
|
||||
contentType: 'application/json',
|
||||
method: 'DELETE'
|
||||
}).done(function(){
|
||||
$row.remove();
|
||||
$modal.modal('hide');
|
||||
});
|
||||
});
|
||||
|
||||
$table.on('click', '.bookmark-delete', function(e) {
|
||||
var $button = $(e.target);
|
||||
$button.prop('disabled', true);
|
||||
|
||||
var $row = $button.parents('tr');
|
||||
$modal.data('row', $row);
|
||||
$modal.modal('show');
|
||||
});
|
||||
|
||||
$(this).on('click', '.bookmark-add', function() {
|
||||
if ($table.find('tr[data-id="new"]').length) return;
|
||||
|
||||
$table.find('.emptytext').remove();
|
||||
var row = $('<tr data-id="new">');
|
||||
|
||||
var inputs = Object.fromEntries(
|
||||
Object.entries(editors).map(function(e) {
|
||||
return [e[0], new e[1]($table)];
|
||||
})
|
||||
);
|
||||
|
||||
row.append($.map(inputs, function(editor, name){
|
||||
var cell = $('<td data-editor="' + name + '" class="' + name + '">');
|
||||
editor.render(cell);
|
||||
return cell;
|
||||
}));
|
||||
row.append($(
|
||||
'<td>' +
|
||||
'<div class="btn-group btn-group-sm">' +
|
||||
'<button type="button" class="btn btn-primary bookmark-save">Save</button>' +
|
||||
'<button type="button" class="btn btn-secondary bookmark-cancel">Cancel</button>' +
|
||||
'</div>' +
|
||||
'</td>'
|
||||
));
|
||||
|
||||
row.on('click', '.bookmark-cancel', function() {
|
||||
row.remove();
|
||||
});
|
||||
|
||||
row.on('click', '.bookmark-save', function() {
|
||||
var data = Object.fromEntries(
|
||||
$.map(inputs, function(input, name){
|
||||
input.disable(true);
|
||||
// double wrapped because jQuery.map() flattens the result
|
||||
return [[name, input.getValue()]];
|
||||
})
|
||||
);
|
||||
|
||||
$.ajax(document.location.href, {
|
||||
data: JSON.stringify([data]),
|
||||
contentType: 'application/json',
|
||||
method: 'POST'
|
||||
}).done(function(data){
|
||||
if (data.length && data.length === 1 && 'bookmark_id' in data[0]) {
|
||||
row.attr('data-id', data[0]['bookmark_id']);
|
||||
var tds = row.find('td');
|
||||
|
||||
Object.values(inputs).forEach(function(input, index) {
|
||||
var td = $(tds[index]);
|
||||
td.data('value', input.getValue());
|
||||
td.html(input.getHtml());
|
||||
});
|
||||
|
||||
var $cell = row.find('td').last();
|
||||
var $group = $cell.find('.btn-group');
|
||||
if ($group.length) {
|
||||
$group.remove;
|
||||
$cell.html('<div class="btn btn-sm btn-danger bookmark-delete">delete</div>');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$table.append(row);
|
||||
row[0].scrollIntoView();
|
||||
});
|
||||
|
||||
var $importModal = $('#importModal').modal({show: false});
|
||||
|
||||
$(this).find('.bookmark-import').on('click', function() {
|
||||
var storage = new BookmarkLocalStorage();
|
||||
var bookmarks = storage.getBookmarks();
|
||||
if (bookmarks.length) {
|
||||
var modes = $table.data('modes');
|
||||
var $list = $('<table class="table table-sm">');
|
||||
$list.append(bookmarks.map(function(b) {
|
||||
var modulation = b.modulation;
|
||||
if (modulation in modes) {
|
||||
modulation = modes[modulation];
|
||||
}
|
||||
var row = $(
|
||||
'<tr>' +
|
||||
'<td><input class="form-check-input select" type="checkbox"></td>' +
|
||||
'<td>' + b.name + '</td>' +
|
||||
'<td class="frequency">' + renderFrequency(b.frequency) + '</td>' +
|
||||
'<td>' + modulation + '</td>' +
|
||||
'</tr>'
|
||||
);
|
||||
row.data('bookmark', b);
|
||||
return row;
|
||||
}));
|
||||
$importModal.find('.bookmark-list').html($list);
|
||||
} else {
|
||||
$importModal.find('.bookmark-list').html('No personal bookmarks found in this browser');
|
||||
}
|
||||
$importModal.modal('show');
|
||||
});
|
||||
|
||||
$importModal.on('click', '.confirm', function() {
|
||||
var $list = $importModal.find('.bookmark-list table');
|
||||
if ($list.length) {
|
||||
var selected = $list.find('tr').filter(function(){
|
||||
return $(this).find('.select').is(':checked');
|
||||
}).map(function(){
|
||||
return $(this).data('bookmark');
|
||||
}).toArray();
|
||||
if (selected.length) {
|
||||
$.ajax(document.location.href, {
|
||||
data: JSON.stringify(selected),
|
||||
contentType: 'application/json',
|
||||
method: 'POST'
|
||||
}).done(function(data){
|
||||
$table.find('.emptytext').remove();
|
||||
var modes = $table.data('modes');
|
||||
if (data.length && data.length == selected.length) {
|
||||
$table.append(data.map(function(obj, index) {
|
||||
var bookmark = selected[index];
|
||||
var modulation_name = bookmark.modulation;
|
||||
if (modulation_name in modes) {
|
||||
modulation_name = modes[modulation_name];
|
||||
}
|
||||
return $(
|
||||
'<tr data-id="' + obj.bookmark_id + '">' +
|
||||
'<td data-editor="name" data-value="' + bookmark.name + '">' + bookmark.name + '</td>' +
|
||||
'<td data-editor="frequency" data-value="' + bookmark.frequency + '" class="frequency">' + renderFrequency(bookmark.frequency) +'</td>' +
|
||||
'<td data-editor="modulation" data-value="' + bookmark.modulation + '">' + modulation_name + '</td>' +
|
||||
'<td>' +
|
||||
'<button type="button" class="btn btn-sm btn-danger bookmark-delete">delete</button>' +
|
||||
'</td>' +
|
||||
'</tr>'
|
||||
)
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
$importModal.modal('hide');
|
||||
});
|
||||
});
|
||||
};
|
45
htdocs/lib/settings/ExponentialInput.js
Normal file
@ -0,0 +1,45 @@
|
||||
$.fn.exponentialInput = function() {
|
||||
var prefixes = {
|
||||
'K': 3,
|
||||
'M': 6,
|
||||
'G': 9,
|
||||
'T': 12
|
||||
};
|
||||
|
||||
this.each(function(){
|
||||
var $group = $(this);
|
||||
var currentExponent = 0;
|
||||
var $input = $group.find('input');
|
||||
|
||||
var setExponent = function() {
|
||||
var newExponent = parseInt($exponent.val());
|
||||
var delta = currentExponent - newExponent;
|
||||
if (delta >= 0) {
|
||||
$input.val(parseFloat($input.val()) * 10 ** delta);
|
||||
} else {
|
||||
// should not be necessary to handle this separately, but floating point precision in javascript
|
||||
// does not handle this well otherwise
|
||||
$input.val(parseFloat($input.val()) / 10 ** -delta);
|
||||
}
|
||||
currentExponent = newExponent;
|
||||
};
|
||||
|
||||
$input.on('keydown', function(e) {
|
||||
var c = String.fromCharCode(e.which);
|
||||
if (c in prefixes) {
|
||||
currentExponent = prefixes[c];
|
||||
$exponent.val(prefixes[c]);
|
||||
}
|
||||
});
|
||||
|
||||
var $exponent = $group.find('select.exponent');
|
||||
$exponent.on('change', setExponent);
|
||||
|
||||
// calculate initial exponent
|
||||
var value = parseFloat($input.val());
|
||||
if (!Number.isNaN(value)) {
|
||||
$exponent.val(Math.floor(Math.log10(value) / 3) * 3);
|
||||
setExponent();
|
||||
}
|
||||
})
|
||||
};
|
17
htdocs/lib/settings/GainInput.js
Normal file
@ -0,0 +1,17 @@
|
||||
$.fn.gainInput = function() {
|
||||
this.each(function() {
|
||||
var $container = $(this);
|
||||
|
||||
var update = function(value){
|
||||
$container.find('.option').hide();
|
||||
$container.find('.option.' + value).show();
|
||||
}
|
||||
|
||||
var $select = $container.find('select');
|
||||
$select.on('change', function(e) {
|
||||
var value = $(e.target).val();
|
||||
update(value);
|
||||
});
|
||||
update($select.val());
|
||||
});
|
||||
}
|
87
htdocs/lib/settings/ImageUpload.js
Normal file
@ -0,0 +1,87 @@
|
||||
$.fn.imageUpload = function() {
|
||||
$.each(this, function(){
|
||||
var $this = $(this);
|
||||
var $uploadButton = $this.find('button.upload');
|
||||
var $restoreButton = $this.find('button.restore');
|
||||
var $img = $this.find('img');
|
||||
var originalUrl = $img.prop('src');
|
||||
var $input = $this.find('input');
|
||||
var id = $input.prop('id');
|
||||
var maxSize = $this.data('max-size');
|
||||
var $error;
|
||||
var handleError = function(message) {
|
||||
clearError();
|
||||
$error = $('<div class="invalid-feedback">' + message + '</div>');
|
||||
$this.after($error);
|
||||
$this.addClass('is-invalid');
|
||||
};
|
||||
var clearError = function(message) {
|
||||
if ($error) $error.remove();
|
||||
$this.removeClass('is-invalid');
|
||||
};
|
||||
$uploadButton.click(function(){
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/jpeg, image/png, image/webp';
|
||||
|
||||
input.onchange = function(e) {
|
||||
$uploadButton.prop('disabled', true);
|
||||
var $spinner = $('<span class="spinner-border spinner-border-sm mr-1" role="status"></span>');
|
||||
$uploadButton.prepend($spinner);
|
||||
|
||||
var reader = new FileReader()
|
||||
reader.readAsArrayBuffer(e.target.files[0]);
|
||||
reader.onprogress = function(e) {
|
||||
if (e.loaded > maxSize) {
|
||||
handleError('Maximum file size exceeded');
|
||||
$uploadButton.prop('disabled', false);
|
||||
$spinner.remove();
|
||||
reader.abort();
|
||||
}
|
||||
};
|
||||
reader.onload = function(e) {
|
||||
if (e.loaded > maxSize) {
|
||||
handleError('Maximum file size exceeded');
|
||||
$uploadButton.prop('disabled', false);
|
||||
$spinner.remove();
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: '../imageupload?id=' + id,
|
||||
type: 'POST',
|
||||
data: e.target.result,
|
||||
processData: false,
|
||||
contentType: 'application/octet-stream',
|
||||
}).done(function(data){
|
||||
$input.val(data.file);
|
||||
$img.one('load', function() {
|
||||
$uploadButton.prop('disabled', false);
|
||||
$spinner.remove();
|
||||
});
|
||||
$img.prop('src', '../imageupload?file=' + data.file);
|
||||
clearError();
|
||||
}).fail(function(xhr, error){
|
||||
try {
|
||||
var res = JSON.parse(xhr.responseText);
|
||||
handleError(res.error || error);
|
||||
} catch (e) {
|
||||
handleError(error);
|
||||
}
|
||||
$uploadButton.prop('disabled', false);
|
||||
$spinner.remove();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
return false;
|
||||
});
|
||||
|
||||
$restoreButton.click(function(){
|
||||
$input.val('restore');
|
||||
$img.prop('src', originalUrl + "&mapped=false");
|
||||
clearError();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
function Input(name, value, options) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.options = options;
|
||||
this.label = options && options.label || name;
|
||||
};
|
||||
|
||||
Input.prototype.getClasses = function() {
|
||||
return ['form-control', 'form-control-sm'];
|
||||
}
|
||||
|
||||
Input.prototype.bootstrapify = function(input) {
|
||||
this.getClasses().forEach(input.addClass.bind(input));
|
||||
return [
|
||||
'<div class="form-group row">',
|
||||
'<label class="col-form-label col-form-label-sm col-3" for="' + this.name + '">' + this.label + '</label>',
|
||||
'<div class="col-9">',
|
||||
$.map(input, function(el) {
|
||||
return el.outerHTML;
|
||||
}).join(''),
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join('');
|
||||
};
|
||||
|
||||
function TextInput() {
|
||||
Input.apply(this, arguments);
|
||||
};
|
||||
|
||||
TextInput.prototype = new Input();
|
||||
|
||||
TextInput.prototype.render = function() {
|
||||
return this.bootstrapify($('<input type="text" name="' + this.name + '" value="' + this.value + '">'));
|
||||
}
|
||||
|
||||
function NumberInput() {
|
||||
Input.apply(this, arguments);
|
||||
};
|
||||
|
||||
NumberInput.prototype = new Input();
|
||||
|
||||
NumberInput.prototype.render = function() {
|
||||
return this.bootstrapify($('<input type="number" name="' + this.name + '" value="' + this.value + '">'));
|
||||
};
|
||||
|
||||
function SoapyGainInput() {
|
||||
Input.apply(this, arguments);
|
||||
}
|
||||
|
||||
SoapyGainInput.prototype = new Input();
|
||||
|
||||
SoapyGainInput.prototype.getClasses = function() {
|
||||
return [];
|
||||
};
|
||||
|
||||
SoapyGainInput.prototype.render = function(){
|
||||
var markup = $(
|
||||
'<div class="row form-group">' +
|
||||
'<div class="col-4">Gain mode</div>' +
|
||||
'<div class="col-8">' +
|
||||
'<select class="form-control form-control-sm">' +
|
||||
'<option value="auto">automatic gain</option>' +
|
||||
'<option value="single">single gain value</option>' +
|
||||
'<option value="separate">separate gain values</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="row option form-group gain-mode-single">' +
|
||||
'<div class="col-4">Gain</div>' +
|
||||
'<div class="col-8">' +
|
||||
'<input class="form-control form-control-sm" type="number">' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
this.options.gains.map(function(g){
|
||||
return '<div class="row option form-group gain-mode-separate">' +
|
||||
'<div class="col-4">' + g + '</div>' +
|
||||
'<div class="col-8">' +
|
||||
'<input class="form-control form-control-sm" data-gain="' + g + '" type="number">' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('')
|
||||
);
|
||||
var el = $(this.bootstrapify(markup))
|
||||
var setMode = function(mode){
|
||||
el.find('select').val(mode);
|
||||
el.find('.option').hide();
|
||||
el.find('.gain-mode-' + mode).show();
|
||||
};
|
||||
el.on('change', 'select', function(){
|
||||
var mode = $(this).val();
|
||||
setMode(mode);
|
||||
});
|
||||
if (typeof(this.value) === 'number') {
|
||||
setMode('single');
|
||||
el.find('.gain-mode-single input').val(this.value);
|
||||
} else if (typeof(this.value) === 'string') {
|
||||
if (this.value === 'auto') {
|
||||
setMode('auto');
|
||||
} else {
|
||||
setMode('separate');
|
||||
values = $.extend.apply($, this.value.split(',').map(function(seg){
|
||||
var split = seg.split('=');
|
||||
if (split.length < 2) return;
|
||||
var res = {};
|
||||
res[split[0]] = parseInt(split[1]);
|
||||
return res;
|
||||
}));
|
||||
el.find('.gain-mode-separate input').each(function(){
|
||||
var $input = $(this);
|
||||
var g = $input.data('gain');
|
||||
$input.val(g in values ? values[g] : 0);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setMode('auto');
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
function ProfileInput() {
|
||||
Input.apply(this, arguments);
|
||||
};
|
||||
|
||||
ProfileInput.prototype = new Input();
|
||||
|
||||
ProfileInput.prototype.render = function() {
|
||||
return $('<div><h3>Profiles</h3></div>');
|
||||
};
|
||||
|
||||
function SchedulerInput() {
|
||||
Input.apply(this, arguments);
|
||||
};
|
||||
|
||||
SchedulerInput.prototype = new Input();
|
||||
|
||||
SchedulerInput.prototype.render = function() {
|
||||
return $('<div><h3>Scheduler</h3></div>');
|
||||
};
|
23
htdocs/lib/settings/MapInput.js
Normal file
@ -0,0 +1,23 @@
|
||||
$.fn.mapInput = function() {
|
||||
this.each(function(el) {
|
||||
var $el = $(this);
|
||||
var field_id = $el.attr("for");
|
||||
var $lat = $('#' + field_id + '-lat');
|
||||
var $lon = $('#' + field_id + '-lon');
|
||||
$.getScript('https://maps.googleapis.com/maps/api/js?key=' + $el.data('key')).done(function(){
|
||||
$el.css('height', '200px');
|
||||
var lp = new locationPicker($el.get(0), {
|
||||
lat: parseFloat($lat.val()),
|
||||
lng: parseFloat($lon.val())
|
||||
}, {
|
||||
zoom: 7
|
||||
});
|
||||
|
||||
google.maps.event.addListener(lp.map, 'idle', function(event){
|
||||
var pos = lp.getMarkerPosition();
|
||||
$lat.val(pos.lat);
|
||||
$lon.val(pos.lng);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
29
htdocs/lib/settings/OptionalSection.js
Normal file
@ -0,0 +1,29 @@
|
||||
$.fn.optionalSection = function(){
|
||||
this.each(function() {
|
||||
var $section = $(this);
|
||||
var $select = $section.find('.optional-select');
|
||||
var $optionalInputs = $section.find('.optional-inputs');
|
||||
$section.on('click', '.option-add-button', function(e){
|
||||
var field = $select.val();
|
||||
var group = $optionalInputs.find(".form-group[data-field='" + field + "']");
|
||||
group.find('input, select').filter(function(){
|
||||
// exclude template inputs
|
||||
return !$(this).parents('.template').length;
|
||||
}).prop('disabled', false);
|
||||
$section.find('hr').before(group);
|
||||
$select.find('option[value=\'' + field + '\']').remove();
|
||||
|
||||
return false;
|
||||
});
|
||||
$section.on('click', '.option-remove-button', function(e) {
|
||||
var group = $(e.target).parents('.form-group')
|
||||
group.find('input, select').prop('disabled', true);
|
||||
$optionalInputs.append(group);
|
||||
var $label = group.find('label');
|
||||
var $option = $('<option value="' + group.data('field') + '">' + $label.text() + '</option>');
|
||||
$select.append($option);
|
||||
|
||||
return false;
|
||||
})
|
||||
});
|
||||
}
|
33
htdocs/lib/settings/SchedulerInput.js
Normal file
@ -0,0 +1,33 @@
|
||||
$.fn.schedulerInput = function() {
|
||||
this.each(function() {
|
||||
var $container = $(this);
|
||||
var $template = $container.find('.template');
|
||||
$template.find('input, select').prop('disabled', true);
|
||||
|
||||
var update = function(value){
|
||||
$container.find('.option').hide();
|
||||
$container.find('.option.' + value).show();
|
||||
}
|
||||
|
||||
var $select = $container.find('select.mode');
|
||||
$select.on('change', function(e) {
|
||||
var value = $(e.target).val();
|
||||
update(value);
|
||||
});
|
||||
update($select.val());
|
||||
|
||||
$container.find('.add-button').on('click', function() {
|
||||
var row = $template.clone();
|
||||
row.removeClass('template').show();
|
||||
row.find('input, select').prop('disabled', false);
|
||||
$template.before(row);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$container.on('click', '.remove-button', function(e) {
|
||||
var row = $(e.target).parents('.scheduler-static-time-inputs');
|
||||
row.remove();
|
||||
});
|
||||
});
|
||||
}
|
@ -1,252 +0,0 @@
|
||||
function SdrDevice(el, data) {
|
||||
this.el = el;
|
||||
this.data = data;
|
||||
this.inputs = {};
|
||||
this.render();
|
||||
|
||||
var self = this;
|
||||
el.on('click', '.fieldselector .btn', function() {
|
||||
var key = el.find('.fieldselector select').val();
|
||||
self.data[key] = self.getInitialValue(key);
|
||||
self.render();
|
||||
});
|
||||
};
|
||||
|
||||
SdrDevice.create = function(el) {
|
||||
var data = JSON.parse(decodeURIComponent(el.data('config')));
|
||||
var type = data.type;
|
||||
var constructor = SdrDevice.types[type] || SdrDevice;
|
||||
return new constructor(el, data);
|
||||
};
|
||||
|
||||
SdrDevice.prototype.getData = function() {
|
||||
return $.extend(new Object(), this.getDefaults(), this.data);
|
||||
};
|
||||
|
||||
SdrDevice.prototype.getDefaults = function() {
|
||||
var defaults = {}
|
||||
$.each(this.getMappings(), function(k, v) {
|
||||
if (!v.includeInDefault) return;
|
||||
defaults[k] = 'initialValue' in v ? v['initialValue'] : false;
|
||||
});
|
||||
return defaults;
|
||||
};
|
||||
|
||||
SdrDevice.prototype.getMappings = function() {
|
||||
return {
|
||||
"name": {
|
||||
constructor: TextInput,
|
||||
inputOptions: {
|
||||
label: "Name"
|
||||
},
|
||||
initialValue: "",
|
||||
includeInDefault: true
|
||||
},
|
||||
"type": {
|
||||
constructor: TextInput,
|
||||
inputOptions: {
|
||||
label: "Type"
|
||||
},
|
||||
initialValue: '',
|
||||
includeInDefault: true
|
||||
},
|
||||
"ppm": {
|
||||
constructor: NumberInput,
|
||||
inputOptions: {
|
||||
label: "PPM"
|
||||
},
|
||||
initialValue: 0
|
||||
},
|
||||
"profiles": {
|
||||
constructor: ProfileInput,
|
||||
inputOptions: {
|
||||
label: "Profiles"
|
||||
},
|
||||
initialValue: [],
|
||||
includeInDefault: true,
|
||||
position: 100
|
||||
},
|
||||
"scheduler": {
|
||||
constructor: SchedulerInput,
|
||||
inputOptions: {
|
||||
label: "Scheduler",
|
||||
},
|
||||
initialValue: {},
|
||||
position: 101
|
||||
},
|
||||
"rf_gain": {
|
||||
constructor: TextInput,
|
||||
inputOptions: {
|
||||
label: "Gain",
|
||||
},
|
||||
initialValue: 0
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
SdrDevice.prototype.getMapping = function(key) {
|
||||
var mappings = this.getMappings();
|
||||
return mappings[key];
|
||||
};
|
||||
|
||||
SdrDevice.prototype.getInputClass = function(key) {
|
||||
var mapping = this.getMapping(key);
|
||||
return mapping && mapping.constructor || TextInput;
|
||||
};
|
||||
|
||||
SdrDevice.prototype.getInitialValue = function(key) {
|
||||
var mapping = this.getMapping(key);
|
||||
return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false;
|
||||
};
|
||||
|
||||
SdrDevice.prototype.getPosition = function(key) {
|
||||
var mapping = this.getMapping(key);
|
||||
return mapping && mapping.position || 10;
|
||||
};
|
||||
|
||||
SdrDevice.prototype.getInputOptions = function(key) {
|
||||
var mapping = this.getMapping(key);
|
||||
return mapping && mapping.inputOptions || {};
|
||||
};
|
||||
|
||||
SdrDevice.prototype.getLabel = function(key) {
|
||||
var options = this.getInputOptions(key);
|
||||
return options && options.label || key;
|
||||
};
|
||||
|
||||
SdrDevice.prototype.render = function() {
|
||||
var self = this;
|
||||
self.el.empty();
|
||||
var data = this.getData();
|
||||
Object.keys(data).sort(function(a, b){
|
||||
return self.getPosition(a) - self.getPosition(b);
|
||||
}).forEach(function(key){
|
||||
var value = data[key];
|
||||
var inputClass = self.getInputClass(key);
|
||||
var input = new inputClass(key, value, self.getInputOptions(key));
|
||||
self.inputs[key] = input;
|
||||
self.el.append(input.render());
|
||||
});
|
||||
self.el.append(this.renderFieldSelector());
|
||||
};
|
||||
|
||||
SdrDevice.prototype.renderFieldSelector = function() {
|
||||
var self = this;
|
||||
return '<div class="fieldselector">' +
|
||||
'<h3>Add new configuration options<h3>' +
|
||||
'<div class="form-group row">' +
|
||||
'<div class="col-3"><select class="form-control form-control-sm">' +
|
||||
Object.keys(self.getMappings()).filter(function(m){
|
||||
return !(m in self.data);
|
||||
}).map(function(m) {
|
||||
return '<option value="' + m + '">' + self.getLabel(m) + '</option>';
|
||||
}).join('') +
|
||||
'</select></div>' +
|
||||
'<div class="col-2">' +
|
||||
'<div class="btn btn-primary">Add to config</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
};
|
||||
|
||||
RtlSdrDevice = function() {
|
||||
SdrDevice.apply(this, arguments);
|
||||
};
|
||||
|
||||
RtlSdrDevice.prototype = Object.create(SdrDevice.prototype);
|
||||
RtlSdrDevice.prototype.constructor = RtlSdrDevice;
|
||||
|
||||
RtlSdrDevice.prototype.getMappings = function() {
|
||||
var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
|
||||
return $.extend(new Object(), mappings, {
|
||||
"device": {
|
||||
constructor: TextInput,
|
||||
inputOptions:{
|
||||
label: "Serial number"
|
||||
},
|
||||
initialValue: ""
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SoapySdrDevice = function() {
|
||||
SdrDevice.apply(this, arguments);
|
||||
};
|
||||
|
||||
SoapySdrDevice.prototype = Object.create(SdrDevice.prototype);
|
||||
SoapySdrDevice.prototype.constructor = SoapySdrDevice;
|
||||
|
||||
SoapySdrDevice.prototype.getMappings = function() {
|
||||
var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
|
||||
return $.extend(new Object(), mappings, {
|
||||
"device": {
|
||||
constructor: TextInput,
|
||||
inputOptions:{
|
||||
label: "Soapy device selector"
|
||||
},
|
||||
initialValue: ""
|
||||
},
|
||||
"rf_gain": {
|
||||
constructor: SoapyGainInput,
|
||||
initialValue: 0,
|
||||
inputOptions: {
|
||||
label: "Gain",
|
||||
gains: this.getGains()
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SoapySdrDevice.prototype.getGains = function() {
|
||||
return [];
|
||||
};
|
||||
|
||||
SdrplaySdrDevice = function() {
|
||||
SoapySdrDevice.apply(this, arguments);
|
||||
};
|
||||
|
||||
SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
|
||||
SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice;
|
||||
|
||||
SdrplaySdrDevice.prototype.getGains = function() {
|
||||
return ['RFGR', 'IFGR'];
|
||||
};
|
||||
|
||||
AirspyHfSdrDevice = function() {
|
||||
SoapySdrDevice.apply(this, arguments);
|
||||
};
|
||||
|
||||
AirspyHfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
|
||||
AirspyHfSdrDevice.prototype.constructor = AirspyHfSdrDevice;
|
||||
|
||||
AirspyHfSdrDevice.prototype.getGains = function() {
|
||||
return ['RF', 'VGA'];
|
||||
};
|
||||
|
||||
HackRfSdrDevice = function() {
|
||||
SoapySdrDevice.apply(this, arguments);
|
||||
};
|
||||
|
||||
HackRfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
|
||||
HackRfSdrDevice.prototype.constructor = HackRfSdrDevice;
|
||||
|
||||
HackRfSdrDevice.prototype.getGains = function() {
|
||||
return ['LNA', 'VGA', 'AMP'];
|
||||
};
|
||||
|
||||
SdrDevice.types = {
|
||||
'rtl_sdr': RtlSdrDevice,
|
||||
'sdrplay': SdrplaySdrDevice,
|
||||
'airspyhf': AirspyHfSdrDevice,
|
||||
'hackrf': HackRfSdrDevice
|
||||
};
|
||||
|
||||
$.fn.sdrdevice = function() {
|
||||
return this.map(function(){
|
||||
var el = $(this);
|
||||
if (!el.data('sdrdevice')) {
|
||||
el.data('sdrdevice', SdrDevice.create(el));
|
||||
}
|
||||
return el.data('sdrdevice');
|
||||
});
|
||||
};
|
11
htdocs/lib/settings/WaterfallDropdown.js
Normal file
@ -0,0 +1,11 @@
|
||||
$.fn.waterfallDropdown = function(){
|
||||
this.each(function(){
|
||||
var $select = $(this);
|
||||
var setVisibility = function() {
|
||||
var show = $select.val() === 'CUSTOM';
|
||||
$('#waterfall_colors').parents('.form-group')[show ? 'show' : 'hide']();
|
||||
}
|
||||
$select.on('change', setVisibility);
|
||||
setVisibility();
|
||||
})
|
||||
}
|
68
htdocs/lib/settings/WsjtDecodingDepthsInput.js
Normal file
@ -0,0 +1,68 @@
|
||||
$.fn.wsjtDecodingDepthsInput = function() {
|
||||
function WsjtDecodingDepthRow(inputs, mode, value) {
|
||||
this.el = $('<tr>');
|
||||
this.modeInput = $(inputs.get(0)).clone();
|
||||
this.modeInput.val(mode);
|
||||
this.valueInput = $(inputs.get(1)).clone();
|
||||
this.valueInput.val(value);
|
||||
this.removeButton = $('<button type="button" class="btn btn-sm btn-danger remove">Remove</button>');
|
||||
this.removeButton.data('row', this);
|
||||
this.el.append([this.modeInput, this.valueInput, this.removeButton].map(function(i) {
|
||||
return $('<td>').append(i);
|
||||
}));
|
||||
}
|
||||
|
||||
WsjtDecodingDepthRow.prototype.getEl = function() {
|
||||
return this.el;
|
||||
}
|
||||
|
||||
WsjtDecodingDepthRow.prototype.getValue = function() {
|
||||
var value = parseInt(this.valueInput.val())
|
||||
if (Number.isNaN(value)) {
|
||||
return {};
|
||||
}
|
||||
return Object.fromEntries([[this.modeInput.val(), value]]);
|
||||
}
|
||||
|
||||
this.each(function(){
|
||||
var $input = $(this);
|
||||
var $el = $input.parent();
|
||||
var $inputs = $el.find('.inputs')
|
||||
var inputs = $inputs.find('input, select');
|
||||
$inputs.remove();
|
||||
var rows = $.map(JSON.parse($input.val()), function(value, mode) {
|
||||
return new WsjtDecodingDepthRow(inputs, mode, value);
|
||||
});
|
||||
var $table = $('<table class="table table-sm table-borderless wsjt-decoding-depths-table">');
|
||||
$table.append(rows.map(function(r) {
|
||||
return r.getEl();
|
||||
}));
|
||||
|
||||
var updateValue = function(){
|
||||
$input.val(JSON.stringify($.extend.apply({}, rows.map(function(r) {
|
||||
return r.getValue();
|
||||
}))));
|
||||
};
|
||||
|
||||
$table.on('change', updateValue);
|
||||
var $addButton = $('<button type="button" class="btn btn-sm btn-primary">Add...</button>');
|
||||
|
||||
$addButton.on('click', function() {
|
||||
var row = new WsjtDecodingDepthRow(inputs)
|
||||
rows.push(row);
|
||||
$table.append(row.getEl());
|
||||
return false;
|
||||
});
|
||||
$el.on('click', '.btn.remove', function(e){
|
||||
var row = $(e.target).data('row');
|
||||
var index = rows.indexOf(row);
|
||||
if (index < 0) return false;
|
||||
rows.splice(index, 1);
|
||||
row.getEl().remove();
|
||||
updateValue();
|
||||
return false;
|
||||
});
|
||||
|
||||
$input.after($table, $addButton);
|
||||
});
|
||||
};
|
@ -11,17 +11,19 @@
|
||||
</head>
|
||||
<body>
|
||||
${header}
|
||||
<div class="login">
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="user">Username</label>
|
||||
<input type="text" class="form-control" id="user" name="user" autofocus="autofocus" placeholder="Username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary btn-login">Login</button>
|
||||
</form>
|
||||
<div class="login-container">
|
||||
<div class="login">
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="user">Username</label>
|
||||
<input type="text" class="form-control" id="user" name="user" autofocus="autofocus" placeholder="Username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary btn-login">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
@ -2,6 +2,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenWebRX Map</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="theme-color" content="#222" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||
<script src="compiled/map.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||
|
159
htdocs/map.js
@ -1,4 +1,4 @@
|
||||
(function(){
|
||||
$(function(){
|
||||
var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){
|
||||
var s = v.split('=');
|
||||
var r = {};
|
||||
@ -9,7 +9,7 @@
|
||||
});
|
||||
|
||||
var expectedCallsign;
|
||||
if (query.callsign) expectedCallsign = query.callsign;
|
||||
if (query.callsign) expectedCallsign = decodeURIComponent(query.callsign);
|
||||
var expectedLocator;
|
||||
if (query.locator) expectedLocator = query.locator;
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
var map;
|
||||
var markers = {};
|
||||
var rectangles = {};
|
||||
var receiverMarker;
|
||||
var updateQueue = [];
|
||||
|
||||
// reasonable default; will be overriden by server
|
||||
@ -44,7 +45,12 @@
|
||||
if (!colorKeys[id]) {
|
||||
var keys = Object.keys(colorKeys);
|
||||
keys.push(id);
|
||||
keys.sort();
|
||||
keys.sort(function(a, b) {
|
||||
var pa = parseFloat(a);
|
||||
var pb = parseFloat(b);
|
||||
if (isNaN(pa) || isNaN(pb)) return a.localeCompare(b);
|
||||
return pa - pb;
|
||||
});
|
||||
var colors = colorScale.colors(keys.length);
|
||||
colorKeys = {};
|
||||
keys.forEach(function(key, index) {
|
||||
@ -81,6 +87,7 @@
|
||||
$('#openwebrx-map-colormode').on('change', function(){
|
||||
colorMode = $(this).val();
|
||||
colorKeys = {};
|
||||
filterRectangles(allRectangles);
|
||||
reColor();
|
||||
updateLegend();
|
||||
});
|
||||
@ -88,7 +95,10 @@
|
||||
|
||||
var updateLegend = function() {
|
||||
var lis = $.map(colorKeys, function(value, key) {
|
||||
return '<li class="square"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>';
|
||||
// fake rectangle to test if the filter would match
|
||||
var fakeRectangle = Object.fromEntries([[colorMode.slice(2), key]]);
|
||||
var disabled = rectangleFilter(fakeRectangle) ? '' : ' disabled';
|
||||
return '<li class="square' + disabled + '" data-selector="' + key + '"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>';
|
||||
});
|
||||
$(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>');
|
||||
}
|
||||
@ -131,14 +141,13 @@
|
||||
marker.band = update.band;
|
||||
marker.comment = update.location.comment;
|
||||
|
||||
// TODO the trim should happen on the server side
|
||||
if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
|
||||
if (expectedCallsign && expectedCallsign == update.callsign) {
|
||||
map.panTo(pos);
|
||||
showMarkerInfoWindow(update.callsign, pos);
|
||||
expectedCallsign = false;
|
||||
}
|
||||
|
||||
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign.trim()) {
|
||||
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign) {
|
||||
showMarkerInfoWindow(infowindow.callsign, pos);
|
||||
}
|
||||
break;
|
||||
@ -159,11 +168,17 @@
|
||||
});
|
||||
rectangles[update.callsign] = rectangle;
|
||||
}
|
||||
rectangle.lastseen = update.lastseen;
|
||||
rectangle.locator = update.location.locator;
|
||||
rectangle.mode = update.mode;
|
||||
rectangle.band = update.band;
|
||||
rectangle.center = center;
|
||||
|
||||
rectangle.setOptions($.extend({
|
||||
strokeColor: color,
|
||||
strokeWeight: 2,
|
||||
fillColor: color,
|
||||
map: map,
|
||||
map: rectangleFilter(rectangle) ? map : undefined,
|
||||
bounds:{
|
||||
north: lat,
|
||||
south: lat + 1,
|
||||
@ -171,11 +186,6 @@
|
||||
east: lon + 2
|
||||
}
|
||||
}, getRectangleOpacityOptions(update.lastseen) ));
|
||||
rectangle.lastseen = update.lastseen;
|
||||
rectangle.locator = update.location.locator;
|
||||
rectangle.mode = update.mode;
|
||||
rectangle.band = update.band;
|
||||
rectangle.center = center;
|
||||
|
||||
if (expectedLocator && expectedLocator == update.location.locator) {
|
||||
map.panTo(center);
|
||||
@ -195,12 +205,15 @@
|
||||
var reset = function(callsign, item) { item.setMap(); };
|
||||
$.each(markers, reset);
|
||||
$.each(rectangles, reset);
|
||||
receiverMarker.setMap();
|
||||
markers = {};
|
||||
rectangles = {};
|
||||
};
|
||||
|
||||
var reconnect_timeout = false;
|
||||
|
||||
var config = {}
|
||||
|
||||
var connect = function(){
|
||||
var ws = new WebSocket(ws_url);
|
||||
ws.onopen = function(){
|
||||
@ -214,40 +227,71 @@
|
||||
return
|
||||
}
|
||||
if (e.data.substr(0, 16) == "CLIENT DE SERVER") {
|
||||
console.log("Server acknowledged WebSocket connection.");
|
||||
return
|
||||
}
|
||||
try {
|
||||
var json = JSON.parse(e.data);
|
||||
switch (json.type) {
|
||||
case "config":
|
||||
var config = json.value;
|
||||
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
|
||||
map = new google.maps.Map($('.openwebrx-map')[0], {
|
||||
center: {
|
||||
lat: config.receiver_gps.lat,
|
||||
lng: config.receiver_gps.lon
|
||||
},
|
||||
zoom: 5,
|
||||
});
|
||||
Object.assign(config, json.value);
|
||||
if ('receiver_gps' in config) {
|
||||
var receiverPos = {
|
||||
lat: config.receiver_gps.lat,
|
||||
lng: config.receiver_gps.lon
|
||||
};
|
||||
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
|
||||
map = new google.maps.Map($('.openwebrx-map')[0], {
|
||||
center: receiverPos,
|
||||
zoom: 5,
|
||||
});
|
||||
|
||||
$.getScript("static/lib/nite-overlay.js").done(function(){
|
||||
nite.init(map);
|
||||
setInterval(function() { nite.refresh() }, 10000); // every 10s
|
||||
$.getScript("static/lib/nite-overlay.js").done(function(){
|
||||
nite.init(map);
|
||||
setInterval(function() { nite.refresh() }, 10000); // every 10s
|
||||
});
|
||||
$.getScript('static/lib/AprsMarker.js').done(function(){
|
||||
processUpdates(updateQueue);
|
||||
updateQueue = [];
|
||||
});
|
||||
|
||||
var $legend = $(".openwebrx-map-legend");
|
||||
setupLegendFilters($legend);
|
||||
map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($legend[0]);
|
||||
|
||||
if (!receiverMarker) {
|
||||
receiverMarker = new google.maps.Marker();
|
||||
receiverMarker.addListener('click', function() {
|
||||
showReceiverInfoWindow(receiverMarker);
|
||||
});
|
||||
}
|
||||
receiverMarker.setOptions({
|
||||
map: map,
|
||||
position: receiverPos,
|
||||
title: config['receiver_name'],
|
||||
config: config
|
||||
});
|
||||
}); else {
|
||||
receiverMarker.setOptions({
|
||||
map: map,
|
||||
position: receiverPos,
|
||||
config: config
|
||||
});
|
||||
}
|
||||
}
|
||||
if ('receiver_name' in config && receiverMarker) {
|
||||
receiverMarker.setOptions({
|
||||
title: config['receiver_name']
|
||||
});
|
||||
$.getScript('static/lib/AprsMarker.js').done(function(){
|
||||
processUpdates(updateQueue);
|
||||
updateQueue = [];
|
||||
});
|
||||
map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($(".openwebrx-map-legend")[0]);
|
||||
});
|
||||
retention_time = config.map_position_retention_time * 1000;
|
||||
}
|
||||
if ('map_position_retention_time' in config) {
|
||||
retention_time = config.map_position_retention_time * 1000;
|
||||
}
|
||||
break;
|
||||
case "update":
|
||||
processUpdates(json.value);
|
||||
break;
|
||||
case 'receiver_details':
|
||||
$('#webrx-top-container').header().setDetails(json['value']);
|
||||
$('.webrx-top-container').header().setDetails(json['value']);
|
||||
break;
|
||||
default:
|
||||
console.warn('received message of unknown type: ' + json['type']);
|
||||
@ -291,6 +335,8 @@
|
||||
delete infowindow.callsign;
|
||||
});
|
||||
}
|
||||
delete infowindow.locator;
|
||||
delete infowindow.callsign;
|
||||
return infowindow;
|
||||
}
|
||||
|
||||
@ -300,7 +346,7 @@
|
||||
infowindow.locator = locator;
|
||||
var inLocator = $.map(rectangles, function(r, callsign) {
|
||||
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
|
||||
}).filter(function(d) {
|
||||
}).filter(rectangleFilter).filter(function(d) {
|
||||
return d.locator == locator;
|
||||
}).sort(function(a, b){
|
||||
return b.lastseen - a.lastseen;
|
||||
@ -339,6 +385,15 @@
|
||||
infowindow.open(map, marker);
|
||||
}
|
||||
|
||||
var showReceiverInfoWindow = function(marker) {
|
||||
var infowindow = getInfoWindow()
|
||||
infowindow.setContent(
|
||||
'<h3>' + marker.config['receiver_name'] + '</h3>' +
|
||||
'<div>Receiver location</div>'
|
||||
);
|
||||
infowindow.open(map, marker);
|
||||
}
|
||||
|
||||
var getScale = function(lastseen) {
|
||||
var age = new Date().getTime() - lastseen;
|
||||
var scale = 1;
|
||||
@ -386,4 +441,36 @@
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
})();
|
||||
var rectangleFilter = allRectangles = function() { return true; };
|
||||
|
||||
var filterRectangles = function(filter) {
|
||||
rectangleFilter = filter;
|
||||
$.each(rectangles, function(_, r) {
|
||||
r.setMap(rectangleFilter(r) ? map : undefined);
|
||||
});
|
||||
};
|
||||
|
||||
var setupLegendFilters = function($legend) {
|
||||
$content = $legend.find('.content');
|
||||
$content.on('click', 'li', function() {
|
||||
var $el = $(this);
|
||||
$lis = $content.find('li');
|
||||
if ($lis.hasClass('disabled') && !$el.hasClass('disabled')) {
|
||||
$lis.removeClass('disabled');
|
||||
filterRectangles(allRectangles);
|
||||
} else {
|
||||
$el.removeClass('disabled');
|
||||
$lis.filter(function() {
|
||||
return this != $el[0]
|
||||
}).addClass('disabled');
|
||||
|
||||
var key = colorMode.slice(2);
|
||||
var selector = $el.data('selector');
|
||||
filterRectangles(function(r) {
|
||||
return r[key] === selector;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -3,7 +3,7 @@
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -32,26 +32,20 @@ var fft_codec;
|
||||
var waterfall_setup_done = 0;
|
||||
var secondary_fft_size;
|
||||
|
||||
function e(what) {
|
||||
return document.getElementById(what);
|
||||
}
|
||||
|
||||
function updateVolume() {
|
||||
audioEngine.setVolume(parseFloat(e("openwebrx-panel-volume").value) / 100);
|
||||
audioEngine.setVolume(parseFloat($("#openwebrx-panel-volume").val()) / 100);
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (mute) {
|
||||
mute = false;
|
||||
e("openwebrx-mute-on").id = "openwebrx-mute-off";
|
||||
e("openwebrx-panel-volume").disabled = false;
|
||||
e("openwebrx-panel-volume").value = volumeBeforeMute;
|
||||
var $muteButton = $('.openwebrx-mute-button');
|
||||
var $volumePanel = $('#openwebrx-panel-volume');
|
||||
if ($muteButton.hasClass('muted')) {
|
||||
$muteButton.removeClass('muted');
|
||||
$volumePanel.prop('disabled', false).val(volumeBeforeMute);
|
||||
} else {
|
||||
mute = true;
|
||||
e("openwebrx-mute-off").id = "openwebrx-mute-on";
|
||||
e("openwebrx-panel-volume").disabled = true;
|
||||
volumeBeforeMute = e("openwebrx-panel-volume").value;
|
||||
e("openwebrx-panel-volume").value = 0;
|
||||
$muteButton.addClass('muted');
|
||||
volumeBeforeMute = $volumePanel.val();
|
||||
$volumePanel.prop('disabled', true).val(0);
|
||||
}
|
||||
|
||||
updateVolume();
|
||||
@ -78,7 +72,8 @@ var waterfall_max_level;
|
||||
var waterfall_min_level_default;
|
||||
var waterfall_max_level_default;
|
||||
var waterfall_colors = buildWaterfallColors(['#000', '#FFF']);
|
||||
var waterfall_auto_level_margin;
|
||||
var waterfall_auto_levels;
|
||||
var waterfall_auto_min_range;
|
||||
|
||||
function buildWaterfallColors(input) {
|
||||
return chroma.scale(input).colors(256, 'rgb')
|
||||
@ -116,9 +111,9 @@ function waterfallColorsDefault() {
|
||||
}
|
||||
|
||||
function waterfallColorsAuto(levels) {
|
||||
var min_level = levels.min - waterfall_auto_level_margin.min;
|
||||
var max_level = levels.max + waterfall_auto_level_margin.max;
|
||||
max_level = Math.max(min_level + (waterfall_auto_level_margin.min_range || 0), max_level);
|
||||
var min_level = levels.min - waterfall_auto_levels.min;
|
||||
var max_level = levels.max + waterfall_auto_levels.max;
|
||||
max_level = Math.max(min_level + (waterfall_auto_min_range || 0), max_level);
|
||||
waterfall_min_level = min_level;
|
||||
waterfall_max_level = max_level;
|
||||
updateWaterfallSliders();
|
||||
@ -151,13 +146,19 @@ function waterfallColorsContinuous(levels) {
|
||||
function setSmeterRelativeValue(value) {
|
||||
if (value < 0) value = 0;
|
||||
if (value > 1.0) value = 1.0;
|
||||
var bar = e("openwebrx-smeter-bar");
|
||||
var outer = e("openwebrx-smeter-outer");
|
||||
bar.style.width = (outer.offsetWidth * value).toString() + "px";
|
||||
var bgRed = "linear-gradient(to top, #ff5939 , #961700)";
|
||||
var bgGreen = "linear-gradient(to top, #22ff2f , #008908)";
|
||||
var bgYellow = "linear-gradient(to top, #fff720 , #a49f00)";
|
||||
bar.style.background = (value > 0.9) ? bgRed : ((value > 0.7) ? bgYellow : bgGreen);
|
||||
var $meter = $("#openwebrx-smeter");
|
||||
var $bar = $meter.find(".openwebrx-smeter-bar");
|
||||
$bar.css({transform: 'translate(' + ((value - 1) * 100) + '%) translateZ(0)'});
|
||||
if (value > 0.9) {
|
||||
// red
|
||||
$bar.css({background: 'linear-gradient(to top, #ff5939 , #961700)'});
|
||||
} else if (value > 0.7) {
|
||||
// yellow
|
||||
$bar.css({background: 'linear-gradient(to top, #fff720 , #a49f00)'});
|
||||
} else {
|
||||
// red
|
||||
$bar.css({background: 'linear-gradient(to top, #22ff2f , #008908)'});
|
||||
}
|
||||
}
|
||||
|
||||
function setSquelchSliderBackground(val) {
|
||||
@ -185,7 +186,7 @@ function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch
|
||||
var highLevel = waterfall_max_level + 20;
|
||||
var percent = (logValue - lowLevel) / (highLevel - lowLevel);
|
||||
setSmeterRelativeValue(percent);
|
||||
e("openwebrx-smeter-db").innerHTML = logValue.toFixed(1) + " dB";
|
||||
$("#openwebrx-smeter-db").html(logValue.toFixed(1) + " dB");
|
||||
}
|
||||
|
||||
function typeInAnimation(element, timeout, what, onFinish) {
|
||||
@ -238,14 +239,14 @@ var scale_ctx;
|
||||
var scale_canvas;
|
||||
|
||||
function scale_setup() {
|
||||
scale_canvas = e("openwebrx-scale-canvas");
|
||||
scale_canvas = $("#openwebrx-scale-canvas")[0];
|
||||
scale_ctx = scale_canvas.getContext("2d");
|
||||
scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false);
|
||||
scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false);
|
||||
scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false);
|
||||
resize_scale();
|
||||
var frequency_container = e("openwebrx-frequency-container");
|
||||
frequency_container.addEventListener("mousemove", frequency_container_mousemove, false);
|
||||
var frequency_container = $("#openwebrx-frequency-container");
|
||||
frequency_container.on("mousemove", frequency_container_mousemove, false);
|
||||
}
|
||||
|
||||
var scale_canvas_drag_params = {
|
||||
@ -292,7 +293,7 @@ function scale_canvas_mousemove(evt) {
|
||||
|
||||
function frequency_container_mousemove(evt) {
|
||||
var frequency = center_freq + scale_offset_freq_from_px(evt.pageX);
|
||||
$('.webrx-mouse-freq').frequencyDisplay().setFrequency(frequency);
|
||||
$('#openwebrx-panel-receiver').demodulatorPanel().setMouseFrequency(frequency);
|
||||
}
|
||||
|
||||
function scale_canvas_end_drag(x) {
|
||||
@ -314,14 +315,16 @@ function scale_px_from_freq(f, range) {
|
||||
}
|
||||
|
||||
function get_visible_freq_range() {
|
||||
var out = {};
|
||||
if (!bandwidth) return false;
|
||||
var fcalc = function (x) {
|
||||
var canvasWidth = waterfallWidth() * zoom_levels[zoom_level];
|
||||
return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2);
|
||||
};
|
||||
out.start = fcalc(0);
|
||||
out.center = fcalc(waterfallWidth() / 2);
|
||||
out.end = fcalc(waterfallWidth());
|
||||
var out = {
|
||||
start: fcalc(0),
|
||||
center: fcalc(waterfallWidth() / 2),
|
||||
end: fcalc(waterfallWidth()),
|
||||
}
|
||||
out.bw = out.end - out.start;
|
||||
out.hps = out.bw / waterfallWidth();
|
||||
return out;
|
||||
@ -426,6 +429,7 @@ var range;
|
||||
function mkscale() {
|
||||
//clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes):
|
||||
range = get_visible_freq_range();
|
||||
if (!range) return;
|
||||
mkenvelopes(range); //when scale changes we will always have to redraw filter envelopes, too
|
||||
scale_ctx.clearRect(0, 22, scale_ctx.canvas.width, scale_ctx.canvas.height - 22);
|
||||
scale_ctx.strokeStyle = "#fff";
|
||||
@ -442,9 +446,7 @@ function mkscale() {
|
||||
};
|
||||
var last_large;
|
||||
var x;
|
||||
for (; ;) {
|
||||
x = scale_px_from_freq(marker_hz, range);
|
||||
if (x > window.innerWidth) break;
|
||||
while ((x = scale_px_from_freq(marker_hz, range)) <= window.innerWidth) {
|
||||
scale_ctx.beginPath();
|
||||
scale_ctx.moveTo(x, 22);
|
||||
if (marker_hz % spacing.params.large_marker_per_hz === 0) { //large marker
|
||||
@ -510,7 +512,7 @@ function resize_scale() {
|
||||
}
|
||||
|
||||
function canvas_get_freq_offset(relativeX) {
|
||||
var rel = (relativeX / canvases[0].clientWidth);
|
||||
var rel = (relativeX / canvas_container.clientWidth);
|
||||
return Math.round((bandwidth * rel) - (bandwidth / 2));
|
||||
}
|
||||
|
||||
@ -570,7 +572,7 @@ function canvas_mousemove(evt) {
|
||||
bookmarks.position();
|
||||
}
|
||||
} else {
|
||||
$('.webrx-mouse-freq').frequencyDisplay().setFrequency(canvas_get_frequency(relativeX));
|
||||
$('#openwebrx-panel-receiver').demodulatorPanel().setMouseFrequency(canvas_get_frequency(relativeX));
|
||||
}
|
||||
}
|
||||
|
||||
@ -683,7 +685,11 @@ function zoom_calc() {
|
||||
}
|
||||
|
||||
var networkSpeedMeasurement;
|
||||
var currentprofile;
|
||||
var currentprofile = {
|
||||
toString: function() {
|
||||
return this['sdr_id'] + '|' + this['profile_id'];
|
||||
}
|
||||
};
|
||||
|
||||
var COMPRESS_FFT_PAD_N = 10; //should be the same as in csdr.c
|
||||
|
||||
@ -693,56 +699,96 @@ function on_ws_recv(evt) {
|
||||
networkSpeedMeasurement.add(evt.data.length);
|
||||
|
||||
if (evt.data.substr(0, 16) === "CLIENT DE SERVER") {
|
||||
divlog("Server acknowledged WebSocket connection.");
|
||||
params = Object.fromEntries(
|
||||
evt.data.slice(17).split(' ').map(function(param) {
|
||||
var args = param.split('=');
|
||||
return [args[0], args.slice(1).join('=')]
|
||||
})
|
||||
);
|
||||
var versionInfo = 'Unknown server';
|
||||
if (params.server && params.server === 'openwebrx' && params.version) {
|
||||
versionInfo = 'OpenWebRX version: ' + params.version;
|
||||
}
|
||||
divlog('Server acknowledged WebSocket connection, ' + versionInfo);
|
||||
} else {
|
||||
try {
|
||||
var json = JSON.parse(evt.data);
|
||||
switch (json.type) {
|
||||
case "config":
|
||||
var config = json['value'];
|
||||
waterfall_colors = buildWaterfallColors(config['waterfall_colors']);
|
||||
waterfall_min_level_default = config['waterfall_min_level'];
|
||||
waterfall_max_level_default = config['waterfall_max_level'];
|
||||
waterfall_auto_level_margin = config['waterfall_auto_level_margin'];
|
||||
if ('waterfall_colors' in config)
|
||||
waterfall_colors = buildWaterfallColors(config['waterfall_colors']);
|
||||
if ('waterfall_levels' in config) {
|
||||
waterfall_min_level_default = config['waterfall_levels']['min'];
|
||||
waterfall_max_level_default = config['waterfall_levels']['max'];
|
||||
}
|
||||
if ('waterfall_auto_levels' in config)
|
||||
waterfall_auto_levels = config['waterfall_auto_levels'];
|
||||
if ('waterfall_auto_min_range' in config)
|
||||
waterfall_auto_min_range = config['waterfall_auto_min_range'];
|
||||
waterfallColorsDefault();
|
||||
|
||||
var initial_demodulator_params = {
|
||||
mod: config['start_mod'],
|
||||
offset_frequency: config['start_offset_freq'],
|
||||
squelch_level: Number.isInteger(config['initial_squelch_level']) ? config['initial_squelch_level'] : -150
|
||||
};
|
||||
var initial_demodulator_params = {};
|
||||
if ('start_mod' in config)
|
||||
initial_demodulator_params['mod'] = config['start_mod'];
|
||||
if ('start_offset_freq' in config)
|
||||
initial_demodulator_params['offset_frequency'] = config['start_offset_freq'];
|
||||
if ('initial_squelch_level' in config)
|
||||
initial_demodulator_params['squelch_level'] = Number.isInteger(config['initial_squelch_level']) ? config['initial_squelch_level'] : -150;
|
||||
|
||||
bandwidth = config['samp_rate'];
|
||||
center_freq = config['center_freq'];
|
||||
fft_size = config['fft_size'];
|
||||
var audio_compression = config['audio_compression'];
|
||||
audioEngine.setCompression(audio_compression);
|
||||
divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
|
||||
fft_compression = config['fft_compression'];
|
||||
divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
|
||||
$('#openwebrx-bar-clients').progressbar().setMaxClients(config['max_clients']);
|
||||
if ('samp_rate' in config)
|
||||
bandwidth = config['samp_rate'];
|
||||
if ('center_freq' in config)
|
||||
center_freq = config['center_freq'];
|
||||
if ('fft_size' in config) {
|
||||
fft_size = config['fft_size'];
|
||||
waterfall_clear();
|
||||
}
|
||||
if ('audio_compression' in config) {
|
||||
var audio_compression = config['audio_compression'];
|
||||
audioEngine.setCompression(audio_compression);
|
||||
divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
|
||||
}
|
||||
if ('fft_compression' in config) {
|
||||
fft_compression = config['fft_compression'];
|
||||
divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
|
||||
}
|
||||
if ('max_clients' in config)
|
||||
$('#openwebrx-bar-clients').progressbar().setMaxClients(config['max_clients']);
|
||||
|
||||
waterfall_init();
|
||||
|
||||
var demodulatorPanel = $('#openwebrx-panel-receiver').demodulatorPanel();
|
||||
demodulatorPanel.setCenterFrequency(center_freq);
|
||||
demodulatorPanel.setInitialParams(initial_demodulator_params);
|
||||
if ('squelch_auto_margin' in config)
|
||||
demodulatorPanel.setSquelchMargin(config['squelch_auto_margin']);
|
||||
bookmarks.loadLocalBookmarks();
|
||||
|
||||
waterfall_clear();
|
||||
if ('sdr_id' in config || 'profile_id' in config) {
|
||||
currentprofile['sdr_id'] = config['sdr_id'] || currentprofile['sdr_id'];
|
||||
currentprofile['profile_id'] = config['profile_id'] || currentprofile['profile_id'];
|
||||
$('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString());
|
||||
|
||||
currentprofile = config['sdr_id'] + '|' + config['profile_id'];
|
||||
$('#openwebrx-sdr-profiles-listbox').val(currentprofile);
|
||||
waterfall_clear();
|
||||
}
|
||||
|
||||
if ('tuning_precision' in config)
|
||||
$('#openwebrx-panel-receiver').demodulatorPanel().setTuningPrecision(config['tuning_precision']);
|
||||
|
||||
break;
|
||||
case "secondary_config":
|
||||
var s = json['value'];
|
||||
window.secondary_fft_size = s['secondary_fft_size'];
|
||||
window.secondary_bw = s['secondary_bw'];
|
||||
window.if_samp_rate = s['if_samp_rate'];
|
||||
if ('secondary_fft_size' in s)
|
||||
window.secondary_fft_size = s['secondary_fft_size'];
|
||||
if ('secondary_bw' in s)
|
||||
window.secondary_bw = s['secondary_bw'];
|
||||
if ('if_samp_rate' in s)
|
||||
window.if_samp_rate = s['if_samp_rate'];
|
||||
secondary_demod_init_canvases();
|
||||
break;
|
||||
case "receiver_details":
|
||||
$('#webrx-top-container').header().setDetails(json['value']);
|
||||
$('.webrx-top-container').header().setDetails(json['value']);
|
||||
break;
|
||||
case "smeter":
|
||||
smeter_level = json['value'];
|
||||
@ -755,25 +801,31 @@ function on_ws_recv(evt) {
|
||||
$('#openwebrx-bar-clients').progressbar().setClients(json['value']);
|
||||
break;
|
||||
case "profiles":
|
||||
var listbox = e("openwebrx-sdr-profiles-listbox");
|
||||
listbox.innerHTML = json['value'].map(function (profile) {
|
||||
var listbox = $("#openwebrx-sdr-profiles-listbox");
|
||||
listbox.html(json['value'].map(function (profile) {
|
||||
return '<option value="' + profile['id'] + '">' + profile['name'] + "</option>";
|
||||
}).join("");
|
||||
if (currentprofile) {
|
||||
$('#openwebrx-sdr-profiles-listbox').val(currentprofile);
|
||||
}).join(""));
|
||||
$('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString());
|
||||
// this is a bit hacky since it only makes sense if the error is actually "no sdr devices"
|
||||
// the only other error condition for which the overlay is used right now is "too many users"
|
||||
// so there shouldn't be a problem here
|
||||
if (Object.keys(json['value']).length) {
|
||||
$('#openwebrx-error-overlay').hide();
|
||||
}
|
||||
break;
|
||||
case "features":
|
||||
Modes.setFeatures(json['value']);
|
||||
break;
|
||||
case "metadata":
|
||||
update_metadata(json['value']);
|
||||
$('.openwebrx-meta-panel').metaPanel().each(function(){
|
||||
this.update(json['value']);
|
||||
});
|
||||
break;
|
||||
case "js8_message":
|
||||
$("#openwebrx-panel-js8-message").js8().pushMessage(json['value']);
|
||||
break;
|
||||
case "wsjt_message":
|
||||
update_wsjt_panel(json['value']);
|
||||
$("#openwebrx-panel-wsjt-message").wsjtMessagePanel().pushMessage(json['value']);
|
||||
break;
|
||||
case "dial_frequencies":
|
||||
var as_bookmarks = json['value'].map(function (d) {
|
||||
@ -786,7 +838,7 @@ function on_ws_recv(evt) {
|
||||
bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies');
|
||||
break;
|
||||
case "aprs_data":
|
||||
update_packet_panel(json['value']);
|
||||
$('#openwebrx-panel-packet-message').packetMessagePanel().pushMessage(json['value']);
|
||||
break;
|
||||
case "bookmarks":
|
||||
bookmarks.replace_bookmarks(json['value'], "server");
|
||||
@ -796,6 +848,7 @@ function on_ws_recv(evt) {
|
||||
var $overlay = $('#openwebrx-error-overlay');
|
||||
$overlay.find('.errormessage').text(json['value']);
|
||||
$overlay.show();
|
||||
$("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator();
|
||||
break;
|
||||
case 'secondary_demod':
|
||||
secondary_demod_push_data(json['value']);
|
||||
@ -804,7 +857,7 @@ function on_ws_recv(evt) {
|
||||
divlog(json['value'], true);
|
||||
break;
|
||||
case 'pocsag_data':
|
||||
update_pocsag_panel(json['value']);
|
||||
$('#openwebrx-panel-pocsag-message').pocsagMessagePanel().pushMessage(json['value']);
|
||||
break;
|
||||
case 'backoff':
|
||||
divlog("Server is currently busy: " + json['reason'], true);
|
||||
@ -877,210 +930,6 @@ function on_ws_recv(evt) {
|
||||
}
|
||||
}
|
||||
|
||||
function update_metadata(meta) {
|
||||
var el;
|
||||
if (meta['protocol']) switch (meta['protocol']) {
|
||||
case 'DMR':
|
||||
if (meta['slot']) {
|
||||
el = $("#openwebrx-panel-metadata-dmr").find(".openwebrx-dmr-timeslot-panel").get(meta['slot']);
|
||||
var id = "";
|
||||
var name = "";
|
||||
var target = "";
|
||||
var group = false;
|
||||
$(el)[meta['sync'] ? "addClass" : "removeClass"]("sync");
|
||||
if (meta['sync'] && meta['sync'] === "voice") {
|
||||
id = (meta['additional'] && meta['additional']['callsign']) || meta['source'] || "";
|
||||
name = (meta['additional'] && meta['additional']['fname']) || "";
|
||||
if (meta['type'] === "group") {
|
||||
target = "Talkgroup: ";
|
||||
group = true;
|
||||
}
|
||||
if (meta['type'] === "direct") target = "Direct: ";
|
||||
target += meta['target'] || "";
|
||||
$(el).addClass("active");
|
||||
} else {
|
||||
$(el).removeClass("active");
|
||||
}
|
||||
$(el).find(".openwebrx-dmr-id").text(id);
|
||||
$(el).find(".openwebrx-dmr-name").text(name);
|
||||
$(el).find(".openwebrx-dmr-target").text(target);
|
||||
$(el).find(".openwebrx-meta-user-image")[group ? "addClass" : "removeClass"]("group");
|
||||
} else {
|
||||
clear_metadata();
|
||||
}
|
||||
break;
|
||||
case 'YSF':
|
||||
el = $("#openwebrx-panel-metadata-ysf");
|
||||
|
||||
var mode = " ";
|
||||
var source = "";
|
||||
var up = "";
|
||||
var down = "";
|
||||
if (meta['mode'] && meta['mode'] !== "") {
|
||||
mode = "Mode: " + meta['mode'];
|
||||
source = meta['source'] || "";
|
||||
if (meta['lat'] && meta['lon'] && meta['source']) {
|
||||
source = "<a class=\"openwebrx-maps-pin\" href=\"map?callsign=" + meta['source'] + "\" target=\"_blank\"></a>" + source;
|
||||
}
|
||||
up = meta['up'] ? "Up: " + meta['up'] : "";
|
||||
down = meta['down'] ? "Down: " + meta['down'] : "";
|
||||
$(el).find(".openwebrx-meta-slot").addClass("active");
|
||||
} else {
|
||||
$(el).find(".openwebrx-meta-slot").removeClass("active");
|
||||
}
|
||||
$(el).find(".openwebrx-ysf-mode").text(mode);
|
||||
$(el).find(".openwebrx-ysf-source").html(source);
|
||||
$(el).find(".openwebrx-ysf-up").text(up);
|
||||
$(el).find(".openwebrx-ysf-down").text(down);
|
||||
|
||||
break;
|
||||
} else {
|
||||
clear_metadata();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function html_escape(input) {
|
||||
return $('<div/>').text(input).html()
|
||||
}
|
||||
|
||||
function update_wsjt_panel(msg) {
|
||||
var $b = $('#openwebrx-panel-wsjt-message').find('tbody');
|
||||
var t = new Date(msg['timestamp']);
|
||||
var pad = function (i) {
|
||||
return ('' + i).padStart(2, "0");
|
||||
};
|
||||
var linkedmsg = msg['msg'];
|
||||
var matches;
|
||||
if (['FT8', 'JT65', 'JT9', 'FT4'].indexOf(msg['mode']) >= 0) {
|
||||
matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/);
|
||||
if (matches && matches[2] !== 'RR73') {
|
||||
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
|
||||
} else {
|
||||
linkedmsg = html_escape(linkedmsg);
|
||||
}
|
||||
} else if (msg['mode'] === 'WSPR') {
|
||||
matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/);
|
||||
if (matches) {
|
||||
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + html_escape(matches[3]);
|
||||
} else {
|
||||
linkedmsg = html_escape(linkedmsg);
|
||||
}
|
||||
}
|
||||
$b.append($(
|
||||
'<tr data-timestamp="' + msg['timestamp'] + '">' +
|
||||
'<td>' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '</td>' +
|
||||
'<td class="decimal">' + msg['db'] + '</td>' +
|
||||
'<td class="decimal">' + msg['dt'] + '</td>' +
|
||||
'<td class="decimal freq">' + msg['freq'] + '</td>' +
|
||||
'<td class="message">' + linkedmsg + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
$b.scrollTop($b[0].scrollHeight);
|
||||
}
|
||||
|
||||
var digital_removal_interval;
|
||||
|
||||
// remove old wsjt messages in fixed intervals
|
||||
function init_digital_removal_timer() {
|
||||
if (digital_removal_interval) clearInterval(digital_removal_interval);
|
||||
digital_removal_interval = setInterval(function () {
|
||||
['#openwebrx-panel-wsjt-message', '#openwebrx-panel-packet-message'].forEach(function (root) {
|
||||
var $elements = $(root + ' tbody tr');
|
||||
// limit to 1000 entries in the list since browsers get laggy at some point
|
||||
var toRemove = $elements.length - 1000;
|
||||
if (toRemove <= 0) return;
|
||||
$elements.slice(0, toRemove).remove();
|
||||
});
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
function update_packet_panel(msg) {
|
||||
var $b = $('#openwebrx-panel-packet-message').find('tbody');
|
||||
var pad = function (i) {
|
||||
return ('' + i).padStart(2, "0");
|
||||
};
|
||||
|
||||
if (msg.type && msg.type === 'thirdparty' && msg.data) {
|
||||
msg = msg.data;
|
||||
}
|
||||
var source = msg.source;
|
||||
if (msg.type) {
|
||||
if (msg.type === 'item') {
|
||||
source = msg.item;
|
||||
}
|
||||
if (msg.type === 'object') {
|
||||
source = msg.object;
|
||||
}
|
||||
}
|
||||
|
||||
var timestamp = '';
|
||||
if (msg.timestamp) {
|
||||
var t = new Date(msg.timestamp);
|
||||
timestamp = pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds())
|
||||
}
|
||||
|
||||
var link = '';
|
||||
var classes = [];
|
||||
var styles = {};
|
||||
var overlay = '';
|
||||
var stylesToString = function (s) {
|
||||
return $.map(s, function (value, key) {
|
||||
return key + ':' + value + ';'
|
||||
}).join('')
|
||||
};
|
||||
if (msg.symbol) {
|
||||
classes.push('aprs-symbol');
|
||||
classes.push('aprs-symboltable-' + (msg.symbol.table === '/' ? 'normal' : 'alternate'));
|
||||
styles['background-position-x'] = -(msg.symbol.index % 16) * 15 + 'px';
|
||||
styles['background-position-y'] = -Math.floor(msg.symbol.index / 16) * 15 + 'px';
|
||||
if (msg.symbol.table !== '/' && msg.symbol.table !== '\\') {
|
||||
var s = {};
|
||||
s['background-position-x'] = -(msg.symbol.tableindex % 16) * 15 + 'px';
|
||||
s['background-position-y'] = -Math.floor(msg.symbol.tableindex / 16) * 15 + 'px';
|
||||
overlay = '<div class="aprs-symbol aprs-symboltable-overlay" style="' + stylesToString(s) + '"></div>';
|
||||
}
|
||||
} else if (msg.lat && msg.lon) {
|
||||
classes.push('openwebrx-maps-pin');
|
||||
}
|
||||
var attrs = [
|
||||
'class="' + classes.join(' ') + '"',
|
||||
'style="' + stylesToString(styles) + '"'
|
||||
].join(' ');
|
||||
if (msg.lat && msg.lon) {
|
||||
link = '<a ' + attrs + ' href="map?callsign=' + source + '" target="openwebrx-map">' + overlay + '</a>';
|
||||
} else {
|
||||
link = '<div ' + attrs + '>' + overlay + '</div>'
|
||||
}
|
||||
|
||||
$b.append($(
|
||||
'<tr>' +
|
||||
'<td>' + timestamp + '</td>' +
|
||||
'<td class="callsign">' + source + '</td>' +
|
||||
'<td class="coord">' + link + '</td>' +
|
||||
'<td class="message">' + (msg.comment || msg.message || '') + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
$b.scrollTop($b[0].scrollHeight);
|
||||
}
|
||||
|
||||
function update_pocsag_panel(msg) {
|
||||
var $b = $('#openwebrx-panel-pocsag-message').find('tbody');
|
||||
$b.append($(
|
||||
'<tr>' +
|
||||
'<td class="address">' + msg.address + '</td>' +
|
||||
'<td class="message">' + msg.message + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
$b.scrollTop($b[0].scrollHeight);
|
||||
}
|
||||
|
||||
function clear_metadata() {
|
||||
$(".openwebrx-meta-panel .openwebrx-meta-autoclear").text("");
|
||||
$(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
|
||||
$(".openwebrx-dmr-timeslot-panel").removeClass("muted");
|
||||
}
|
||||
|
||||
var waterfall_measure_minmax_now = false;
|
||||
var waterfall_measure_minmax_continuous = false;
|
||||
|
||||
@ -1125,7 +974,7 @@ function divlog(what, is_error) {
|
||||
what = "<span class=\"webrx-error\">" + what + "</span>";
|
||||
toggle_panel("openwebrx-panel-log", true); //show panel if any error is present
|
||||
}
|
||||
e("openwebrx-debugdiv").innerHTML += what + "<br />";
|
||||
$('#openwebrx-debugdiv')[0].innerHTML += what + "<br />";
|
||||
var nano = $('.nano');
|
||||
nano.nanoScroller();
|
||||
nano.nanoScroller({scroll: 'bottom'});
|
||||
@ -1157,7 +1006,9 @@ function onAudioStart(apiType){
|
||||
var reconnect_timeout = false;
|
||||
|
||||
function on_ws_closed() {
|
||||
$("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator();
|
||||
var demodulatorPanel = $("#openwebrx-panel-receiver").demodulatorPanel();
|
||||
demodulatorPanel.stopDemodulator();
|
||||
demodulatorPanel.resetInitialParams();
|
||||
if (reconnect_timeout) {
|
||||
// max value: roundabout 8 and a half minutes
|
||||
reconnect_timeout = Math.min(reconnect_timeout * 2, 512000);
|
||||
@ -1230,15 +1081,15 @@ var canvas_context;
|
||||
var canvases = [];
|
||||
var canvas_default_height = 200;
|
||||
var canvas_container;
|
||||
var canvas_actual_line;
|
||||
var canvas_actual_line = -1;
|
||||
|
||||
function add_canvas() {
|
||||
var new_canvas = document.createElement("canvas");
|
||||
new_canvas.width = fft_size;
|
||||
new_canvas.height = canvas_default_height;
|
||||
canvas_actual_line = canvas_default_height - 1;
|
||||
new_canvas.openwebrx_top = (-canvas_default_height + 1);
|
||||
new_canvas.style.top = new_canvas.openwebrx_top.toString() + "px";
|
||||
canvas_actual_line = canvas_default_height;
|
||||
new_canvas.openwebrx_top = -canvas_default_height;
|
||||
new_canvas.style.transform = 'translate(0, ' + new_canvas.openwebrx_top.toString() + 'px)';
|
||||
canvas_context = new_canvas.getContext("2d");
|
||||
canvas_container.appendChild(new_canvas);
|
||||
canvases.push(new_canvas);
|
||||
@ -1251,22 +1102,21 @@ function add_canvas() {
|
||||
|
||||
|
||||
function init_canvas_container() {
|
||||
canvas_container = e("webrx-canvas-container");
|
||||
canvas_container = $("#webrx-canvas-container")[0];
|
||||
canvas_container.addEventListener("mouseleave", canvas_container_mouseleave, false);
|
||||
canvas_container.addEventListener("mousemove", canvas_mousemove, false);
|
||||
canvas_container.addEventListener("mouseup", canvas_mouseup, false);
|
||||
canvas_container.addEventListener("mousedown", canvas_mousedown, false);
|
||||
canvas_container.addEventListener("wheel", canvas_mousewheel, false);
|
||||
var frequency_container = e("openwebrx-frequency-container");
|
||||
frequency_container.addEventListener("wheel", canvas_mousewheel, false);
|
||||
add_canvas();
|
||||
var frequency_container = $("#openwebrx-frequency-container");
|
||||
frequency_container.on("wheel", canvas_mousewheel, false);
|
||||
}
|
||||
|
||||
canvas_maxshift = 0;
|
||||
|
||||
function shift_canvases() {
|
||||
canvases.forEach(function (p) {
|
||||
p.style.top = (p.openwebrx_top++).toString() + "px";
|
||||
p.style.transform = 'translate(0, ' + (p.openwebrx_top++).toString() + 'px)';
|
||||
});
|
||||
canvas_maxshift++;
|
||||
}
|
||||
@ -1305,6 +1155,9 @@ function waterfall_add(data) {
|
||||
waterfallColorsContinuous(level);
|
||||
}
|
||||
|
||||
// create new canvas if the current one is full (or there isn't one)
|
||||
if (canvas_actual_line <= 0) add_canvas();
|
||||
|
||||
//Add line to waterfall image
|
||||
var oneline_image = canvas_context.createImageData(w, 1);
|
||||
for (var x = 0; x < w; x++) {
|
||||
@ -1314,18 +1167,17 @@ function waterfall_add(data) {
|
||||
}
|
||||
|
||||
//Draw image
|
||||
canvas_context.putImageData(oneline_image, 0, canvas_actual_line--);
|
||||
canvas_context.putImageData(oneline_image, 0, --canvas_actual_line);
|
||||
shift_canvases();
|
||||
if (canvas_actual_line < 0) add_canvas();
|
||||
}
|
||||
|
||||
function waterfall_clear() {
|
||||
while (canvases.length) //delete all canvases
|
||||
{
|
||||
//delete all canvases
|
||||
while (canvases.length) {
|
||||
var x = canvases.shift();
|
||||
x.parentNode.removeChild(x);
|
||||
}
|
||||
add_canvas();
|
||||
canvas_actual_line = -1;
|
||||
}
|
||||
|
||||
function openwebrx_resize() {
|
||||
@ -1361,12 +1213,20 @@ var audioEngine;
|
||||
|
||||
function openwebrx_init() {
|
||||
audioEngine = new AudioEngine(audio_buffer_maximal_length_sec, audioReporter);
|
||||
$overlay = $('#openwebrx-autoplay-overlay');
|
||||
$overlay.on('click', function(){
|
||||
$('body').on('click', '#openwebrx-autoplay-overlay', function(){
|
||||
audioEngine.resume();
|
||||
});
|
||||
audioEngine.onStart(onAudioStart);
|
||||
if (!audioEngine.isAllowed()) {
|
||||
var $overlay = $(
|
||||
'<div id="openwebrx-autoplay-overlay" class="openwebrx-overlay" style="display:none;">' +
|
||||
'<div class="overlay-content">' +
|
||||
'<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.svg" />' +
|
||||
'<div>Start OpenWebRX</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
$('body').append($overlay);
|
||||
$overlay.show();
|
||||
}
|
||||
fft_codec = new ImaAdpcmCodec();
|
||||
@ -1375,7 +1235,6 @@ function openwebrx_init() {
|
||||
secondary_demod_init();
|
||||
digimodes_init();
|
||||
initPanels();
|
||||
$('.webrx-mouse-freq').frequencyDisplay();
|
||||
$('#openwebrx-panel-receiver').demodulatorPanel();
|
||||
window.addEventListener("resize", openwebrx_resize);
|
||||
bookmarks = new BookmarkBar();
|
||||
@ -1414,6 +1273,8 @@ function digimodes_init() {
|
||||
$(e.currentTarget).toggleClass("muted");
|
||||
update_dmr_timeslot_filtering();
|
||||
});
|
||||
|
||||
$('.openwebrx-meta-panel').metaPanel();
|
||||
}
|
||||
|
||||
function update_dmr_timeslot_filtering() {
|
||||
@ -1444,7 +1305,7 @@ var rt = function (s, n) {
|
||||
// ========================================================
|
||||
|
||||
function panel_displayed(el){
|
||||
return !(el.style && el.style.display && el.style.display === 'none')
|
||||
return !(el.style && el.style.display && el.style.display === 'none') && !(el.movement && el.movement === 'collapse');
|
||||
}
|
||||
|
||||
function toggle_panel(what, on) {
|
||||
@ -1454,14 +1315,13 @@ function toggle_panel(what, on) {
|
||||
if (typeof on !== "undefined" && displayed === on) {
|
||||
return;
|
||||
}
|
||||
if (item.openwebrxDisableClick) return;
|
||||
if (displayed) {
|
||||
item.movement = 'collapse';
|
||||
item.style.transform = "perspective(600px) rotateX(90deg)";
|
||||
item.style.transitionProperty = 'transform';
|
||||
} else {
|
||||
item.movement = 'expand';
|
||||
item.style.display = 'block';
|
||||
item.style.display = null;
|
||||
setTimeout(function(){
|
||||
item.style.transitionProperty = 'transform';
|
||||
item.style.transform = 'perspective(600px) rotateX(0deg)';
|
||||
@ -1469,9 +1329,6 @@ function toggle_panel(what, on) {
|
||||
}
|
||||
item.style.transitionDuration = "600ms";
|
||||
item.style.transitionDelay = "0ms";
|
||||
|
||||
item.openwebrxDisableClick = true;
|
||||
|
||||
}
|
||||
|
||||
function first_show_panel(panel) {
|
||||
@ -1500,13 +1357,13 @@ function initPanels() {
|
||||
el.openwebrxPanelTransparent = (!!el.dataset.panelTransparent);
|
||||
el.addEventListener('transitionend', function(ev){
|
||||
if (ev.target !== el) return;
|
||||
el.openwebrxDisableClick = false;
|
||||
el.style.transitionDuration = null;
|
||||
el.style.transitionDelay = null;
|
||||
el.style.transitionProperty = null;
|
||||
if (el.movement && el.movement === 'collapse') {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
delete el.movement;
|
||||
});
|
||||
if (panel_displayed(el)) first_show_panel(el);
|
||||
});
|
||||
@ -1569,7 +1426,9 @@ function secondary_demod_init_canvases() {
|
||||
}
|
||||
|
||||
function secondary_demod_canvases_update_top() {
|
||||
for (var i = 0; i < 2; i++) secondary_demod_canvases[i].style.top = secondary_demod_canvases[i].openwebrx_top + "px";
|
||||
for (var i = 0; i < 2; i++) {
|
||||
secondary_demod_canvases[i].style.transform = 'translate(0, ' + secondary_demod_canvases[i].openwebrx_top + 'px)';
|
||||
}
|
||||
}
|
||||
|
||||
function secondary_demod_swap_canvases() {
|
||||
@ -1587,7 +1446,10 @@ function secondary_demod_init() {
|
||||
.mousedown(secondary_demod_canvas_container_mousedown)
|
||||
.mouseenter(secondary_demod_canvas_container_mousein)
|
||||
.mouseleave(secondary_demod_canvas_container_mouseleave);
|
||||
init_digital_removal_timer();
|
||||
$('#openwebrx-panel-wsjt-message').wsjtMessagePanel();
|
||||
$('#openwebrx-panel-packet-message').packetMessagePanel();
|
||||
$('#openwebrx-panel-pocsag-message').pocsagMessagePanel();
|
||||
$('#openwebrx-panel-js8-message').js8();
|
||||
}
|
||||
|
||||
function secondary_demod_push_data(x) {
|
||||
|
32
htdocs/pwchange.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenWebRX Password change</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="static/css/login.css" />
|
||||
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
||||
<script src="static/lib/Header.js"></script>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
${header}
|
||||
<div class="login-container">
|
||||
<div class="login">
|
||||
<div class="alert alert-primary">
|
||||
Your password has been automatically generated and must be changed in order to proceed.
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm">Password confirmation</label>
|
||||
<input type="password" class="form-control" id="confirm" name="confirm" placeholder="Password confirmation">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary btn-login">Change password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
@ -1,21 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenWebRX Settings</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
|
||||
<script src="compiled/settings.js"></script>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
${header}
|
||||
<div class="container">
|
||||
<div class="col-12">
|
||||
<h1>SDR device settings</h1>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
${devices}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
@ -11,17 +11,31 @@
|
||||
<body>
|
||||
${header}
|
||||
<div class="container">
|
||||
<div class="col-12">
|
||||
<h1>Settings</h1>
|
||||
<div class="row">
|
||||
<h1 class="col-12">Settings</h1>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="generalsettings">General settings</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="sdrsettings">SDR device settings</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="features">Feature report</a>
|
||||
<div class="row settings-grid">
|
||||
<div class="col-4">
|
||||
<a class="btn btn-secondary" href="settings/general">General settings</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a class="btn btn-secondary" href="settings/sdr">SDR devices and profiles</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a class="btn btn-secondary" href="settings/bookmarks">Bookmark editor</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a class="btn btn-secondary" href="settings/decoding">Demodulation and decoding</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a class="btn btn-secondary" href="settings/backgrounddecoding">Background decoding</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a class="btn btn-secondary" href="settings/reporting">Spotting and reporting</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a class="btn btn-secondary" href="features">Feature report</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
@ -1,25 +1,11 @@
|
||||
$(function(){
|
||||
$(".map-input").each(function(el) {
|
||||
var $el = $(this);
|
||||
var field_id = $el.attr("for");
|
||||
var $lat = $('#' + field_id + '-lat');
|
||||
var $lon = $('#' + field_id + '-lon');
|
||||
$.getScript("https://maps.googleapis.com/maps/api/js?key=" + $el.data("key")).done(function(){
|
||||
$el.css("height", "200px");
|
||||
var lp = new locationPicker($el.get(0), {
|
||||
lat: parseFloat($lat.val()),
|
||||
lng: parseFloat($lon.val())
|
||||
}, {
|
||||
zoom: 7
|
||||
});
|
||||
|
||||
google.maps.event.addListener(lp.map, 'idle', function(event){
|
||||
var pos = lp.getMarkerPosition();
|
||||
$lat.val(pos.lat);
|
||||
$lon.val(pos.lng);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$(".sdrdevice").sdrdevice();
|
||||
$('.map-input').mapInput();
|
||||
$('.imageupload').imageUpload();
|
||||
$('.bookmarks').bookmarktable();
|
||||
$('.wsjt-decoding-depths').wsjtDecodingDepthsInput();
|
||||
$('#waterfall_scheme').waterfallDropdown();
|
||||
$('#rf_gain').gainInput();
|
||||
$('.optional-section').optionalSection();
|
||||
$('#scheduler').schedulerInput();
|
||||
$('.exponential-input').exponentialInput();
|
||||
});
|
69
htdocs/settings/bookmarks.html
Normal file
@ -0,0 +1,69 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenWebRX Settings</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="../static/favicon.ico" />
|
||||
<link rel="stylesheet" href="../static/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../static/css/admin.css" />
|
||||
<script src="../compiled/settings.js"></script>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
${header}
|
||||
<div class="container">
|
||||
${breadcrumb}
|
||||
<div class="row">
|
||||
<h1 class="col-12">Bookmarks</h1>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">Double-click the values in the table to edit them.</div>
|
||||
</div>
|
||||
<div class="row mt-3 bookmarks">
|
||||
${bookmarks}
|
||||
<div class="buttons container">
|
||||
<button type="button" class="btn btn-info bookmark-import">Import personal bookmarks...</button>
|
||||
<button type="button" class="btn btn-primary bookmark-add">Add a new bookmark</button>
|
||||
</div>
|
||||
</div>
|
||||
${breadcrumb}
|
||||
</div>
|
||||
<div class="modal" id="deleteModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5>Please confirm</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Do you really want to delete this bookmark?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger confirm">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" id="importModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5>Import from personal bookmarks</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Please select the bookmarks you would like to import:</p>
|
||||
<div class="bookmark-list"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary confirm">Import</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
23
htdocs/settings/general.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenWebRX Settings</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="${document_root}static/favicon.ico" />
|
||||
<link rel="stylesheet" href="${document_root}static/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="${document_root}static/css/admin.css" />
|
||||
<script src="${document_root}compiled/settings.js"></script>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
${header}
|
||||
<div class="container">
|
||||
${breadcrumb}
|
||||
${error}
|
||||
<div class="row">
|
||||
<h1 class="col-12">${title}</h1>
|
||||
</div>
|
||||
${content}
|
||||
${breadcrumb}
|
||||
</div>
|
||||
${modal}
|
||||
</body>
|
10
openwebrx.conf
Normal file
@ -0,0 +1,10 @@
|
||||
[core]
|
||||
data_directory = /var/lib/openwebrx
|
||||
temporary_directory = /tmp
|
||||
|
||||
[web]
|
||||
port = 8073
|
||||
|
||||
[aprs]
|
||||
# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols)
|
||||
symbols_path = /usr/share/aprs-symbols/png
|
@ -1,25 +1,71 @@
|
||||
import logging
|
||||
|
||||
# the linter will complain about this, but the logging must be configured before importing all the other modules
|
||||
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from http.server import HTTPServer
|
||||
from owrx.http import RequestHandler
|
||||
from owrx.config.core import CoreConfig
|
||||
from owrx.config import Config
|
||||
from owrx.config.commands import MigrateCommand
|
||||
from owrx.feature import FeatureDetector
|
||||
from owrx.sdr import SdrService
|
||||
from socketserver import ThreadingMixIn
|
||||
from owrx.service import Services
|
||||
from owrx.websocket import WebSocketConnection
|
||||
from owrx.pskreporter import PskReporter
|
||||
from owrx.reporting import ReportingEngine
|
||||
from owrx.version import openwebrx_version
|
||||
from owrx.audio.queue import DecoderQueue
|
||||
from owrx.admin import add_admin_parser, run_admin_action
|
||||
import signal
|
||||
import argparse
|
||||
|
||||
|
||||
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
|
||||
pass
|
||||
|
||||
|
||||
class SignalException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def handleSignal(sig, frame):
|
||||
raise SignalException("Received Signal {sig}".format(sig=sig))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="OpenWebRX - Open Source SDR Web App for Everyone!")
|
||||
parser.add_argument("-v", "--version", action="store_true", help="Show the software version")
|
||||
parser.add_argument("--debug", action="store_true", help="Set loglevel to DEBUG")
|
||||
|
||||
moduleparser = parser.add_subparsers(title="Modules", dest="module")
|
||||
adminparser = moduleparser.add_parser("admin", help="Administration actions")
|
||||
add_admin_parser(adminparser)
|
||||
|
||||
configparser = moduleparser.add_parser("config", help="Configuration actions")
|
||||
configcommandparser = configparser.add_subparsers(title="Commands", dest="command")
|
||||
|
||||
migrateparser = configcommandparser.add_parser("migrate", help="Migrate configuration files")
|
||||
migrateparser.set_defaults(cls=MigrateCommand)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# set loglevel to info for CLI commands
|
||||
if args.module is not None and not args.debug:
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
if args.version:
|
||||
print("OpenWebRX version {version}".format(version=openwebrx_version))
|
||||
elif args.module == "admin":
|
||||
run_admin_action(adminparser, args)
|
||||
elif args.module == "config":
|
||||
run_admin_action(configparser, args)
|
||||
else:
|
||||
start_receiver()
|
||||
|
||||
|
||||
def start_receiver():
|
||||
print(
|
||||
"""
|
||||
|
||||
@ -35,16 +81,12 @@ Support and info: https://groups.io/g/openwebrx
|
||||
|
||||
logger.info("OpenWebRX version {0} starting up...".format(openwebrx_version))
|
||||
|
||||
pm = Config.get()
|
||||
for sig in [signal.SIGINT, signal.SIGTERM]:
|
||||
signal.signal(sig, handleSignal)
|
||||
|
||||
configErrors = Config.validateConfig()
|
||||
if configErrors:
|
||||
logger.error(
|
||||
"your configuration contains errors. please address the following errors:"
|
||||
)
|
||||
for e in configErrors:
|
||||
logger.error(e)
|
||||
return
|
||||
# config warmup
|
||||
Config.validateConfig()
|
||||
coreConfig = CoreConfig()
|
||||
|
||||
featureDetector = FeatureDetector()
|
||||
if not featureDetector.is_available("core"):
|
||||
@ -56,14 +98,18 @@ Support and info: https://groups.io/g/openwebrx
|
||||
return
|
||||
|
||||
# Get error messages about unknown / unavailable features as soon as possible
|
||||
SdrService.loadProps()
|
||||
# start up "always-on" sources right away
|
||||
SdrService.getAllSources()
|
||||
|
||||
Services.start()
|
||||
|
||||
try:
|
||||
server = ThreadedHttpServer(("0.0.0.0", pm["web_port"]), RequestHandler)
|
||||
server = ThreadedHttpServer(("0.0.0.0", coreConfig.get_web_port()), RequestHandler)
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
WebSocketConnection.closeAll()
|
||||
Services.stop()
|
||||
PskReporter.stop()
|
||||
except SignalException:
|
||||
pass
|
||||
|
||||
WebSocketConnection.closeAll()
|
||||
Services.stop()
|
||||
ReportingEngine.stopAll()
|
||||
DecoderQueue.stopAll()
|
||||
|
60
owrx/admin/__init__.py
Normal file
@ -0,0 +1,60 @@
|
||||
from owrx.admin.commands import NewUser, DeleteUser, ResetPassword, ListUsers, DisableUser, EnableUser, HasUser
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
def add_admin_parser(moduleparser):
|
||||
subparsers = moduleparser.add_subparsers(title="Commands", dest="command")
|
||||
|
||||
adduser_parser = subparsers.add_parser("adduser", help="Add a new user")
|
||||
adduser_parser.add_argument("user", help="Username to be added")
|
||||
adduser_parser.set_defaults(cls=NewUser)
|
||||
|
||||
removeuser_parser = subparsers.add_parser("removeuser", help="Remove an existing user")
|
||||
removeuser_parser.add_argument("user", help="Username to be remvoed")
|
||||
removeuser_parser.set_defaults(cls=DeleteUser)
|
||||
|
||||
resetpassword_parser = subparsers.add_parser("resetpassword", help="Reset a user's password")
|
||||
resetpassword_parser.add_argument("user", help="Username to be remvoed")
|
||||
resetpassword_parser.set_defaults(cls=ResetPassword)
|
||||
|
||||
listusers_parser = subparsers.add_parser("listusers", help="List enabled users")
|
||||
listusers_parser.add_argument("-a", "--all", action="store_true", help="Show all users (including disabled ones)")
|
||||
listusers_parser.set_defaults(cls=ListUsers)
|
||||
|
||||
disableuser_parser = subparsers.add_parser("disableuser", help="Disable a user")
|
||||
disableuser_parser.add_argument("user", help="Username to be disabled")
|
||||
disableuser_parser.set_defaults(cls=DisableUser)
|
||||
|
||||
enableuser_parser = subparsers.add_parser("enableuser", help="Enable a user")
|
||||
enableuser_parser.add_argument("user", help="Username to be enabled")
|
||||
enableuser_parser.set_defaults(cls=EnableUser)
|
||||
|
||||
hasuser_parser = subparsers.add_parser("hasuser", help="Test if a user exists")
|
||||
hasuser_parser.add_argument("user", help="Username to be checked")
|
||||
hasuser_parser.set_defaults(cls=HasUser)
|
||||
|
||||
moduleparser.add_argument(
|
||||
"--noninteractive", action="store_true", help="Don't ask for any user input (useful for automation)"
|
||||
)
|
||||
moduleparser.add_argument("--silent", action="store_true", help="Ignore errors (useful for automation)")
|
||||
|
||||
|
||||
def run_admin_action(parser, args):
|
||||
if hasattr(args, "cls"):
|
||||
command = args.cls()
|
||||
else:
|
||||
if not hasattr(args, "silent") or not args.silent:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
command.run(args)
|
||||
except Exception:
|
||||
if not hasattr(args, "silent") or not args.silent:
|
||||
print("Error running command:")
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
115
owrx/admin/commands.py
Normal file
@ -0,0 +1,115 @@
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
from getpass import getpass
|
||||
from owrx.users import UserList, User, DefaultPasswordClass
|
||||
import sys
|
||||
import random
|
||||
import string
|
||||
import os
|
||||
|
||||
|
||||
class Command(ABC):
|
||||
@abstractmethod
|
||||
def run(self, args):
|
||||
pass
|
||||
|
||||
|
||||
class UserCommand(Command, metaclass=ABCMeta):
|
||||
def getPassword(self, args, username):
|
||||
if args.noninteractive:
|
||||
if "OWRX_PASSWORD" in os.environ:
|
||||
password = os.environ["OWRX_PASSWORD"]
|
||||
generated = False
|
||||
else:
|
||||
print("Generating password for user {username}...".format(username=username))
|
||||
password = self.getRandomPassword()
|
||||
generated = True
|
||||
print('Password for {username} is "{password}".'.format(username=username, password=password))
|
||||
print('This password is suitable for initial setup only, you will be asked to reset it on initial use.')
|
||||
print('This password cannot be recovered from the system, please copy it now.')
|
||||
else:
|
||||
password = getpass("Please enter the new password for {username}: ".format(username=username))
|
||||
confirm = getpass("Please confirm the new password: ")
|
||||
if password != confirm:
|
||||
print("ERROR: Password mismatch.")
|
||||
sys.exit(1)
|
||||
generated = False
|
||||
return password, generated
|
||||
|
||||
def getRandomPassword(self, length=10):
|
||||
printable = list(string.ascii_letters) + list(string.digits)
|
||||
return ''.join(random.choices(printable, k=length))
|
||||
|
||||
|
||||
class NewUser(UserCommand):
|
||||
def run(self, args):
|
||||
username = args.user
|
||||
userList = UserList()
|
||||
# early test to bypass the password stuff if the user already exists
|
||||
if username in userList:
|
||||
raise KeyError("User {username} already exists".format(username=username))
|
||||
|
||||
password, generated = self.getPassword(args, username)
|
||||
|
||||
print("Creating user {username}...".format(username=username))
|
||||
user = User(name=username, enabled=True, password=DefaultPasswordClass(password), must_change_password=generated)
|
||||
userList.addUser(user)
|
||||
|
||||
|
||||
class DeleteUser(UserCommand):
|
||||
def run(self, args):
|
||||
username = args.user
|
||||
print("Deleting user {username}...".format(username=username))
|
||||
userList = UserList()
|
||||
userList.deleteUser(username)
|
||||
|
||||
|
||||
class ResetPassword(UserCommand):
|
||||
def run(self, args):
|
||||
username = args.user
|
||||
password, generated = self.getPassword(args, username)
|
||||
userList = UserList()
|
||||
userList[username].setPassword(DefaultPasswordClass(password), must_change_password=generated)
|
||||
# this is a change to an object in the list, not the list itself
|
||||
# in this case, store() is explicit
|
||||
userList.store()
|
||||
|
||||
|
||||
class DisableUser(UserCommand):
|
||||
def run(self, args):
|
||||
username = args.user
|
||||
userList = UserList()
|
||||
userList[username].disable()
|
||||
userList.store()
|
||||
|
||||
|
||||
class EnableUser(UserCommand):
|
||||
def run(self, args):
|
||||
username = args.user
|
||||
userList = UserList()
|
||||
userList[username].enable()
|
||||
userList.store()
|
||||
|
||||
|
||||
class ListUsers(Command):
|
||||
def run(self, args):
|
||||
userList = UserList()
|
||||
print("List of enabled users:")
|
||||
for u in userList.values():
|
||||
if args.all or u.enabled:
|
||||
print(" {name}".format(name=u.name))
|
||||
|
||||
|
||||
class HasUser(Command):
|
||||
"""
|
||||
internal command used by the debian config scripts to test if the admin user has already been created
|
||||
"""
|
||||
def run(self, args):
|
||||
userList = UserList()
|
||||
if args.user in userList:
|
||||
if not args.silent:
|
||||
print('User "{name}" exists.'.format(name=args.user))
|
||||
else:
|
||||
if not args.silent:
|
||||
print('User "{name}" does not exist.'.format(name=args.user))
|
||||
# in bash, a return code > 0 is interpreted as "false"
|
||||
sys.exit(1)
|
270
owrx/audio.py
@ -1,270 +0,0 @@
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
from owrx.config import Config
|
||||
from owrx.metrics import Metrics, CounterMetric, DirectMetric
|
||||
import threading
|
||||
import wave
|
||||
import subprocess
|
||||
import os
|
||||
from multiprocessing.connection import Pipe, wait
|
||||
from datetime import datetime, timedelta
|
||||
from queue import Queue, Full
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class QueueJob(object):
|
||||
def __init__(self, decoder, file, freq):
|
||||
self.decoder = decoder
|
||||
self.file = file
|
||||
self.freq = freq
|
||||
|
||||
def run(self):
|
||||
self.decoder.decode(self)
|
||||
|
||||
def unlink(self):
|
||||
try:
|
||||
os.unlink(self.file)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
class QueueWorker(threading.Thread):
|
||||
def __init__(self, queue):
|
||||
self.queue = queue
|
||||
self.doRun = True
|
||||
super().__init__(daemon=True)
|
||||
|
||||
def run(self) -> None:
|
||||
while self.doRun:
|
||||
job = self.queue.get()
|
||||
try:
|
||||
job.run()
|
||||
except Exception:
|
||||
logger.exception("failed to decode job")
|
||||
self.queue.onError()
|
||||
finally:
|
||||
job.unlink()
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
|
||||
class DecoderQueue(Queue):
|
||||
sharedInstance = None
|
||||
creationLock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
with DecoderQueue.creationLock:
|
||||
if DecoderQueue.sharedInstance is None:
|
||||
pm = Config.get()
|
||||
DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"])
|
||||
return DecoderQueue.sharedInstance
|
||||
|
||||
def __init__(self, maxsize, workers):
|
||||
super().__init__(maxsize)
|
||||
metrics = Metrics.getSharedInstance()
|
||||
metrics.addMetric("decoding.queue.length", DirectMetric(self.qsize))
|
||||
self.inCounter = CounterMetric()
|
||||
metrics.addMetric("decoding.queue.in", self.inCounter)
|
||||
self.outCounter = CounterMetric()
|
||||
metrics.addMetric("decoding.queue.out", self.outCounter)
|
||||
self.overflowCounter = CounterMetric()
|
||||
metrics.addMetric("decoding.queue.overflow", self.overflowCounter)
|
||||
self.errorCounter = CounterMetric()
|
||||
metrics.addMetric("decoding.queue.error", self.errorCounter)
|
||||
self.workers = [self.newWorker() for _ in range(0, workers)]
|
||||
|
||||
def put(self, item, **kwars):
|
||||
self.inCounter.inc()
|
||||
try:
|
||||
super(DecoderQueue, self).put(item, block=False)
|
||||
except Full:
|
||||
self.overflowCounter.inc()
|
||||
raise
|
||||
|
||||
def get(self, **kwargs):
|
||||
# super.get() is blocking, so it would mess up the stats to inc() first
|
||||
out = super(DecoderQueue, self).get(**kwargs)
|
||||
self.outCounter.inc()
|
||||
return out
|
||||
|
||||
def newWorker(self):
|
||||
worker = QueueWorker(self)
|
||||
worker.start()
|
||||
return worker
|
||||
|
||||
def onError(self):
|
||||
self.errorCounter.inc()
|
||||
|
||||
|
||||
class AudioChopperProfile(ABC):
|
||||
@abstractmethod
|
||||
def getInterval(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def getFileTimestampFormat(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def decoder_commandline(self, file):
|
||||
pass
|
||||
|
||||
|
||||
class AudioWriter(object):
|
||||
def __init__(self, dsp, source, profile: AudioChopperProfile):
|
||||
self.dsp = dsp
|
||||
self.source = source
|
||||
self.profile = profile
|
||||
self.tmp_dir = Config.get()["temporary_directory"]
|
||||
self.wavefile = None
|
||||
self.wavefilename = None
|
||||
self.switchingLock = threading.Lock()
|
||||
self.timer = None
|
||||
(self.outputReader, self.outputWriter) = Pipe()
|
||||
|
||||
def getWaveFile(self):
|
||||
filename = "{tmp_dir}/openwebrx-audiochopper-{id}-{timestamp}.wav".format(
|
||||
tmp_dir=self.tmp_dir,
|
||||
id=id(self),
|
||||
timestamp=datetime.utcnow().strftime(self.profile.getFileTimestampFormat()),
|
||||
)
|
||||
wavefile = wave.open(filename, "wb")
|
||||
wavefile.setnchannels(1)
|
||||
wavefile.setsampwidth(2)
|
||||
wavefile.setframerate(12000)
|
||||
return filename, wavefile
|
||||
|
||||
def getNextDecodingTime(self):
|
||||
t = datetime.utcnow()
|
||||
zeroed = t.replace(minute=0, second=0, microsecond=0)
|
||||
delta = t - zeroed
|
||||
interval = self.profile.getInterval()
|
||||
seconds = (int(delta.total_seconds() / interval) + 1) * interval
|
||||
t = zeroed + timedelta(seconds=seconds)
|
||||
logger.debug("scheduling: {0}".format(t))
|
||||
return t
|
||||
|
||||
def cancelTimer(self):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
self.timer = None
|
||||
|
||||
def _scheduleNextSwitch(self):
|
||||
self.cancelTimer()
|
||||
delta = self.getNextDecodingTime() - datetime.utcnow()
|
||||
self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
|
||||
self.timer.start()
|
||||
|
||||
def switchFiles(self):
|
||||
self.switchingLock.acquire()
|
||||
file = self.wavefile
|
||||
filename = self.wavefilename
|
||||
(self.wavefilename, self.wavefile) = self.getWaveFile()
|
||||
self.switchingLock.release()
|
||||
|
||||
file.close()
|
||||
job = QueueJob(self, filename, self.dsp.get_operating_freq())
|
||||
try:
|
||||
DecoderQueue.getSharedInstance().put(job)
|
||||
except Full:
|
||||
logger.warning("decoding queue overflow; dropping one file")
|
||||
job.unlink()
|
||||
self._scheduleNextSwitch()
|
||||
|
||||
def decode(self, job: QueueJob):
|
||||
logger.debug("processing file %s", job.file)
|
||||
decoder = subprocess.Popen(
|
||||
["nice", "-n", "10"] + self.profile.decoder_commandline(job.file),
|
||||
stdout=subprocess.PIPE,
|
||||
cwd=self.tmp_dir,
|
||||
close_fds=True,
|
||||
)
|
||||
try:
|
||||
for line in decoder.stdout:
|
||||
self.outputWriter.send((job.freq, line))
|
||||
except OSError:
|
||||
decoder.stdout.flush()
|
||||
# TODO uncouple parsing from the output so that decodes can still go to the map and the spotters
|
||||
logger.debug("output has gone away while decoding job.")
|
||||
try:
|
||||
rc = decoder.wait(timeout=10)
|
||||
if rc != 0:
|
||||
logger.warning("decoder return code: %i", rc)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
|
||||
decoder.kill()
|
||||
|
||||
def start(self):
|
||||
(self.wavefilename, self.wavefile) = self.getWaveFile()
|
||||
self._scheduleNextSwitch()
|
||||
|
||||
def write(self, data):
|
||||
self.switchingLock.acquire()
|
||||
self.wavefile.writeframes(data)
|
||||
self.switchingLock.release()
|
||||
|
||||
def stop(self):
|
||||
self.outputWriter.close()
|
||||
self.outputWriter = None
|
||||
|
||||
# drain messages left in the queue so that the queue can be successfully closed
|
||||
# this is necessary since python keeps the file descriptors open otherwise
|
||||
try:
|
||||
while True:
|
||||
self.outputReader.recv()
|
||||
except EOFError:
|
||||
pass
|
||||
self.outputReader.close()
|
||||
self.outputReader = None
|
||||
|
||||
self.cancelTimer()
|
||||
try:
|
||||
self.wavefile.close()
|
||||
except Exception:
|
||||
logger.exception("error closing wave file")
|
||||
try:
|
||||
os.unlink(self.wavefilename)
|
||||
except Exception:
|
||||
logger.exception("error removing undecoded file")
|
||||
self.wavefile = None
|
||||
self.wavefilename = None
|
||||
|
||||
|
||||
class AudioChopper(threading.Thread, metaclass=ABCMeta):
|
||||
def __init__(self, dsp, source, *profiles: AudioChopperProfile):
|
||||
self.source = source
|
||||
self.writers = [AudioWriter(dsp, source, p) for p in profiles]
|
||||
self.doRun = True
|
||||
super().__init__()
|
||||
|
||||
def run(self) -> None:
|
||||
logger.debug("Audio chopper starting up")
|
||||
for w in self.writers:
|
||||
w.start()
|
||||
while self.doRun:
|
||||
data = None
|
||||
try:
|
||||
data = self.source.read(256)
|
||||
except ValueError:
|
||||
pass
|
||||
if data is None or (isinstance(data, bytes) and len(data) == 0):
|
||||
self.doRun = False
|
||||
else:
|
||||
for w in self.writers:
|
||||
w.write(data)
|
||||
|
||||
logger.debug("Audio chopper shutting down")
|
||||
for w in self.writers:
|
||||
w.stop()
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
readers = wait([w.outputReader for w in self.writers])
|
||||
return [r.recv() for r in readers]
|
||||
except (EOFError, OSError):
|
||||
return None
|
86
owrx/audio/__init__.py
Normal file
@ -0,0 +1,86 @@
|
||||
from owrx.config import Config
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
from typing import List
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AudioChopperProfile(ABC):
|
||||
@abstractmethod
|
||||
def getInterval(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def getFileTimestampFormat(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def decoder_commandline(self, file):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileSourceSubscriber(ABC):
|
||||
@abstractmethod
|
||||
def onProfilesChanged(self):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileSource(ABC):
|
||||
def __init__(self):
|
||||
self.subscribers = []
|
||||
|
||||
@abstractmethod
|
||||
def getProfiles(self) -> List[AudioChopperProfile]:
|
||||
pass
|
||||
|
||||
def subscribe(self, subscriber: ProfileSourceSubscriber):
|
||||
if subscriber in self.subscribers:
|
||||
return
|
||||
self.subscribers.append(subscriber)
|
||||
|
||||
def unsubscribe(self, subscriber: ProfileSourceSubscriber):
|
||||
if subscriber not in self.subscribers:
|
||||
return
|
||||
self.subscribers.remove(subscriber)
|
||||
|
||||
def fireProfilesChanged(self):
|
||||
for sub in self.subscribers.copy():
|
||||
try:
|
||||
sub.onProfilesChanged()
|
||||
except Exception:
|
||||
logger.exception("Error while notifying profile subscriptions")
|
||||
|
||||
|
||||
class ConfigWiredProfileSource(ProfileSource, metaclass=ABCMeta):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.configSub = None
|
||||
|
||||
@abstractmethod
|
||||
def getPropertiesToWire(self) -> List[str]:
|
||||
pass
|
||||
|
||||
def subscribe(self, subscriber: ProfileSourceSubscriber):
|
||||
super().subscribe(subscriber)
|
||||
if self.subscribers and self.configSub is None:
|
||||
self.configSub = Config.get().filter(*self.getPropertiesToWire()).wire(self.fireProfilesChanged)
|
||||
|
||||
def unsubscribe(self, subscriber: ProfileSourceSubscriber):
|
||||
super().unsubscribe(subscriber)
|
||||
if not self.subscribers and self.configSub is not None:
|
||||
self.configSub.cancel()
|
||||
self.configSub = None
|
||||
|
||||
def fireProfilesChanged(self, *args):
|
||||
super().fireProfilesChanged()
|
||||
|
||||
|
||||
class StaticProfileSource(ProfileSource):
|
||||
def __init__(self, profiles: List[AudioChopperProfile]):
|
||||
super().__init__()
|
||||
self.profiles = profiles
|
||||
|
||||
def getProfiles(self) -> List[AudioChopperProfile]:
|
||||
return self.profiles
|
90
owrx/audio/chopper.py
Normal file
@ -0,0 +1,90 @@
|
||||
from owrx.modes import Modes, AudioChopperMode
|
||||
from csdr.output import Output
|
||||
from itertools import groupby
|
||||
import threading
|
||||
from owrx.audio import ProfileSourceSubscriber
|
||||
from owrx.audio.wav import AudioWriter
|
||||
from multiprocessing.connection import Pipe
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class AudioChopper(threading.Thread, Output, ProfileSourceSubscriber):
|
||||
def __init__(self, active_dsp, mode_str: str):
|
||||
self.read_fn = None
|
||||
self.doRun = True
|
||||
self.dsp = active_dsp
|
||||
self.writers = []
|
||||
mode = Modes.findByModulation(mode_str)
|
||||
if mode is None or not isinstance(mode, AudioChopperMode):
|
||||
raise ValueError("Mode {} is not an audio chopper mode".format(mode_str))
|
||||
self.profile_source = mode.get_profile_source()
|
||||
(self.outputReader, self.outputWriter) = Pipe()
|
||||
super().__init__()
|
||||
|
||||
def stop_writers(self):
|
||||
while self.writers:
|
||||
self.writers.pop().stop()
|
||||
|
||||
def setup_writers(self):
|
||||
self.stop_writers()
|
||||
sorted_profiles = sorted(self.profile_source.getProfiles(), key=lambda p: p.getInterval())
|
||||
groups = {interval: list(group) for interval, group in groupby(sorted_profiles, key=lambda p: p.getInterval())}
|
||||
writers = [
|
||||
AudioWriter(self.dsp, self.outputWriter, interval, profiles) for interval, profiles in groups.items()
|
||||
]
|
||||
for w in writers:
|
||||
w.start()
|
||||
self.writers = writers
|
||||
|
||||
def supports_type(self, t):
|
||||
return t == "audio"
|
||||
|
||||
def receive_output(self, t, read_fn):
|
||||
self.read_fn = read_fn
|
||||
self.start()
|
||||
|
||||
def run(self) -> None:
|
||||
logger.debug("Audio chopper starting up")
|
||||
self.setup_writers()
|
||||
self.profile_source.subscribe(self)
|
||||
while self.doRun:
|
||||
data = None
|
||||
try:
|
||||
data = self.read_fn(256)
|
||||
except ValueError:
|
||||
pass
|
||||
if data is None or (isinstance(data, bytes) and len(data) == 0):
|
||||
self.doRun = False
|
||||
else:
|
||||
for w in self.writers:
|
||||
w.write(data)
|
||||
|
||||
logger.debug("Audio chopper shutting down")
|
||||
self.profile_source.unsubscribe(self)
|
||||
self.stop_writers()
|
||||
self.outputWriter.close()
|
||||
self.outputWriter = None
|
||||
|
||||
# drain messages left in the queue so that the queue can be successfully closed
|
||||
# this is necessary since python keeps the file descriptors open otherwise
|
||||
try:
|
||||
while True:
|
||||
self.outputReader.recv()
|
||||
except EOFError:
|
||||
pass
|
||||
self.outputReader.close()
|
||||
self.outputReader = None
|
||||
|
||||
def onProfilesChanged(self):
|
||||
logger.debug("profile change received, resetting writers...")
|
||||
self.setup_writers()
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
return self.outputReader.recv()
|
||||
except (EOFError, OSError):
|
||||
return None
|
172
owrx/audio/queue.py
Normal file
@ -0,0 +1,172 @@
|
||||
from owrx.config import Config
|
||||
from owrx.config.core import CoreConfig
|
||||
from owrx.metrics import Metrics, CounterMetric, DirectMetric
|
||||
from queue import Queue, Full, Empty
|
||||
import subprocess
|
||||
import os
|
||||
import threading
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class QueueJob(object):
|
||||
def __init__(self, profile, writer, file, freq):
|
||||
self.profile = profile
|
||||
self.writer = writer
|
||||
self.file = file
|
||||
self.freq = freq
|
||||
|
||||
def run(self):
|
||||
logger.debug("processing file %s", self.file)
|
||||
tmp_dir = CoreConfig().get_temporary_directory()
|
||||
decoder = subprocess.Popen(
|
||||
["nice", "-n", "10"] + self.profile.decoder_commandline(self.file),
|
||||
stdout=subprocess.PIPE,
|
||||
cwd=tmp_dir,
|
||||
close_fds=True,
|
||||
)
|
||||
try:
|
||||
for line in decoder.stdout:
|
||||
self.writer.send((self.profile, self.freq, line))
|
||||
except (OSError, AttributeError):
|
||||
decoder.stdout.flush()
|
||||
# TODO uncouple parsing from the output so that decodes can still go to the map and the spotters
|
||||
logger.debug("output has gone away while decoding job.")
|
||||
try:
|
||||
rc = decoder.wait(timeout=10)
|
||||
if rc != 0:
|
||||
raise RuntimeError("decoder return code: {0}".format(rc))
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
|
||||
decoder.kill()
|
||||
raise
|
||||
|
||||
def unlink(self):
|
||||
try:
|
||||
os.unlink(self.file)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
PoisonPill = object()
|
||||
|
||||
|
||||
class QueueWorker(threading.Thread):
|
||||
def __init__(self, queue):
|
||||
self.queue = queue
|
||||
self.doRun = True
|
||||
super().__init__()
|
||||
|
||||
def run(self) -> None:
|
||||
while self.doRun:
|
||||
job = self.queue.get()
|
||||
if job is PoisonPill:
|
||||
self.stop()
|
||||
else:
|
||||
try:
|
||||
job.run()
|
||||
except Exception:
|
||||
logger.exception("failed to decode job")
|
||||
self.queue.onError()
|
||||
finally:
|
||||
job.unlink()
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
def stop(self):
|
||||
self.doRun = False
|
||||
|
||||
|
||||
class DecoderQueue(Queue):
|
||||
sharedInstance = None
|
||||
creationLock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
with DecoderQueue.creationLock:
|
||||
if DecoderQueue.sharedInstance is None:
|
||||
DecoderQueue.sharedInstance = DecoderQueue()
|
||||
return DecoderQueue.sharedInstance
|
||||
|
||||
@staticmethod
|
||||
def stopAll():
|
||||
with DecoderQueue.creationLock:
|
||||
if DecoderQueue.sharedInstance is not None:
|
||||
DecoderQueue.sharedInstance.stop()
|
||||
DecoderQueue.sharedInstance = None
|
||||
|
||||
def __init__(self):
|
||||
pm = Config.get()
|
||||
super().__init__(pm["decoding_queue_length"])
|
||||
self.workers = []
|
||||
self._setWorkers(pm["decoding_queue_workers"])
|
||||
self.subscriptions = [
|
||||
pm.wireProperty("decoding_queue_length", self._setMaxSize),
|
||||
pm.wireProperty("decoding_queue_workers", self._setWorkers),
|
||||
]
|
||||
metrics = Metrics.getSharedInstance()
|
||||
metrics.addMetric("decoding.queue.length", DirectMetric(self.qsize))
|
||||
self.inCounter = CounterMetric()
|
||||
metrics.addMetric("decoding.queue.in", self.inCounter)
|
||||
self.outCounter = CounterMetric()
|
||||
metrics.addMetric("decoding.queue.out", self.outCounter)
|
||||
self.overflowCounter = CounterMetric()
|
||||
metrics.addMetric("decoding.queue.overflow", self.overflowCounter)
|
||||
self.errorCounter = CounterMetric()
|
||||
metrics.addMetric("decoding.queue.error", self.errorCounter)
|
||||
|
||||
def _setMaxSize(self, size):
|
||||
if self.maxsize == size:
|
||||
return
|
||||
self.maxsize = size
|
||||
|
||||
def _setWorkers(self, workers):
|
||||
while len(self.workers) > workers:
|
||||
logger.debug("stopping one worker")
|
||||
self.workers.pop().stop()
|
||||
while len(self.workers) < workers:
|
||||
logger.debug("starting one worker")
|
||||
self.workers.append(self.newWorker())
|
||||
|
||||
def stop(self):
|
||||
logger.debug("shutting down the queue")
|
||||
while self.subscriptions:
|
||||
self.subscriptions.pop().cancel()
|
||||
try:
|
||||
# purge all remaining jobs
|
||||
while not self.empty():
|
||||
job = self.get()
|
||||
job.unlink()
|
||||
self.task_done()
|
||||
except Empty:
|
||||
pass
|
||||
# put() a PoisonPill for all active workers to shut them down
|
||||
for w in self.workers:
|
||||
if w.is_alive():
|
||||
self.put(PoisonPill)
|
||||
self.join()
|
||||
|
||||
def put(self, item, **kwargs):
|
||||
self.inCounter.inc()
|
||||
try:
|
||||
super(DecoderQueue, self).put(item, block=False)
|
||||
except Full:
|
||||
self.overflowCounter.inc()
|
||||
raise
|
||||
|
||||
def get(self, **kwargs):
|
||||
# super.get() is blocking, so it would mess up the stats to inc() first
|
||||
out = super(DecoderQueue, self).get(**kwargs)
|
||||
self.outCounter.inc()
|
||||
return out
|
||||
|
||||
def newWorker(self):
|
||||
worker = QueueWorker(self)
|
||||
worker.start()
|
||||
return worker
|
||||
|
||||
def onError(self):
|
||||
self.errorCounter.inc()
|
139
owrx/audio/wav.py
Normal file
@ -0,0 +1,139 @@
|
||||
from owrx.config.core import CoreConfig
|
||||
from owrx.audio import AudioChopperProfile
|
||||
from owrx.audio.queue import QueueJob, DecoderQueue
|
||||
import threading
|
||||
import wave
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from queue import Full
|
||||
from typing import List
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class WaveFile(object):
|
||||
def __init__(self, writer_id):
|
||||
self.timestamp = datetime.utcnow()
|
||||
self.writer_id = writer_id
|
||||
tmp_dir = CoreConfig().get_temporary_directory()
|
||||
self.filename = "{tmp_dir}/openwebrx-audiochopper-master-{id}-{timestamp}.wav".format(
|
||||
tmp_dir=tmp_dir,
|
||||
id=self.writer_id,
|
||||
timestamp=self.timestamp.strftime("%y%m%d_%H%M%S"),
|
||||
)
|
||||
self.waveFile = wave.open(self.filename, "wb")
|
||||
self.waveFile.setnchannels(1)
|
||||
self.waveFile.setsampwidth(2)
|
||||
self.waveFile.setframerate(12000)
|
||||
|
||||
def close(self):
|
||||
self.waveFile.close()
|
||||
|
||||
def getFileName(self):
|
||||
return self.filename
|
||||
|
||||
def getTimestamp(self):
|
||||
return self.timestamp
|
||||
|
||||
def writeframes(self, data):
|
||||
return self.waveFile.writeframes(data)
|
||||
|
||||
def unlink(self):
|
||||
os.unlink(self.filename)
|
||||
self.waveFile = None
|
||||
|
||||
|
||||
class AudioWriter(object):
|
||||
def __init__(self, active_dsp, outputWriter, interval, profiles: List[AudioChopperProfile]):
|
||||
self.dsp = active_dsp
|
||||
self.outputWriter = outputWriter
|
||||
self.interval = interval
|
||||
self.profiles = profiles
|
||||
self.wavefile = None
|
||||
self.switchingLock = threading.Lock()
|
||||
self.timer = None
|
||||
|
||||
def getWaveFile(self):
|
||||
return WaveFile(id(self))
|
||||
|
||||
def getNextDecodingTime(self):
|
||||
# add one second to have the intervals tick over one second earlier
|
||||
# this avoids filename collisions, but also avoids decoding wave files with less than one second of audio
|
||||
t = datetime.utcnow() + timedelta(seconds=1)
|
||||
zeroed = t.replace(minute=0, second=0, microsecond=0)
|
||||
delta = t - zeroed
|
||||
seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval
|
||||
t = zeroed + timedelta(seconds=seconds)
|
||||
logger.debug("scheduling: {0}".format(t))
|
||||
return t
|
||||
|
||||
def cancelTimer(self):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
self.timer = None
|
||||
|
||||
def _scheduleNextSwitch(self):
|
||||
self.cancelTimer()
|
||||
delta = self.getNextDecodingTime() - datetime.utcnow()
|
||||
self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
|
||||
self.timer.start()
|
||||
|
||||
def switchFiles(self):
|
||||
with self.switchingLock:
|
||||
file = self.wavefile
|
||||
self.wavefile = self.getWaveFile()
|
||||
|
||||
file.close()
|
||||
tmp_dir = CoreConfig().get_temporary_directory()
|
||||
|
||||
for profile in self.profiles:
|
||||
# create hardlinks for the individual profiles
|
||||
filename = "{tmp_dir}/openwebrx-audiochopper-{pid}-{timestamp}.wav".format(
|
||||
tmp_dir=tmp_dir,
|
||||
pid=id(profile),
|
||||
timestamp=file.getTimestamp().strftime(profile.getFileTimestampFormat()),
|
||||
)
|
||||
try:
|
||||
os.link(file.getFileName(), filename)
|
||||
except OSError:
|
||||
logger.exception("Error while linking job files")
|
||||
continue
|
||||
|
||||
job = QueueJob(profile, self.outputWriter, filename, self.dsp.get_operating_freq())
|
||||
try:
|
||||
DecoderQueue.getSharedInstance().put(job)
|
||||
except Full:
|
||||
logger.warning("decoding queue overflow; dropping one file")
|
||||
job.unlink()
|
||||
|
||||
try:
|
||||
# our master can be deleted now, the profiles will delete their hardlinked copies after processing
|
||||
file.unlink()
|
||||
except OSError:
|
||||
logger.exception("Error while unlinking job files")
|
||||
|
||||
self._scheduleNextSwitch()
|
||||
|
||||
def start(self):
|
||||
self.wavefile = self.getWaveFile()
|
||||
self._scheduleNextSwitch()
|
||||
|
||||
def write(self, data):
|
||||
with self.switchingLock:
|
||||
self.wavefile.writeframes(data)
|
||||
|
||||
def stop(self):
|
||||
self.cancelTimer()
|
||||
try:
|
||||
self.wavefile.close()
|
||||
except Exception:
|
||||
logger.exception("error closing wave file")
|
||||
try:
|
||||
with self.switchingLock:
|
||||
self.wavefile.unlink()
|
||||
except Exception:
|
||||
logger.exception("error removing undecoded file")
|
||||
self.wavefile = None
|
@ -1,4 +1,7 @@
|
||||
from owrx.modes import Modes
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import os
|
||||
|
||||
import logging
|
||||
|
||||
@ -12,7 +15,15 @@ class Band(object):
|
||||
self.upper_bound = dict["upper_bound"]
|
||||
self.frequencies = []
|
||||
if "frequencies" in dict:
|
||||
availableModes = [mode.modulation for mode in Modes.getAvailableModes()]
|
||||
for (mode, freqs) in dict["frequencies"].items():
|
||||
if mode not in availableModes:
|
||||
logger.info(
|
||||
'Modulation "{mode}" is not available, bandplan bookmark will not be displayed'.format(
|
||||
mode=mode
|
||||
)
|
||||
)
|
||||
continue
|
||||
if not isinstance(freqs, list):
|
||||
freqs = [freqs]
|
||||
for f in freqs:
|
||||
@ -22,8 +33,8 @@ class Band(object):
|
||||
mode=mode, frequency=f, band=self.name
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.frequencies.append({"mode": mode, "frequency": f})
|
||||
continue
|
||||
self.frequencies.append({"mode": mode, "frequency": f})
|
||||
|
||||
def inBand(self, freq):
|
||||
return self.lower_bound <= freq <= self.upper_bound
|
||||
@ -46,10 +57,29 @@ class Bandplan(object):
|
||||
return Bandplan.sharedInstance
|
||||
|
||||
def __init__(self):
|
||||
self.bands = self.loadBands()
|
||||
self.bands = []
|
||||
self.file_modified = None
|
||||
self.fileList = ["/etc/openwebrx/bands.json", "bands.json"]
|
||||
|
||||
def loadBands(self):
|
||||
for file in ["/etc/openwebrx/bands.json", "bands.json"]:
|
||||
def _refresh(self):
|
||||
modified = self._getFileModifiedTimestamp()
|
||||
if self.file_modified is None or modified > self.file_modified:
|
||||
logger.debug("reloading bands from disk due to file modification")
|
||||
self.bands = self._loadBands()
|
||||
self.file_modified = modified
|
||||
|
||||
def _getFileModifiedTimestamp(self):
|
||||
timestamp = 0
|
||||
for file in self.fileList:
|
||||
try:
|
||||
timestamp = os.path.getmtime(file)
|
||||
break
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return datetime.fromtimestamp(timestamp, timezone.utc)
|
||||
|
||||
def _loadBands(self):
|
||||
for file in self.fileList:
|
||||
try:
|
||||
f = open(file, "r")
|
||||
bands_json = json.load(f)
|
||||
@ -66,6 +96,7 @@ class Bandplan(object):
|
||||
return []
|
||||
|
||||
def findBands(self, freq):
|
||||
self._refresh()
|
||||
return [band for band in self.bands if band.inBand(freq)]
|
||||
|
||||
def findBand(self, freq):
|
||||
@ -76,4 +107,5 @@ class Bandplan(object):
|
||||
return None
|
||||
|
||||
def collectDialFrequencies(self, range):
|
||||
self._refresh()
|
||||
return [e for b in self.bands for e in b.getDialFrequencies(range)]
|
||||
|
@ -1,4 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
from owrx.config.core import CoreConfig
|
||||
import json
|
||||
import os
|
||||
|
||||
import logging
|
||||
|
||||
@ -28,6 +31,23 @@ class Bookmark(object):
|
||||
}
|
||||
|
||||
|
||||
class BookmakrSubscription(object):
|
||||
def __init__(self, subscriptee, range, subscriber: callable):
|
||||
self.subscriptee = subscriptee
|
||||
self.range = range
|
||||
self.subscriber = subscriber
|
||||
|
||||
def inRange(self, bookmark: Bookmark):
|
||||
low, high = self.range
|
||||
return low <= bookmark.getFrequency() <= high
|
||||
|
||||
def call(self, *args, **kwargs):
|
||||
self.subscriber(*args, **kwargs)
|
||||
|
||||
def cancel(self):
|
||||
self.subscriptee.unsubscribe(self)
|
||||
|
||||
|
||||
class Bookmarks(object):
|
||||
sharedInstance = None
|
||||
|
||||
@ -38,15 +58,36 @@ class Bookmarks(object):
|
||||
return Bookmarks.sharedInstance
|
||||
|
||||
def __init__(self):
|
||||
self.bookmarks = self.loadBookmarks()
|
||||
self.file_modified = None
|
||||
self.bookmarks = []
|
||||
self.subscriptions = []
|
||||
self.fileList = [Bookmarks._getBookmarksFile(), "/etc/openwebrx/bookmarks.json", "bookmarks.json"]
|
||||
|
||||
def loadBookmarks(self):
|
||||
for file in ["/etc/openwebrx/bookmarks.json", "bookmarks.json"]:
|
||||
def _refresh(self):
|
||||
modified = self._getFileModifiedTimestamp()
|
||||
if self.file_modified is None or modified > self.file_modified:
|
||||
logger.debug("reloading bookmarks from disk due to file modification")
|
||||
self.bookmarks = self._loadBookmarks()
|
||||
self.file_modified = modified
|
||||
|
||||
def _getFileModifiedTimestamp(self):
|
||||
timestamp = 0
|
||||
for file in self.fileList:
|
||||
try:
|
||||
f = open(file, "r")
|
||||
bookmarks_json = json.load(f)
|
||||
f.close()
|
||||
return [Bookmark(d) for d in bookmarks_json]
|
||||
timestamp = os.path.getmtime(file)
|
||||
break
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return datetime.fromtimestamp(timestamp, timezone.utc)
|
||||
|
||||
def _loadBookmarks(self):
|
||||
for file in self.fileList:
|
||||
try:
|
||||
with open(file, "r") as f:
|
||||
content = f.read()
|
||||
if content:
|
||||
bookmarks_json = json.loads(content)
|
||||
return [Bookmark(d) for d in bookmarks_json]
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except json.JSONDecodeError:
|
||||
@ -57,6 +98,48 @@ class Bookmarks(object):
|
||||
return []
|
||||
return []
|
||||
|
||||
def getBookmarks(self, range):
|
||||
(lo, hi) = range
|
||||
return [b for b in self.bookmarks if lo <= b.getFrequency() <= hi]
|
||||
def getBookmarks(self, range=None):
|
||||
self._refresh()
|
||||
if range is None:
|
||||
return self.bookmarks
|
||||
else:
|
||||
(lo, hi) = range
|
||||
return [b for b in self.bookmarks if lo <= b.getFrequency() <= hi]
|
||||
|
||||
@staticmethod
|
||||
def _getBookmarksFile():
|
||||
coreConfig = CoreConfig()
|
||||
return "{data_directory}/bookmarks.json".format(data_directory=coreConfig.get_data_directory())
|
||||
|
||||
def store(self):
|
||||
# don't write directly to file to avoid corruption on exceptions
|
||||
jsonContent = json.dumps([b.__dict__() for b in self.bookmarks], indent=4)
|
||||
with open(Bookmarks._getBookmarksFile(), "w") as file:
|
||||
file.write(jsonContent)
|
||||
self.file_modified = self._getFileModifiedTimestamp()
|
||||
|
||||
def addBookmark(self, bookmark: Bookmark):
|
||||
self.bookmarks.append(bookmark)
|
||||
self.notifySubscriptions(bookmark)
|
||||
|
||||
def removeBookmark(self, bookmark: Bookmark):
|
||||
if bookmark not in self.bookmarks:
|
||||
return
|
||||
self.bookmarks.remove(bookmark)
|
||||
self.notifySubscriptions(bookmark)
|
||||
|
||||
def notifySubscriptions(self, bookmark: Bookmark):
|
||||
for sub in self.subscriptions:
|
||||
if sub.inRange(bookmark):
|
||||
try:
|
||||
sub.call()
|
||||
except Exception:
|
||||
logger.exception("Error while calling bookmark subscriptions")
|
||||
|
||||
def subscribe(self, range, callback):
|
||||
self.subscriptions.append(BookmakrSubscription(self, range, callback))
|
||||
|
||||
def unsubscribe(self, subscriptions: BookmakrSubscription):
|
||||
if subscriptions not in self.subscriptions:
|
||||
return
|
||||
self.subscriptions.remove(subscriptions)
|
||||
|
44
owrx/breadcrumb.py
Normal file
@ -0,0 +1,44 @@
|
||||
from typing import List
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class BreadcrumbItem(object):
|
||||
def __init__(self, title, href):
|
||||
self.title = title
|
||||
self.href = href
|
||||
|
||||
def render(self, documentRoot, active=False):
|
||||
return '<li class="breadcrumb-item {active}"><a href="{documentRoot}{href}">{title}</a></li>'.format(
|
||||
documentRoot=documentRoot, href=self.href, title=self.title, active="active" if active else ""
|
||||
)
|
||||
|
||||
|
||||
class Breadcrumb(object):
|
||||
def __init__(self, breadcrumbs: List[BreadcrumbItem]):
|
||||
self.items = breadcrumbs
|
||||
|
||||
def render(self, documentRoot):
|
||||
return """
|
||||
<ol class="breadcrumb">
|
||||
{crumbs}
|
||||
{last_crumb}
|
||||
</ol>
|
||||
""".format(
|
||||
crumbs="".join(item.render(documentRoot) for item in self.items[:-1]),
|
||||
last_crumb="".join(item.render(documentRoot, True) for item in self.items[-1:]),
|
||||
)
|
||||
|
||||
def append(self, crumb: BreadcrumbItem):
|
||||
self.items.append(crumb)
|
||||
return self
|
||||
|
||||
|
||||
class BreadcrumbMixin(ABC):
|
||||
def template_variables(self):
|
||||
variables = super().template_variables()
|
||||
variables["breadcrumb"] = self.get_breadcrumb().render(self.get_document_root())
|
||||
return variables
|
||||
|
||||
@abstractmethod
|
||||
def get_breadcrumb(self) -> Breadcrumb:
|
||||
pass
|
@ -23,6 +23,7 @@ class ClientRegistry(object):
|
||||
|
||||
def __init__(self):
|
||||
self.clients = []
|
||||
Config.get().wireProperty("max_clients", self._checkClientCount)
|
||||
super().__init__()
|
||||
|
||||
def broadcast(self):
|
||||
@ -46,3 +47,8 @@ class ClientRegistry(object):
|
||||
except ValueError:
|
||||
pass
|
||||
self.broadcast()
|
||||
|
||||
def _checkClientCount(self, new_count):
|
||||
for client in self.clients[new_count:]:
|
||||
logger.debug("closing one connection...")
|
||||
client.close()
|
||||
|