Compare commits
374 Commits
image-2020
...
0.19.0
Author | SHA1 | Date | |
---|---|---|---|
3479148b86 | |||
017ad818ef | |||
09caae2fcc | |||
ae295d72ae | |||
16c59c3245 | |||
ea65ef0100 | |||
379e39aa3e | |||
835501a5f4 | |||
c87cfed525 | |||
ebd1e04414 | |||
1019ed5793 | |||
adcac7b54a | |||
d3a3078504 | |||
ac18a76c14 | |||
66b5f17d38 | |||
9763f302f3 | |||
1359da5b14 | |||
063d22f88c | |||
7681830256 | |||
3371697e18 | |||
bfe6c00f90 | |||
e90973bcd4 | |||
e0648d63ad | |||
564c1e26b6 | |||
27d6802dfc | |||
d2a4f2bc46 | |||
d24abd436e | |||
305adc94fa | |||
d9db693aec | |||
d64f08490a | |||
a982c86794 | |||
6c307d885f | |||
048210d7da | |||
d2be712de8 | |||
3a8256e3bc | |||
385c241858 | |||
a1da591218 | |||
f1d9a4a28c | |||
29b3f530d2 | |||
e1f83727b7 | |||
17f4f671a6 | |||
4b8ef29775 | |||
5377087848 | |||
1fedd0e50f | |||
6cac3b4d39 | |||
d9292587ec | |||
cf4f1dce32 | |||
1299f5e9cc | |||
48b177defa | |||
63475dda78 | |||
9dd7a7e653 | |||
b624bef345 | |||
a03176223a | |||
98cb1a8389 | |||
ddbc844954 | |||
d22ab23771 | |||
0a60b505b8 | |||
2b4799591f | |||
048aab682f | |||
e557d46c0d | |||
10d6309608 | |||
7d41fc8b06 | |||
2483398b0f | |||
a94209a2bc | |||
db7b4f195e | |||
b0f7fd5d00 | |||
96b1de1856 | |||
9366d67218 | |||
8df885b727 | |||
11cf2a96e2 | |||
f62bd8be36 | |||
813474b5d6 | |||
508ea2cf96 | |||
a37e5ac93f | |||
2c1ec7df74 | |||
4971bee67c | |||
eaa41c3256 | |||
5606646064 | |||
59a7842c6d | |||
149ad8dcc6 | |||
3a5e227ab5 | |||
3202f48f8e | |||
3a455a0452 | |||
f2288ceb49 | |||
dba4f91c77 | |||
1f565355ec | |||
af1cfee754 | |||
9563adacf7 | |||
fc7188145b | |||
ceafcbf850 | |||
7fbd024ed5 | |||
66a4f29911 | |||
eab3bf780e | |||
efa9771ad7 | |||
e2cacc1fa0 | |||
93b8f75cc3 | |||
a6a29b7032 | |||
981d3b6673 | |||
8e313517d1 | |||
beed0c1a70 | |||
d98abe42bc | |||
52367e53f5 | |||
acb392e56c | |||
ac136313cb | |||
e92a91663d | |||
26ba8ca999 | |||
e409c37158 | |||
2f2d52df85 | |||
0868e643c9 | |||
1bfe768601 | |||
3405bc485b | |||
6ff1b7d20a | |||
3504c8b54e | |||
e01a12a945 | |||
8c8445eb3b | |||
7a3043559f | |||
54812f0de1 | |||
28c1425a8f | |||
a96690c8bd | |||
e5196c6af9 | |||
19518da2e2 | |||
b956a0dcd6 | |||
20023e3989 | |||
d9a818525d | |||
b8f7686a6d | |||
5013af2117 | |||
02a6326605 | |||
1441b9610c | |||
56f3f089a1 | |||
1764abe65f | |||
33762574c3 | |||
f1dc9af651 | |||
25a7bbd86a | |||
6a8168025d | |||
26321ab68b | |||
449b3b3986 | |||
39f9d4c273 | |||
bb1b561c47 | |||
907787cfdc | |||
e61d3a22a3 | |||
fb90a4e54b | |||
5282b5f8df | |||
9942b3baf2 | |||
b874583931 | |||
2f011ea249 | |||
a4ebf87263 | |||
dd492fa63c | |||
4dc10fb6a3 | |||
4a2b81c793 | |||
e064352621 | |||
b58357741a | |||
2198c00d00 | |||
978eea400d | |||
a828f61c72 | |||
4e67be8a3c | |||
623f21f769 | |||
34838abfa9 | |||
280e39d9c4 | |||
2df56ad8b9 | |||
5ab2f02f63 | |||
0120b33a25 | |||
9622cd6a2a | |||
78ccaa7d65 | |||
4f07c62cc9 | |||
520ddbb034 | |||
0a16500133 | |||
681a583711 | |||
aa4362fe9f | |||
0c12d07a26 | |||
f474ab94d2 | |||
5ba77012a7 | |||
a573fa0b93 | |||
9a86bc23be | |||
c90b415c8b | |||
4287387a5e | |||
32bd1bb4aa | |||
1023087c8a | |||
5843aec342 | |||
f52bf560ec | |||
05a4139f94 | |||
116e20335e | |||
5e6b45eaec | |||
aa38340415 | |||
4d157d275a | |||
70818836de | |||
1f70b93310 | |||
4c604bf400 | |||
7fe694ba0a | |||
eb9059a711 | |||
da4917998d | |||
99b4a25de7 | |||
899445d586 | |||
2de0cbc6c0 | |||
7948d1f27a | |||
bcb8a2315c | |||
ddfd85c586 | |||
0e8715b5a1 | |||
1b2e237816 | |||
6d43126fa5 | |||
3c0146b1c4 | |||
893a56aa83 | |||
f7c9fbcc22 | |||
aa29836039 | |||
c30740c4e3 | |||
d07cbb2b10 | |||
8fdf263e4b | |||
4d67b684e4 | |||
d06e9151b9 | |||
366def0235 | |||
2301141b44 | |||
112eda2021 | |||
d9e15357f3 | |||
70ba0cd618 | |||
78704885d7 | |||
513b477fac | |||
6c3bb0b520 | |||
c2e85ce9a6 | |||
3f742c7b1a | |||
b7831b824a | |||
f0ef5bb371 | |||
29566430a6 | |||
a3126b060d | |||
2ef80eee1d | |||
65a0320cea | |||
199dfe106a | |||
056a8a3289 | |||
1d5f450f74 | |||
7914202df3 | |||
a6b5984dce | |||
fd9e913a49 | |||
2b7d6738f1 | |||
f81e53e455 | |||
3011e62fad | |||
54dc412c4a | |||
0e9bb45d89 | |||
6493fb86c1 | |||
df21a1eed6 | |||
c5a5d25320 | |||
7efe254a66 | |||
d71dc35239 | |||
ab9df41a21 | |||
16639c0b5b | |||
2d86483907 | |||
24a4d03eff | |||
0d93186066 | |||
69b43b40b5 | |||
16d5db00af | |||
b87f7017d1 | |||
8a053f47d4 | |||
895d8019e3 | |||
25755d09dd | |||
a7345bb16f | |||
0bffc2b3dd | |||
14382e012f | |||
0e19a40968 | |||
4aac5c9584 | |||
8a2356580a | |||
4e4266f1c4 | |||
cfea251d60 | |||
d1ef1810bf | |||
25b287344f | |||
f30cf3fecd | |||
236f3d2058 | |||
14634af83c | |||
4b7ac0e299 | |||
cc5c130f49 | |||
d5c2f8414e | |||
c83d8580ba | |||
7562dc8ecb | |||
37e74f9027 | |||
7cae383127 | |||
b25e61ae9a | |||
885d02ceca | |||
b3a5a36d9c | |||
5076f79aaa | |||
9768fa7c50 | |||
92cd65b66f | |||
541c38151f | |||
7948b7bfa1 | |||
05485ba8e3 | |||
2505e95d1c | |||
135e9ae7b9 | |||
8ed6dbe5d1 | |||
752cd42ad7 | |||
fbf74a1286 | |||
55e1a97d43 | |||
8a03951713 | |||
1a1ad670ee | |||
5273131b25 | |||
d74b79f585 | |||
e1af089658 | |||
34ee5d8e3b | |||
68e8a77b1d | |||
edded220b5 | |||
1581c659af | |||
ca5889f925 | |||
6e6861479d | |||
8e87aa0342 | |||
97cb51d990 | |||
d2ce27eeab | |||
00a7b7877c | |||
c387fe0fe9 | |||
fea2cd1cc5 | |||
7742d7a048 | |||
e37e2f4540 | |||
4deb4c781e | |||
5da2047935 | |||
fb82daf936 | |||
ede40e4a68 | |||
3852f28fd4 | |||
c385fd635b | |||
b9ac887eed | |||
a2dc2b3085 | |||
6ab77f958c | |||
4928f80929 | |||
687e504af4 | |||
14b293e0cb | |||
beb59da6a6 | |||
c2702e02a9 | |||
6b4509fca5 | |||
8abfe059b7 | |||
10523dbbd7 | |||
b8c71109b8 | |||
9cc850e578 | |||
0e47f2d92a | |||
fbcfb550a2 | |||
a388acdf03 | |||
d36be799d0 | |||
c325368be8 | |||
388218f9df | |||
6b2656efae | |||
278fab268f | |||
bd8b8ca410 | |||
fb7422e5a8 | |||
a70c51193b | |||
fa75cac7f5 | |||
de3694248a | |||
9f06149ae3 | |||
437e28c3a9 | |||
cad6175db0 | |||
af053b9ac4 | |||
0a20cb5e41 | |||
aa9737498a | |||
42191f4e77 | |||
451eb99f8a | |||
b110705f45 | |||
36e94d4e3c | |||
4e98bbc1c9 | |||
c3b13b224c | |||
5f388fd38d | |||
9bc161c140 | |||
dbb7c0cde3 | |||
52e517dfc3 | |||
37ffb2a02c | |||
91b3713dad | |||
c53ac1aa4f | |||
c4166997be | |||
f0f9455c6e | |||
7bc78425cd | |||
d1dc14d9e5 | |||
521755b9f2 | |||
ad565c5a2b | |||
ebba6e1ada | |||
0b7b5d985f | |||
b948e06a4f | |||
eaa98b0d64 | |||
16b3c11678 | |||
c92929a32d | |||
46c3e5077d | |||
dc12c54ae6 | |||
bdc43455a5 | |||
42eeb00a0f | |||
5951d2a874 | |||
9a5aba7313 |
@ -4,3 +4,4 @@
|
|||||||
**/*.pyc
|
**/*.pyc
|
||||||
**/*.swp
|
**/*.swp
|
||||||
black-env
|
black-env
|
||||||
|
debian
|
32
CHANGELOG.md
32
CHANGELOG.md
@ -1,3 +1,35 @@
|
|||||||
|
**0.19.0**
|
||||||
|
- Fix direwolf connection setup by implementing a retry loop
|
||||||
|
- Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector
|
||||||
|
- OSM maps instead of Google when google_maps_api_key is not set (thanks @jquagga)
|
||||||
|
- Improved logic to pass parameters to soapy devices.
|
||||||
|
- `rtl_sdr_soapy`: added support for `bias_tee`
|
||||||
|
- `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch`
|
||||||
|
- `airspy`: added support for `bitpack`
|
||||||
|
- Added support for Perseus-SDR devices, (thanks @amontefusco)
|
||||||
|
- Property System has been rewritten so that defaults on sdr behave as expected
|
||||||
|
- Waterfall range auto-adjustment now only takes the center 80% of the spectrum into account, which should work better
|
||||||
|
with SDRs that oversample or have rather flat filter curves towards the spectrum edges
|
||||||
|
- Bugfix for negative network usage
|
||||||
|
- FiFi SDR: prevent arecord from shutting down after 2GB of data has been sent
|
||||||
|
- Added support for bias tee control on rtl_sdr devices
|
||||||
|
- All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
|
||||||
|
- `rtl_sdr` type now also supports the `direct_sampling` option
|
||||||
|
- Added decoding implementation for for digimode "JS8Call"
|
||||||
|
(requires an installation of [js8call](http://js8call.com/) and
|
||||||
|
[the js8py library](https://github.com/jketterl/js8py))
|
||||||
|
- Reorganization of the frontend demodulator code
|
||||||
|
- Improve receiver load time by concatenating javascript assets
|
||||||
|
- Docker images migrated to Debian slim images; This was necessary to allow the use of function multiversioning in
|
||||||
|
csdr and owrx_connector to allow the images to run on a wider range of CPUs
|
||||||
|
- Docker containers have been updated to include the SDRplay driver version 3
|
||||||
|
- HackRF support is now based on SoapyHackRF
|
||||||
|
- Removed sdr.hu server listing support since the site has been shut down
|
||||||
|
- Added support for Radioberry 2 Rasbperry Pi SDR Cape
|
||||||
|
|
||||||
|
**0.18.0**
|
||||||
|
- Support for SoapyRemote
|
||||||
|
|
||||||
**2020-02-08**
|
**2020-02-08**
|
||||||
- Compression, resampling and filtering in the frontend have been rewritten in javascript, sdr.js has been removed
|
- Compression, resampling and filtering in the frontend have been rewritten in javascript, sdr.js has been removed
|
||||||
- Decoding of Pocsag modulation is now possible
|
- Decoding of Pocsag modulation is now possible
|
||||||
|
63
README.md
63
README.md
@ -3,7 +3,7 @@ OpenWebRX
|
|||||||
|
|
||||||
OpenWebRX is a multi-user SDR receiver software with a web interface.
|
OpenWebRX is a multi-user SDR receiver software with a web interface.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
It has the following features:
|
It has the following features:
|
||||||
|
|
||||||
@ -19,58 +19,21 @@ It has the following features:
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### Raspberry Pi SD Card Images
|
The following methods of setting up a receiver are currently available:
|
||||||
|
|
||||||
Probably the quickest way to get started is to download the latest Raspberry Pi SD Card Image. It contains all the
|
- Raspberry Pi SD card images
|
||||||
depencencies out of the box, and should work on all Raspberry Pis. It is based off the Raspbian Lite distribution,
|
- Debian repository
|
||||||
so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply.
|
- Docker images
|
||||||
|
- Manual installation
|
||||||
|
|
||||||
You can find the latest images [here](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/index.html). You can
|
Please checkout the [setup guide on the wiki](https://github.com/jketterl/openwebrx/wiki/Setup-Guide) for more details
|
||||||
also checkout the `nightly` folder, which has the most recent builds, albeit untested.
|
on the respective methods.
|
||||||
|
|
||||||
Once you have booted a Raspberry with the SD Card, it will appear in your network with the hostname "openwebrx", which
|
## Community
|
||||||
should make it available as https://openwebrx/ on most networks. This may vary depending on your specific setup.
|
|
||||||
|
|
||||||
For Digital voice, the minimum requirement right now seems to be a Rasbperry Pi 3B+. I would like to work on optimizing
|
If you have trouble setting up or configuring your receiver, you have some great idea you want to see implemented, or
|
||||||
this for lower specs, but at this point I am not sure how much can be done.
|
you just generally want to have some OpenWebRX-related chat, come visit us over on
|
||||||
|
[our groups.io group](https://groups.io/g/openwebrx).
|
||||||
### Docker Images
|
|
||||||
|
|
||||||
For those familiar with docker, I am providing
|
|
||||||
[recent builds and Releases for both x86 and arm processors on the Docker hub](https://hub.docker.com/r/jketterl/openwebrx).
|
|
||||||
You can find a short introduction there.
|
|
||||||
|
|
||||||
### Manual Installation
|
|
||||||
|
|
||||||
OpenWebRX currently requires Linux and python >= 3.6 to run.
|
|
||||||
|
|
||||||
First you will need to install the dependencies:
|
|
||||||
|
|
||||||
- [csdr](https://github.com/jketterl/csdr)
|
|
||||||
- [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr)
|
|
||||||
- [owrx_connector](https://github.com/jketterl/owrx_connector)
|
|
||||||
|
|
||||||
Optional dependencies if you want to be able to listen do digital voice:
|
|
||||||
|
|
||||||
- [digiham](https://github.com/jketterl/digiham)
|
|
||||||
- [dsd](https://github.com/f4exb/dsdcc)
|
|
||||||
|
|
||||||
Optional dependency if you want to decode WSJT-X modes:
|
|
||||||
|
|
||||||
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html)
|
|
||||||
|
|
||||||
[Detailed installation instructions in the Wiki](https://github.com/jketterl/openwebrx/wiki/Manual-Package-installation-(including-digital-voice))
|
|
||||||
|
|
||||||
After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server:
|
|
||||||
|
|
||||||
./openwebrx.py
|
|
||||||
|
|
||||||
You can now open the GUI at <a href="http://localhost:8073">http://localhost:8073</a>.
|
|
||||||
|
|
||||||
Now the next step is to customize the parameters of your server in `config_webrx.py`.
|
|
||||||
|
|
||||||
Actually, if you do something cool with OpenWebRX, please drop me a mail:
|
|
||||||
*Jakob Ketterl, DD5JFK <dd5jfk@darc.de>*
|
|
||||||
|
|
||||||
## Usage tips
|
## Usage tips
|
||||||
|
|
||||||
@ -83,7 +46,7 @@ However, if you hold down the shift key, you can drag the center line (BFO) or t
|
|||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
OpenWebRX is available under Affero GPL v3 license
|
OpenWebRX is available under Affero GPL v3 license
|
||||||
([summary](https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)).
|
([summary](https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0))).
|
||||||
|
|
||||||
OpenWebRX is also available under a commercial license on request. Please contact me at the address
|
OpenWebRX is also available under a commercial license on request. Please contact me at the address
|
||||||
*<randras@sdr.hu>* for licensing options.
|
*<randras@sdr.hu>* for licensing options.
|
||||||
|
100
bands.json
100
bands.json
@ -8,7 +8,8 @@
|
|||||||
"ft8": 1840000,
|
"ft8": 1840000,
|
||||||
"wspr": 1836600,
|
"wspr": 1836600,
|
||||||
"jt65": 1838000,
|
"jt65": 1838000,
|
||||||
"jt9": 1839000
|
"jt9": 1839000,
|
||||||
|
"js8": 1842000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -21,7 +22,8 @@
|
|||||||
"wspr": 3592600,
|
"wspr": 3592600,
|
||||||
"jt65": 3570000,
|
"jt65": 3570000,
|
||||||
"jt9": 3572000,
|
"jt9": 3572000,
|
||||||
"ft4": [3568000, 3575000]
|
"ft4": [3568000, 3575000],
|
||||||
|
"js8": 3578000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -43,7 +45,8 @@
|
|||||||
"wspr": 7038600,
|
"wspr": 7038600,
|
||||||
"jt65": 7076000,
|
"jt65": 7076000,
|
||||||
"jt9": 7078000,
|
"jt9": 7078000,
|
||||||
"ft4": 7047500
|
"ft4": 7047500,
|
||||||
|
"js8": 7078000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -56,7 +59,8 @@
|
|||||||
"wspr": 10138700,
|
"wspr": 10138700,
|
||||||
"jt65": 10138000,
|
"jt65": 10138000,
|
||||||
"jt9": 10140000,
|
"jt9": 10140000,
|
||||||
"ft4": 10140000
|
"ft4": 10140000,
|
||||||
|
"js8": 10130000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -69,7 +73,8 @@
|
|||||||
"wspr": 14095600,
|
"wspr": 14095600,
|
||||||
"jt65": 14076000,
|
"jt65": 14076000,
|
||||||
"jt9": 14078000,
|
"jt9": 14078000,
|
||||||
"ft4": 14080000
|
"ft4": 14080000,
|
||||||
|
"js8": 14078000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -82,7 +87,8 @@
|
|||||||
"wspr": 18104600,
|
"wspr": 18104600,
|
||||||
"jt65": 18102000,
|
"jt65": 18102000,
|
||||||
"jt9": 18104000,
|
"jt9": 18104000,
|
||||||
"ft4": 18104000
|
"ft4": 18104000,
|
||||||
|
"js8": 18104000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -95,7 +101,8 @@
|
|||||||
"wspr": 21094600,
|
"wspr": 21094600,
|
||||||
"jt65": 21076000,
|
"jt65": 21076000,
|
||||||
"jt9": 21078000,
|
"jt9": 21078000,
|
||||||
"ft4": 21140000
|
"ft4": 21140000,
|
||||||
|
"js8": 21078000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -108,7 +115,8 @@
|
|||||||
"wspr": 24924600,
|
"wspr": 24924600,
|
||||||
"jt65": 24917000,
|
"jt65": 24917000,
|
||||||
"jt9": 24919000,
|
"jt9": 24919000,
|
||||||
"ft4": 24919000
|
"ft4": 24919000,
|
||||||
|
"js8": 24922000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -121,7 +129,8 @@
|
|||||||
"wspr": 28124600,
|
"wspr": 28124600,
|
||||||
"jt65": 28076000,
|
"jt65": 28076000,
|
||||||
"jt9": 28078000,
|
"jt9": 28078000,
|
||||||
"ft4": 28180000
|
"ft4": 28180000,
|
||||||
|
"js8": 28078000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -134,7 +143,8 @@
|
|||||||
"wspr": 50293000,
|
"wspr": 50293000,
|
||||||
"jt65": 50310000,
|
"jt65": 50310000,
|
||||||
"jt9": 50312000,
|
"jt9": 50312000,
|
||||||
"ft4": 50318000
|
"ft4": 50318000,
|
||||||
|
"js8": 50318000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -189,5 +199,75 @@
|
|||||||
"name": "3cm",
|
"name": "3cm",
|
||||||
"lower_bound": 10000000000,
|
"lower_bound": 10000000000,
|
||||||
"upper_bound": 10500000000
|
"upper_bound": 10500000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "120m Broadcast",
|
||||||
|
"lower_bound": 2300000,
|
||||||
|
"upper_bound": 2495000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "90m Broadcast",
|
||||||
|
"lower_bound": 3200000,
|
||||||
|
"upper_bound": 3400000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "75m Broadcast",
|
||||||
|
"lower_bound": 3900000,
|
||||||
|
"upper_bound": 4000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "60m Broadcast",
|
||||||
|
"lower_bound": 4750000,
|
||||||
|
"upper_bound": 4995000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "49m Broadcast",
|
||||||
|
"lower_bound": 5900000,
|
||||||
|
"upper_bound": 6200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "41m Broadcast",
|
||||||
|
"lower_bound": 7200000,
|
||||||
|
"upper_bound": 7450000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "31m Broadcast",
|
||||||
|
"lower_bound": 9400000,
|
||||||
|
"upper_bound": 9900000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "25m Broadcast",
|
||||||
|
"lower_bound": 11600000,
|
||||||
|
"upper_bound": 12100000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "22m Broadcast",
|
||||||
|
"lower_bound": 13570000,
|
||||||
|
"upper_bound": 13870000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "19m Broadcast",
|
||||||
|
"lower_bound": 15100000,
|
||||||
|
"upper_bound": 15830000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "16m Broadcast",
|
||||||
|
"lower_bound": 17480000,
|
||||||
|
"upper_bound": 17900000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "15m Broadcast",
|
||||||
|
"lower_bound": 18900000,
|
||||||
|
"upper_bound": 19020000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "13m Broadcast",
|
||||||
|
"lower_bound": 21450000,
|
||||||
|
"upper_bound": 21850000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "11m Broadcast",
|
||||||
|
"lower_bound": 25670000,
|
||||||
|
"upper_bound": 26100000
|
||||||
}
|
}
|
||||||
]
|
]
|
10
build.sh
10
build.sh
@ -1,10 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
. docker/env
|
||||||
ARCH=$(uname -m)
|
|
||||||
|
|
||||||
TAG="latest"
|
|
||||||
ARCHTAG="$TAG-$ARCH"
|
|
||||||
|
|
||||||
docker build --pull -t openwebrx-base:$ARCHTAG -f docker/Dockerfiles/Dockerfile-base .
|
docker build --pull -t openwebrx-base:$ARCHTAG -f docker/Dockerfiles/Dockerfile-base .
|
||||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr .
|
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr .
|
||||||
@ -13,4 +9,8 @@ docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-sdrplay:$ARCHTAG
|
|||||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-hackrf:$ARCHTAG -f docker/Dockerfiles/Dockerfile-hackrf .
|
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-hackrf:$ARCHTAG -f docker/Dockerfiles/Dockerfile-hackrf .
|
||||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-airspy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-airspy .
|
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-airspy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-airspy .
|
||||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr-soapy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr-soapy .
|
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr-soapy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr-soapy .
|
||||||
|
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-plutosdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-plutosdr .
|
||||||
|
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-limesdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-limesdr .
|
||||||
|
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-soapyremote:$ARCHTAG -f docker/Dockerfiles/Dockerfile-soapyremote .
|
||||||
|
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-perseus:$ARCHTAG -f docker/Dockerfiles/Dockerfile-perseus .
|
||||||
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-full:$ARCHTAG -t jketterl/openwebrx:$ARCHTAG -f docker/Dockerfiles/Dockerfile-full .
|
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-full:$ARCHTAG -t jketterl/openwebrx:$ARCHTAG -f docker/Dockerfiles/Dockerfile-full .
|
||||||
|
104
config_webrx.py
104
config_webrx.py
@ -32,8 +32,11 @@ config_webrx: configuration options for OpenWebRX
|
|||||||
and use them for running your web service with OpenWebRX.)
|
and use them for running your web service with OpenWebRX.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# configuration version. please only modify if you're able to perform the associated migration steps.
|
||||||
|
version = 2
|
||||||
|
|
||||||
# NOTE: you can find additional information about configuring OpenWebRX in the Wiki:
|
# NOTE: you can find additional information about configuring OpenWebRX in the Wiki:
|
||||||
# https://github.com/simonyiszk/openwebrx/wiki
|
# https://github.com/jketterl/openwebrx/wiki/Configuration-guide
|
||||||
|
|
||||||
# ==== Server settings ====
|
# ==== Server settings ====
|
||||||
web_port = 8073
|
web_port = 8073
|
||||||
@ -44,25 +47,18 @@ receiver_name = "[Callsign]"
|
|||||||
receiver_location = "Budapest, Hungary"
|
receiver_location = "Budapest, Hungary"
|
||||||
receiver_asl = 200
|
receiver_asl = 200
|
||||||
receiver_admin = "example@example.com"
|
receiver_admin = "example@example.com"
|
||||||
receiver_gps = (47.000000, 19.000000)
|
receiver_gps = {"lat": 47.000000, "lon": 19.000000}
|
||||||
photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
|
photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
|
||||||
|
# photo_desc allows you to put pretty much any HTML you like into the receiver description.
|
||||||
|
# The lines below should give you some examples of what's possible.
|
||||||
photo_desc = """
|
photo_desc = """
|
||||||
You can add your own background photo and receiver information.<br />
|
You can add your own background photo and receiver information.<br />
|
||||||
Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/>
|
Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
|
||||||
Device: %[RX_DEVICE]<br />
|
Device: Receiver Device<br />
|
||||||
Antenna: %[RX_ANT]<br />
|
Antenna: Receiver Antenna<br />
|
||||||
Website: <a href="http://localhost" target="_blank">http://localhost</a>
|
Website: <a href="http://localhost" target="_blank">http://localhost</a>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ==== sdr.hu listing ====
|
|
||||||
# If you want your ham receiver to be listed publicly on sdr.hu, then take the following steps:
|
|
||||||
# 1. Register at: http://sdr.hu/register
|
|
||||||
# 2. You will get an unique key by email. Copy it and paste here:
|
|
||||||
sdrhu_key = ""
|
|
||||||
# 3. Set this setting to True to enable listing:
|
|
||||||
sdrhu_public_listing = False
|
|
||||||
server_hostname = "localhost"
|
|
||||||
|
|
||||||
# ==== DSP/RX settings ====
|
# ==== DSP/RX settings ====
|
||||||
fft_fps = 9
|
fft_fps = 9
|
||||||
fft_size = 4096 # Should be power of 2
|
fft_size = 4096 # Should be power of 2
|
||||||
@ -93,13 +89,14 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
|
|||||||
# ==== I/Q sources ====
|
# ==== I/Q sources ====
|
||||||
# (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.)
|
# (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.)
|
||||||
|
|
||||||
#################################################################################################
|
###############################################################################
|
||||||
# Is my SDR hardware supported? #
|
# Is my SDR hardware supported? #
|
||||||
# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support #
|
# Check here: https://github.com/jketterl/openwebrx/wiki/Supported-Hardware #
|
||||||
#################################################################################################
|
###############################################################################
|
||||||
|
|
||||||
# Currently supported types of sdr receivers:
|
# Currently supported types of sdr receivers:
|
||||||
# "rtl_sdr", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr"
|
# "rtl_sdr", "rtl_sdr_soapy", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr",
|
||||||
|
# "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote"
|
||||||
#
|
#
|
||||||
# In order to use rtl_sdr, you will need to install librtlsdr-dev and the connector.
|
# In order to use rtl_sdr, you will need to install librtlsdr-dev and the connector.
|
||||||
# In order to use sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the
|
# In order to use sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the
|
||||||
@ -107,8 +104,13 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
|
|||||||
#
|
#
|
||||||
# https://github.com/jketterl/owrx_connector
|
# https://github.com/jketterl/owrx_connector
|
||||||
#
|
#
|
||||||
# NOTE: The connector sources have replaced the old piped nmux style of reading input. If you still have any sdrs
|
# In order to use Perseus HF you need to install the libperseus-sdr
|
||||||
# configured that have type endin in "_connector", simply remove that suffix.
|
#
|
||||||
|
# https://github.com/Microtelecom/libperseus-sdr
|
||||||
|
#
|
||||||
|
# and do the proper changes to the sdrs object below
|
||||||
|
# (see also Wiki in https://github.com/jketterl/openwebrx/wiki/Sample-configuration-for-Perseus-HF-receiver).
|
||||||
|
#
|
||||||
|
|
||||||
sdrs = {
|
sdrs = {
|
||||||
"rtlsdr": {
|
"rtlsdr": {
|
||||||
@ -141,19 +143,18 @@ sdrs = {
|
|||||||
"name": "Airspy HF+",
|
"name": "Airspy HF+",
|
||||||
"type": "airspyhf",
|
"type": "airspyhf",
|
||||||
"ppm": 0,
|
"ppm": 0,
|
||||||
|
"rf_gain": "auto",
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"20m": {
|
"20m": {
|
||||||
"name": "20m",
|
"name": "20m",
|
||||||
"center_freq": 14150000,
|
"center_freq": 14150000,
|
||||||
"rf_gain": 10,
|
"samp_rate": 384000,
|
||||||
"samp_rate": 768000,
|
|
||||||
"start_freq": 14070000,
|
"start_freq": 14070000,
|
||||||
"start_mod": "usb",
|
"start_mod": "usb",
|
||||||
},
|
},
|
||||||
"30m": {
|
"30m": {
|
||||||
"name": "30m",
|
"name": "30m",
|
||||||
"center_freq": 10125000,
|
"center_freq": 10125000,
|
||||||
"rf_gain": 10,
|
|
||||||
"samp_rate": 192000,
|
"samp_rate": 192000,
|
||||||
"start_freq": 10142000,
|
"start_freq": 10142000,
|
||||||
"start_mod": "usb",
|
"start_mod": "usb",
|
||||||
@ -161,24 +162,21 @@ sdrs = {
|
|||||||
"40m": {
|
"40m": {
|
||||||
"name": "40m",
|
"name": "40m",
|
||||||
"center_freq": 7100000,
|
"center_freq": 7100000,
|
||||||
"rf_gain": 10,
|
|
||||||
"samp_rate": 256000,
|
"samp_rate": 256000,
|
||||||
"start_freq": 7070000,
|
"start_freq": 7070000,
|
||||||
"start_mod": "usb",
|
"start_mod": "lsb",
|
||||||
},
|
},
|
||||||
"80m": {
|
"80m": {
|
||||||
"name": "80m",
|
"name": "80m",
|
||||||
"center_freq": 3650000,
|
"center_freq": 3650000,
|
||||||
"rf_gain": 10,
|
"samp_rate": 384000,
|
||||||
"samp_rate": 768000,
|
|
||||||
"start_freq": 3570000,
|
"start_freq": 3570000,
|
||||||
"start_mod": "usb",
|
"start_mod": "lsb",
|
||||||
},
|
},
|
||||||
"49m": {
|
"49m": {
|
||||||
"name": "49m Broadcast",
|
"name": "49m Broadcast",
|
||||||
"center_freq": 6000000,
|
"center_freq": 6050000,
|
||||||
"rf_gain": 10,
|
"samp_rate": 384000,
|
||||||
"samp_rate": 768000,
|
|
||||||
"start_freq": 6070000,
|
"start_freq": 6070000,
|
||||||
"start_mod": "am",
|
"start_mod": "am",
|
||||||
},
|
},
|
||||||
@ -188,6 +186,7 @@ sdrs = {
|
|||||||
"name": "SDRPlay RSP2",
|
"name": "SDRPlay RSP2",
|
||||||
"type": "sdrplay",
|
"type": "sdrplay",
|
||||||
"ppm": 0,
|
"ppm": 0,
|
||||||
|
"antenna": "Antenna A",
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"20m": {
|
"20m": {
|
||||||
"name": "20m",
|
"name": "20m",
|
||||||
@ -196,7 +195,6 @@ sdrs = {
|
|||||||
"samp_rate": 500000,
|
"samp_rate": 500000,
|
||||||
"start_freq": 14070000,
|
"start_freq": 14070000,
|
||||||
"start_mod": "usb",
|
"start_mod": "usb",
|
||||||
"antenna": "Antenna A",
|
|
||||||
},
|
},
|
||||||
"30m": {
|
"30m": {
|
||||||
"name": "30m",
|
"name": "30m",
|
||||||
@ -212,8 +210,7 @@ sdrs = {
|
|||||||
"rf_gain": 0,
|
"rf_gain": 0,
|
||||||
"samp_rate": 500000,
|
"samp_rate": 500000,
|
||||||
"start_freq": 7070000,
|
"start_freq": 7070000,
|
||||||
"start_mod": "usb",
|
"start_mod": "lsb",
|
||||||
"antenna": "Antenna A",
|
|
||||||
},
|
},
|
||||||
"80m": {
|
"80m": {
|
||||||
"name": "80m",
|
"name": "80m",
|
||||||
@ -221,8 +218,7 @@ sdrs = {
|
|||||||
"rf_gain": 0,
|
"rf_gain": 0,
|
||||||
"samp_rate": 500000,
|
"samp_rate": 500000,
|
||||||
"start_freq": 3570000,
|
"start_freq": 3570000,
|
||||||
"start_mod": "usb",
|
"start_mod": "lsb",
|
||||||
"antenna": "Antenna A",
|
|
||||||
},
|
},
|
||||||
"49m": {
|
"49m": {
|
||||||
"name": "49m Broadcast",
|
"name": "49m Broadcast",
|
||||||
@ -231,7 +227,6 @@ sdrs = {
|
|||||||
"samp_rate": 500000,
|
"samp_rate": 500000,
|
||||||
"start_freq": 6070000,
|
"start_freq": 6070000,
|
||||||
"start_mod": "am",
|
"start_mod": "am",
|
||||||
"antenna": "Antenna A",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -245,20 +240,20 @@ sdrs = {
|
|||||||
waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF]
|
waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF]
|
||||||
waterfall_min_level = -88 # in dB
|
waterfall_min_level = -88 # in dB
|
||||||
waterfall_max_level = -20
|
waterfall_max_level = -20
|
||||||
waterfall_auto_level_margin = (5, 40)
|
waterfall_auto_level_margin = {"min": 5, "max": 40}
|
||||||
### old theme by HA7ILM:
|
### old theme by HA7ILM:
|
||||||
# waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
|
# waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
|
||||||
# waterfall_min_level = -115 #in dB
|
# waterfall_min_level = -115 #in dB
|
||||||
# waterfall_max_level = 0
|
# waterfall_max_level = 0
|
||||||
# waterfall_auto_level_margin = (20, 30)
|
# waterfall_auto_level_margin = {"min": 20, "max": 30}
|
||||||
##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
|
##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
|
||||||
|
|
||||||
# Note: When the auto waterfall level button is clicked, the following happens:
|
# Note: When the auto waterfall level button is clicked, the following happens:
|
||||||
# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]]
|
# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin["min"]]
|
||||||
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]]
|
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin["max"]]
|
||||||
#
|
#
|
||||||
# ___|____________________________________|____________________________________|____________________________________|___> signal power
|
# ___|________________________________________|____________________________________|________________________________________|___> signal power
|
||||||
# \_waterfall_auto_level_margin[0]_/ |__ current_min_power_level | \_waterfall_auto_level_margin[1]_/
|
# \_waterfall_auto_level_margin["min"]_/ |__ current_min_power_level | \_waterfall_auto_level_margin["max"]_/
|
||||||
# current_max_power_level __|
|
# current_max_power_level __|
|
||||||
|
|
||||||
# === Experimental settings ===
|
# === Experimental settings ===
|
||||||
@ -276,22 +271,28 @@ google_maps_api_key = ""
|
|||||||
# in seconds; default: 2 hours
|
# in seconds; default: 2 hours
|
||||||
map_position_retention_time = 2 * 60 * 60
|
map_position_retention_time = 2 * 60 * 60
|
||||||
|
|
||||||
# wsjt decoder queue configuration
|
# decoder queue configuration
|
||||||
# due to the nature of the wsjt operating modes (ft8, ft8, jt9, jt65 and wspr), the data is recorded for a given amount
|
# due to the nature of some operating modes (ft8, ft8, jt9, jt65, wspr and js8), the data is recorded for a given amount
|
||||||
# of time (6.5 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
|
# of time (6 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
|
||||||
# to mitigate this, the recordings will be queued and processed in sequence.
|
# to mitigate this, the recordings will be queued and processed in sequence.
|
||||||
# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
|
# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
|
||||||
wsjt_queue_workers = 2
|
decoding_queue_workers = 2
|
||||||
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up
|
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up
|
||||||
# if you are running background services, make sure this number is high enough to accept the task influx during peaks
|
# if you are running background services, make sure this number is high enough to accept the task influx during peaks
|
||||||
# i.e. this should be higher than the number of wsjt services running at the same time
|
# i.e. this should be higher than the number of decoding services running at the same time
|
||||||
wsjt_queue_length = 10
|
decoding_queue_length = 10
|
||||||
|
|
||||||
# wsjt decoding depth will allow more results, but will also consume more cpu
|
# wsjt decoding depth will allow more results, but will also consume more cpu
|
||||||
wsjt_decoding_depth = 3
|
wsjt_decoding_depth = 3
|
||||||
# can also be set for each mode separately
|
# can also be set for each mode separately
|
||||||
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
|
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
|
||||||
wsjt_decoding_depths = {"jt65": 1}
|
wsjt_decoding_depths = {"jt65": 1}
|
||||||
|
|
||||||
|
# JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled.
|
||||||
|
js8_enabled_profiles = ["normal", "slow"]
|
||||||
|
# JS8 decoding depth; higher value will get more results, but will also consume more cpu
|
||||||
|
js8_decoding_depth = 3
|
||||||
|
|
||||||
temporary_directory = "/tmp"
|
temporary_directory = "/tmp"
|
||||||
|
|
||||||
services_enabled = False
|
services_enabled = False
|
||||||
@ -314,3 +315,8 @@ aprs_symbols_path = "/opt/aprs-symbols/png"
|
|||||||
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator
|
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator
|
||||||
pskreporter_enabled = False
|
pskreporter_enabled = False
|
||||||
pskreporter_callsign = "N0CALL"
|
pskreporter_callsign = "N0CALL"
|
||||||
|
|
||||||
|
# === Web admin settings ===
|
||||||
|
# this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote
|
||||||
|
# changes to the receiver settings. enable for testing in controlled environment only.
|
||||||
|
# webadmin_enabled = False
|
||||||
|
317
csdr/csdr.py
317
csdr/csdr.py
@ -29,7 +29,9 @@ import math
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from owrx.kiss import KissClient, DirewolfConfig
|
from owrx.kiss import KissClient, DirewolfConfig
|
||||||
from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper
|
from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile
|
||||||
|
from owrx.js8 import Js8Profiles
|
||||||
|
from owrx.audio import AudioChopper
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -64,12 +66,105 @@ class output(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Pipe(object):
|
||||||
|
READ = "r"
|
||||||
|
WRITE = "w"
|
||||||
|
NONE = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(path, t, encoding=None):
|
||||||
|
if t == Pipe.READ:
|
||||||
|
return ReadingPipe(path, encoding=encoding)
|
||||||
|
elif t == Pipe.WRITE:
|
||||||
|
return WritingPipe(path, encoding=encoding)
|
||||||
|
elif t == Pipe.NONE:
|
||||||
|
return Pipe(path, None, encoding=encoding)
|
||||||
|
|
||||||
|
def __init__(self, path, direction, encoding=None):
|
||||||
|
self.path = path
|
||||||
|
self.direction = direction
|
||||||
|
self.encoding = encoding
|
||||||
|
self.file = None
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
os.mkfifo(path)
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
self.file = open(self.path, self.direction, encoding=self.encoding)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.file is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.file.close()
|
||||||
|
os.unlink(self.path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# it seems like we keep calling this twice. no idea why, but we don't need the resulting error.
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Pipe.close()")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.path
|
||||||
|
|
||||||
|
|
||||||
|
class WritingPipe(Pipe):
|
||||||
|
def __init__(self, path, encoding=None):
|
||||||
|
self.queue = []
|
||||||
|
self.queueLock = threading.Lock()
|
||||||
|
super().__init__(path, "w", encoding=encoding)
|
||||||
|
self.open()
|
||||||
|
|
||||||
|
def open_and_dequeue(self):
|
||||||
|
super().open()
|
||||||
|
with self.queueLock:
|
||||||
|
for i in self.queue:
|
||||||
|
self.file.write(i)
|
||||||
|
self.file.flush()
|
||||||
|
self.queue = None
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
threading.Thread(target=self.open_and_dequeue).start()
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
if self.file is None:
|
||||||
|
with self.queueLock:
|
||||||
|
self.queue.append(data)
|
||||||
|
return
|
||||||
|
r = self.file.write(data)
|
||||||
|
self.file.flush()
|
||||||
|
return r
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.file is None:
|
||||||
|
logger.warning("queue %s never successfully opened - thread leak!", self.path)
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
|
||||||
|
class ReadingPipe(Pipe):
|
||||||
|
def __init__(self, path, encoding=None):
|
||||||
|
super().__init__(path, "r", encoding=encoding)
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
if self.file is None:
|
||||||
|
self.open()
|
||||||
|
return self.file.read()
|
||||||
|
|
||||||
|
def readline(self):
|
||||||
|
if self.file is None:
|
||||||
|
self.open()
|
||||||
|
return self.file.readline()
|
||||||
|
|
||||||
|
|
||||||
class dsp(object):
|
class dsp(object):
|
||||||
def __init__(self, output):
|
def __init__(self, output):
|
||||||
self.samp_rate = 250000
|
self.samp_rate = 250000
|
||||||
self.output_rate = 11025
|
self.output_rate = 11025
|
||||||
self.fft_size = 1024
|
self.fft_size = 1024
|
||||||
self.fft_fps = 5
|
self.fft_fps = 5
|
||||||
|
self.center_freq = 0
|
||||||
self.offset_freq = 0
|
self.offset_freq = 0
|
||||||
self.low_cut = -4000
|
self.low_cut = -4000
|
||||||
self.high_cut = 4000
|
self.high_cut = 4000
|
||||||
@ -82,6 +177,8 @@ class dsp(object):
|
|||||||
self.demodulator = "nfm"
|
self.demodulator = "nfm"
|
||||||
self.name = "csdr"
|
self.name = "csdr"
|
||||||
self.base_bufsize = 512
|
self.base_bufsize = 512
|
||||||
|
self.decimation = None
|
||||||
|
self.last_decimation = None
|
||||||
self.nc_port = None
|
self.nc_port = None
|
||||||
self.csdr_dynamic_bufsize = False
|
self.csdr_dynamic_bufsize = False
|
||||||
self.csdr_print_bufsizes = False
|
self.csdr_print_bufsizes = False
|
||||||
@ -94,31 +191,38 @@ class dsp(object):
|
|||||||
self.secondary_fft_size = 1024
|
self.secondary_fft_size = 1024
|
||||||
self.secondary_process_fft = None
|
self.secondary_process_fft = None
|
||||||
self.secondary_process_demod = None
|
self.secondary_process_demod = None
|
||||||
self.pipe_names = [
|
self.pipe_names = {
|
||||||
"bpf_pipe",
|
"bpf_pipe": Pipe.WRITE,
|
||||||
"shift_pipe",
|
"shift_pipe": Pipe.WRITE,
|
||||||
"squelch_pipe",
|
"squelch_pipe": Pipe.WRITE,
|
||||||
"smeter_pipe",
|
"smeter_pipe": Pipe.READ,
|
||||||
"meta_pipe",
|
"meta_pipe": Pipe.READ,
|
||||||
"iqtee_pipe",
|
"iqtee_pipe": Pipe.NONE,
|
||||||
"iqtee2_pipe",
|
"iqtee2_pipe": Pipe.NONE,
|
||||||
"dmr_control_pipe",
|
"dmr_control_pipe": Pipe.WRITE,
|
||||||
]
|
}
|
||||||
self.secondary_pipe_names = ["secondary_shift_pipe"]
|
self.pipes = {}
|
||||||
|
self.secondary_pipe_names = {"secondary_shift_pipe": Pipe.WRITE}
|
||||||
self.secondary_offset_freq = 1000
|
self.secondary_offset_freq = 1000
|
||||||
self.unvoiced_quality = 1
|
self.unvoiced_quality = 1
|
||||||
self.modification_lock = threading.Lock()
|
self.modification_lock = threading.Lock()
|
||||||
self.output = output
|
self.output = output
|
||||||
self.temporary_directory = "/tmp"
|
|
||||||
|
self.temporary_directory = None
|
||||||
|
self.pipe_base_path = None
|
||||||
|
self.set_temporary_directory("/tmp")
|
||||||
|
|
||||||
self.is_service = False
|
self.is_service = False
|
||||||
self.direwolf_config = None
|
self.direwolf_config = None
|
||||||
self.direwolf_port = None
|
self.direwolf_port = None
|
||||||
|
self.process = None
|
||||||
|
|
||||||
def set_service(self, flag=True):
|
def set_service(self, flag=True):
|
||||||
self.is_service = flag
|
self.is_service = flag
|
||||||
|
|
||||||
def set_temporary_directory(self, what):
|
def set_temporary_directory(self, what):
|
||||||
self.temporary_directory = what
|
self.temporary_directory = what
|
||||||
|
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self))
|
||||||
|
|
||||||
def chain(self, which):
|
def chain(self, which):
|
||||||
chain = ["nc -v 127.0.0.1 {nc_port}"]
|
chain = ["nc -v 127.0.0.1 {nc_port}"]
|
||||||
@ -137,7 +241,7 @@ class dsp(object):
|
|||||||
if self.fft_compression == "adpcm":
|
if self.fft_compression == "adpcm":
|
||||||
chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"]
|
chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"]
|
||||||
return chain
|
return chain
|
||||||
chain += ["csdr shift_addition_cc --fifo {shift_pipe}"]
|
chain += ["csdr shift_addfast_cc --fifo {shift_pipe}"]
|
||||||
if self.decimation > 1:
|
if self.decimation > 1:
|
||||||
chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"]
|
chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"]
|
||||||
chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"]
|
chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"]
|
||||||
@ -227,14 +331,14 @@ class dsp(object):
|
|||||||
return chain
|
return chain
|
||||||
elif which == "bpsk31" or which == "bpsk63":
|
elif which == "bpsk31" or which == "bpsk63":
|
||||||
return chain + [
|
return chain + [
|
||||||
"csdr shift_addition_cc --fifo {secondary_shift_pipe}",
|
"csdr shift_addfast_cc --fifo {secondary_shift_pipe}",
|
||||||
"csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}",
|
"csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}",
|
||||||
"csdr simple_agc_cc 0.001 0.5",
|
"csdr simple_agc_cc 0.001 0.5",
|
||||||
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q",
|
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q",
|
||||||
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8",
|
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8",
|
||||||
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8",
|
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8",
|
||||||
]
|
]
|
||||||
elif self.isWsjtMode(which):
|
elif self.isWsjtMode(which) or self.isJs8(which):
|
||||||
chain += ["csdr realpart_cf"]
|
chain += ["csdr realpart_cf"]
|
||||||
if self.last_decimation != 1.0:
|
if self.last_decimation != 1.0:
|
||||||
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
||||||
@ -243,7 +347,7 @@ class dsp(object):
|
|||||||
chain += ["csdr fmdemod_quadri_cf"]
|
chain += ["csdr fmdemod_quadri_cf"]
|
||||||
if self.last_decimation != 1.0:
|
if self.last_decimation != 1.0:
|
||||||
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
||||||
return chain + ["csdr convert_f_s16", "direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h - 1>&2"]
|
return chain + ["csdr convert_f_s16", "direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h 1>&2"]
|
||||||
elif which == "pocsag":
|
elif which == "pocsag":
|
||||||
chain += ["csdr fmdemod_quadri_cf"]
|
chain += ["csdr fmdemod_quadri_cf"]
|
||||||
if self.last_decimation != 1.0:
|
if self.last_decimation != 1.0:
|
||||||
@ -301,8 +405,8 @@ class dsp(object):
|
|||||||
self.try_create_configs(secondary_command_demod)
|
self.try_create_configs(secondary_command_demod)
|
||||||
|
|
||||||
secondary_command_demod = secondary_command_demod.format(
|
secondary_command_demod = secondary_command_demod.format(
|
||||||
input_pipe=self.iqtee2_pipe,
|
input_pipe=self.pipes["iqtee2_pipe"],
|
||||||
secondary_shift_pipe=self.secondary_shift_pipe,
|
secondary_shift_pipe=self.pipes["secondary_shift_pipe"],
|
||||||
secondary_decimation=self.secondary_decimation(),
|
secondary_decimation=self.secondary_decimation(),
|
||||||
secondary_samples_per_bits=self.secondary_samples_per_bits(),
|
secondary_samples_per_bits=self.secondary_samples_per_bits(),
|
||||||
secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
|
secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
|
||||||
@ -321,7 +425,7 @@ class dsp(object):
|
|||||||
if self.output.supports_type("secondary_fft"):
|
if self.output.supports_type("secondary_fft"):
|
||||||
secondary_command_fft = " | ".join(self.secondary_chain("fft"))
|
secondary_command_fft = " | ".join(self.secondary_chain("fft"))
|
||||||
secondary_command_fft = secondary_command_fft.format(
|
secondary_command_fft = secondary_command_fft.format(
|
||||||
input_pipe=self.iqtee_pipe,
|
input_pipe=self.pipes["iqtee_pipe"],
|
||||||
secondary_fft_input_size=self.secondary_fft_size,
|
secondary_fft_input_size=self.secondary_fft_size,
|
||||||
secondary_fft_size=self.secondary_fft_size,
|
secondary_fft_size=self.secondary_fft_size,
|
||||||
secondary_fft_block_size=self.secondary_fft_block_size(),
|
secondary_fft_block_size=self.secondary_fft_block_size(),
|
||||||
@ -347,18 +451,25 @@ class dsp(object):
|
|||||||
|
|
||||||
if self.isWsjtMode():
|
if self.isWsjtMode():
|
||||||
smd = self.get_secondary_demodulator()
|
smd = self.get_secondary_demodulator()
|
||||||
|
chopper_profile = None
|
||||||
if smd == "ft8":
|
if smd == "ft8":
|
||||||
chopper = Ft8Chopper(self.secondary_process_demod.stdout)
|
chopper_profile = Ft8Profile()
|
||||||
elif smd == "wspr":
|
elif smd == "wspr":
|
||||||
chopper = WsprChopper(self.secondary_process_demod.stdout)
|
chopper_profile = WsprProfile()
|
||||||
elif smd == "jt65":
|
elif smd == "jt65":
|
||||||
chopper = Jt65Chopper(self.secondary_process_demod.stdout)
|
chopper_profile = Jt65Profile()
|
||||||
elif smd == "jt9":
|
elif smd == "jt9":
|
||||||
chopper = Jt9Chopper(self.secondary_process_demod.stdout)
|
chopper_profile = Jt9Profile()
|
||||||
elif smd == "ft4":
|
elif smd == "ft4":
|
||||||
chopper = Ft4Chopper(self.secondary_process_demod.stdout)
|
chopper_profile = Ft4Profile()
|
||||||
|
if chopper_profile is not None:
|
||||||
|
chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile)
|
||||||
chopper.start()
|
chopper.start()
|
||||||
self.output.send_output("wsjt_demod", chopper.read)
|
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)
|
||||||
elif self.isPacket():
|
elif self.isPacket():
|
||||||
# we best get the ax25 packets from the kiss socket
|
# we best get the ax25 packets from the kiss socket
|
||||||
kiss = KissClient(self.direwolf_port)
|
kiss = KissClient(self.direwolf_port)
|
||||||
@ -369,18 +480,16 @@ class dsp(object):
|
|||||||
self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
|
self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
|
||||||
|
|
||||||
# open control pipes for csdr and send initialization data
|
# open control pipes for csdr and send initialization data
|
||||||
if self.secondary_shift_pipe != None: # TODO digimodes
|
if self.has_pipe("secondary_shift_pipe"): # TODO digimodes
|
||||||
self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w") # TODO digimodes
|
|
||||||
self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes
|
self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes
|
||||||
|
|
||||||
def set_secondary_offset_freq(self, value):
|
def set_secondary_offset_freq(self, value):
|
||||||
self.secondary_offset_freq = value
|
self.secondary_offset_freq = value
|
||||||
if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"):
|
if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"):
|
||||||
self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
|
self.pipes["secondary_shift_pipe"].write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
|
||||||
self.secondary_shift_pipe_file.flush()
|
|
||||||
|
|
||||||
def stop_secondary_demodulator(self):
|
def stop_secondary_demodulator(self):
|
||||||
if self.secondary_processes_running == False:
|
if not self.secondary_processes_running:
|
||||||
return
|
return
|
||||||
self.try_delete_pipes(self.secondary_pipe_names)
|
self.try_delete_pipes(self.secondary_pipe_names)
|
||||||
self.try_delete_configs()
|
self.try_delete_configs()
|
||||||
@ -447,7 +556,7 @@ class dsp(object):
|
|||||||
decimation += 1
|
decimation += 1
|
||||||
fraction = float(input_rate / decimation) / output_rate
|
fraction = float(input_rate / decimation) / output_rate
|
||||||
intermediate_rate = input_rate / decimation
|
intermediate_rate = input_rate / decimation
|
||||||
return (decimation, fraction, intermediate_rate)
|
return decimation, fraction, intermediate_rate
|
||||||
|
|
||||||
def if_samp_rate(self):
|
def if_samp_rate(self):
|
||||||
return self.samp_rate / self.decimation
|
return self.samp_rate / self.decimation
|
||||||
@ -461,7 +570,7 @@ class dsp(object):
|
|||||||
def get_audio_rate(self):
|
def get_audio_rate(self):
|
||||||
if self.isDigitalVoice() or self.isPacket() or self.isPocsag():
|
if self.isDigitalVoice() or self.isPacket() or self.isPocsag():
|
||||||
return 48000
|
return 48000
|
||||||
elif self.isWsjtMode():
|
elif self.isWsjtMode() or self.isJs8():
|
||||||
return 12000
|
return 12000
|
||||||
return self.get_output_rate()
|
return self.get_output_rate()
|
||||||
|
|
||||||
@ -475,6 +584,11 @@ class dsp(object):
|
|||||||
demodulator = self.get_secondary_demodulator()
|
demodulator = self.get_secondary_demodulator()
|
||||||
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
|
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
|
||||||
|
|
||||||
|
def isJs8(self, demodulator = None):
|
||||||
|
if demodulator is None:
|
||||||
|
demodulator = self.get_secondary_demodulator()
|
||||||
|
return demodulator == "js8"
|
||||||
|
|
||||||
def isPacket(self, demodulator=None):
|
def isPacket(self, demodulator=None):
|
||||||
if demodulator is None:
|
if demodulator is None:
|
||||||
demodulator = self.get_secondary_demodulator()
|
demodulator = self.get_secondary_demodulator()
|
||||||
@ -493,6 +607,8 @@ class dsp(object):
|
|||||||
self.restart()
|
self.restart()
|
||||||
|
|
||||||
def set_demodulator(self, demodulator):
|
def set_demodulator(self, demodulator):
|
||||||
|
if demodulator in ["usb", "lsb", "cw"]:
|
||||||
|
demodulator = "ssb"
|
||||||
if self.demodulator == demodulator:
|
if self.demodulator == demodulator:
|
||||||
return
|
return
|
||||||
self.demodulator = demodulator
|
self.demodulator = demodulator
|
||||||
@ -523,21 +639,22 @@ class dsp(object):
|
|||||||
def set_offset_freq(self, offset_freq):
|
def set_offset_freq(self, offset_freq):
|
||||||
self.offset_freq = offset_freq
|
self.offset_freq = offset_freq
|
||||||
if self.running:
|
if self.running:
|
||||||
self.modification_lock.acquire()
|
self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
|
||||||
self.shift_pipe_file.write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
|
|
||||||
self.shift_pipe_file.flush()
|
def set_center_freq(self, center_freq):
|
||||||
self.modification_lock.release()
|
# dsp only needs to know this to be able to pass it to decoders in the form of get_operating_freq()
|
||||||
|
self.center_freq = center_freq
|
||||||
|
|
||||||
|
def get_operating_freq(self):
|
||||||
|
return self.center_freq + self.offset_freq
|
||||||
|
|
||||||
def set_bpf(self, low_cut, high_cut):
|
def set_bpf(self, low_cut, high_cut):
|
||||||
self.low_cut = low_cut
|
self.low_cut = low_cut
|
||||||
self.high_cut = high_cut
|
self.high_cut = high_cut
|
||||||
if self.running:
|
if self.running:
|
||||||
self.modification_lock.acquire()
|
self.pipes["bpf_pipe"].write(
|
||||||
self.bpf_pipe_file.write(
|
|
||||||
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
|
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
|
||||||
)
|
)
|
||||||
self.bpf_pipe_file.flush()
|
|
||||||
self.modification_lock.release()
|
|
||||||
|
|
||||||
def get_bpf(self):
|
def get_bpf(self):
|
||||||
return [self.low_cut, self.high_cut]
|
return [self.low_cut, self.high_cut]
|
||||||
@ -550,10 +667,7 @@ class dsp(object):
|
|||||||
# no squelch required on digital voice modes
|
# no squelch required on digital voice modes
|
||||||
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() else self.squelch_level
|
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() else self.squelch_level
|
||||||
if self.running:
|
if self.running:
|
||||||
self.modification_lock.acquire()
|
self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
|
||||||
self.squelch_pipe_file.write("%g\n" % (self.convertToLinear(actual_squelch)))
|
|
||||||
self.squelch_pipe_file.flush()
|
|
||||||
self.modification_lock.release()
|
|
||||||
|
|
||||||
def set_unvoiced_quality(self, q):
|
def set_unvoiced_quality(self, q):
|
||||||
self.unvoiced_quality = q
|
self.unvoiced_quality = q
|
||||||
@ -563,39 +677,33 @@ class dsp(object):
|
|||||||
return self.unvoiced_quality
|
return self.unvoiced_quality
|
||||||
|
|
||||||
def set_dmr_filter(self, filter):
|
def set_dmr_filter(self, filter):
|
||||||
if self.dmr_control_pipe_file:
|
if self.has_pipe("dmr_control_pipe"):
|
||||||
self.dmr_control_pipe_file.write("{0}\n".format(filter))
|
self.pipes["dmr_control_pipe"].write("{0}\n".format(filter))
|
||||||
self.dmr_control_pipe_file.flush()
|
|
||||||
|
|
||||||
def mkfifo(self, path):
|
|
||||||
try:
|
|
||||||
os.unlink(path)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
os.mkfifo(path)
|
|
||||||
|
|
||||||
def ddc_transition_bw(self):
|
def ddc_transition_bw(self):
|
||||||
return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate))
|
return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate))
|
||||||
|
|
||||||
def try_create_pipes(self, pipe_names, command_base):
|
def try_create_pipes(self, pipe_names, command_base):
|
||||||
for pipe_name in pipe_names:
|
for pipe_name, pipe_type in pipe_names.items():
|
||||||
if "{" + pipe_name + "}" in command_base:
|
if "{" + pipe_name + "}" in command_base:
|
||||||
setattr(self, pipe_name, self.pipe_base_path + pipe_name)
|
p = self.pipe_base_path + pipe_name
|
||||||
self.mkfifo(getattr(self, pipe_name))
|
encoding = None
|
||||||
|
# TODO make digiham output unicode and then change this here
|
||||||
|
# the whole pipe enoding feature onlye exists because of this
|
||||||
|
if pipe_name == "meta_pipe":
|
||||||
|
encoding = "cp437"
|
||||||
|
self.pipes[pipe_name] = Pipe.create(p, pipe_type, encoding=encoding)
|
||||||
else:
|
else:
|
||||||
setattr(self, pipe_name, None)
|
self.pipes[pipe_name] = None
|
||||||
|
|
||||||
|
def has_pipe(self, name):
|
||||||
|
return name in self.pipes and self.pipes[name] is not None
|
||||||
|
|
||||||
def try_delete_pipes(self, pipe_names):
|
def try_delete_pipes(self, pipe_names):
|
||||||
for pipe_name in pipe_names:
|
for pipe_name in pipe_names:
|
||||||
pipe_path = getattr(self, pipe_name, None)
|
if self.has_pipe(pipe_name):
|
||||||
if pipe_path:
|
self.pipes[pipe_name].close()
|
||||||
try:
|
self.pipes[pipe_name] = None
|
||||||
os.unlink(pipe_path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
# it seems like we keep calling this twice. no idea why, but we don't need the resulting error.
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
logger.exception("try_delete_pipes()")
|
|
||||||
|
|
||||||
def try_create_configs(self, command):
|
def try_create_configs(self, command):
|
||||||
if "{direwolf_config}" in command:
|
if "{direwolf_config}" in command:
|
||||||
@ -622,23 +730,36 @@ class dsp(object):
|
|||||||
self.direwolf_config = None
|
self.direwolf_config = None
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.modification_lock.acquire()
|
with self.modification_lock:
|
||||||
if self.running:
|
if self.running:
|
||||||
self.modification_lock.release()
|
|
||||||
return
|
return
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
command_base = " | ".join(self.chain(self.demodulator))
|
command_base = " | ".join(self.chain(self.demodulator))
|
||||||
|
|
||||||
# create control pipes for csdr
|
# create control pipes for csdr
|
||||||
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self))
|
|
||||||
|
|
||||||
self.try_create_pipes(self.pipe_names, command_base)
|
self.try_create_pipes(self.pipe_names, command_base)
|
||||||
|
|
||||||
|
# send initial config through the pipes
|
||||||
|
if self.has_pipe("bpf_pipe"):
|
||||||
|
self.set_bpf(self.low_cut, self.high_cut)
|
||||||
|
if self.has_pipe("shift_pipe"):
|
||||||
|
self.set_offset_freq(self.offset_freq)
|
||||||
|
if self.has_pipe("squelch_pipe"):
|
||||||
|
self.set_squelch_level(self.squelch_level)
|
||||||
|
if self.has_pipe("dmr_control_pipe"):
|
||||||
|
self.set_dmr_filter(3)
|
||||||
|
|
||||||
# run the command
|
# run the command
|
||||||
command = command_base.format(
|
command = command_base.format(
|
||||||
bpf_pipe=self.bpf_pipe,
|
bpf_pipe=self.pipes["bpf_pipe"],
|
||||||
shift_pipe=self.shift_pipe,
|
shift_pipe=self.pipes["shift_pipe"],
|
||||||
|
squelch_pipe=self.pipes["squelch_pipe"],
|
||||||
|
smeter_pipe=self.pipes["smeter_pipe"],
|
||||||
|
meta_pipe=self.pipes["meta_pipe"],
|
||||||
|
iqtee_pipe=self.pipes["iqtee_pipe"],
|
||||||
|
iqtee2_pipe=self.pipes["iqtee2_pipe"],
|
||||||
|
dmr_control_pipe=self.pipes["dmr_control_pipe"],
|
||||||
decimation=self.decimation,
|
decimation=self.decimation,
|
||||||
last_decimation=self.last_decimation,
|
last_decimation=self.last_decimation,
|
||||||
fft_size=self.fft_size,
|
fft_size=self.fft_size,
|
||||||
@ -649,15 +770,9 @@ class dsp(object):
|
|||||||
flowcontrol=int(self.samp_rate * 2),
|
flowcontrol=int(self.samp_rate * 2),
|
||||||
start_bufsize=self.base_bufsize * self.decimation,
|
start_bufsize=self.base_bufsize * self.decimation,
|
||||||
nc_port=self.nc_port,
|
nc_port=self.nc_port,
|
||||||
squelch_pipe=self.squelch_pipe,
|
|
||||||
smeter_pipe=self.smeter_pipe,
|
|
||||||
meta_pipe=self.meta_pipe,
|
|
||||||
iqtee_pipe=self.iqtee_pipe,
|
|
||||||
iqtee2_pipe=self.iqtee2_pipe,
|
|
||||||
output_rate=self.get_output_rate(),
|
output_rate=self.get_output_rate(),
|
||||||
smeter_report_every=int(self.if_samp_rate() / 6000),
|
smeter_report_every=int(self.if_samp_rate() / 6000),
|
||||||
unvoiced_quality=self.get_unvoiced_quality(),
|
unvoiced_quality=self.get_unvoiced_quality(),
|
||||||
dmr_control_pipe=self.dmr_control_pipe,
|
|
||||||
audio_rate=self.get_audio_rate(),
|
audio_rate=self.get_audio_rate(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -689,41 +804,20 @@ class dsp(object):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# open control pipes for csdr
|
|
||||||
if self.bpf_pipe:
|
|
||||||
self.bpf_pipe_file = open(self.bpf_pipe, "w")
|
|
||||||
if self.shift_pipe:
|
|
||||||
self.shift_pipe_file = open(self.shift_pipe, "w")
|
|
||||||
if self.squelch_pipe:
|
|
||||||
self.squelch_pipe_file = open(self.squelch_pipe, "w")
|
|
||||||
self.start_secondary_demodulator()
|
self.start_secondary_demodulator()
|
||||||
|
|
||||||
self.modification_lock.release()
|
if self.has_pipe("smeter_pipe"):
|
||||||
|
|
||||||
# send initial config through the pipes
|
|
||||||
if self.squelch_pipe:
|
|
||||||
self.set_squelch_level(self.squelch_level)
|
|
||||||
if self.shift_pipe:
|
|
||||||
self.set_offset_freq(self.offset_freq)
|
|
||||||
if self.bpf_pipe:
|
|
||||||
self.set_bpf(self.low_cut, self.high_cut)
|
|
||||||
if self.smeter_pipe:
|
|
||||||
self.smeter_pipe_file = open(self.smeter_pipe, "r")
|
|
||||||
|
|
||||||
def read_smeter():
|
def read_smeter():
|
||||||
raw = self.smeter_pipe_file.readline()
|
raw = self.pipes["smeter_pipe"].readline()
|
||||||
if len(raw) == 0:
|
if len(raw) == 0:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return float(raw.rstrip("\n"))
|
return float(raw.rstrip("\n"))
|
||||||
|
|
||||||
self.output.send_output("smeter", read_smeter)
|
self.output.send_output("smeter", read_smeter)
|
||||||
if self.meta_pipe != None:
|
if self.has_pipe("meta_pipe"):
|
||||||
# TODO make digiham output unicode and then change this here
|
|
||||||
self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437")
|
|
||||||
|
|
||||||
def read_meta():
|
def read_meta():
|
||||||
raw = self.meta_pipe_file.readline()
|
raw = self.pipes["meta_pipe"].readline()
|
||||||
if len(raw) == 0:
|
if len(raw) == 0:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
@ -731,15 +825,17 @@ class dsp(object):
|
|||||||
|
|
||||||
self.output.send_output("meta", read_meta)
|
self.output.send_output("meta", read_meta)
|
||||||
|
|
||||||
if self.dmr_control_pipe:
|
if self.csdr_dynamic_bufsize:
|
||||||
self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w")
|
self.process.stdout.read(8) # dummy read to skip bufsize & preamble
|
||||||
|
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.modification_lock.acquire()
|
with self.modification_lock:
|
||||||
self.running = False
|
self.running = False
|
||||||
if hasattr(self, "process"):
|
if self.process is not None:
|
||||||
try:
|
try:
|
||||||
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
||||||
|
self.process = None
|
||||||
except ProcessLookupError:
|
except ProcessLookupError:
|
||||||
# been killed by something else, ignore
|
# been killed by something else, ignore
|
||||||
pass
|
pass
|
||||||
@ -747,8 +843,6 @@ class dsp(object):
|
|||||||
|
|
||||||
self.try_delete_pipes(self.pipe_names)
|
self.try_delete_pipes(self.pipe_names)
|
||||||
|
|
||||||
self.modification_lock.release()
|
|
||||||
|
|
||||||
def restart(self):
|
def restart(self):
|
||||||
if not self.running:
|
if not self.running:
|
||||||
return
|
return
|
||||||
@ -757,4 +851,3 @@ class dsp(object):
|
|||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.stop()
|
self.stop()
|
||||||
del self.process
|
|
||||||
|
129
debian/changelog
vendored
129
debian/changelog
vendored
@ -1,5 +1,128 @@
|
|||||||
openwebrx (0.18) UNRELEASED; urgency=low
|
openwebrx (0.19.0) buster focal; urgency=low
|
||||||
|
* Fix direwolf connection setup by implementing a retry loop
|
||||||
|
* Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector
|
||||||
|
* OSM maps instead of Google when google_maps_api_key is not set (thanks
|
||||||
|
@jquagga)
|
||||||
|
* Improved logic to pass parameters to soapy devices.
|
||||||
|
- `rtl_sdr_soapy`: added support for `bias_tee`
|
||||||
|
- `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch`
|
||||||
|
- `airspy`: added support for `bitpack`
|
||||||
|
* Added support for Perseus-SDR devices, (thanks @amontefusco)
|
||||||
|
* Property System has been rewritten so that defaults on sdr behave as
|
||||||
|
expected
|
||||||
|
* Waterfall range auto-adjustment now only takes the center 80% of the
|
||||||
|
spectrum into account, which should work better with SDRs that oversample
|
||||||
|
or have rather flat filter curves towards the spectrum edges
|
||||||
|
* Bugfix for negative network usage
|
||||||
|
* FiFi SDR: prevent arecord from shutting down after 2GB of data has been
|
||||||
|
sent
|
||||||
|
* Added support for bias tee control on rtl_sdr devices
|
||||||
|
* All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
|
||||||
|
* `rtl_sdr` type now also supports the `direct_sampling` option
|
||||||
|
* Added decoding implementation for for digimode "JS8Call" (requires an
|
||||||
|
installation of js8call and the js8py library)
|
||||||
|
* Reorganization of the frontend demodulator code
|
||||||
|
* Improve receiver load time by concatenating javascript assets
|
||||||
|
* HackRF support is now based on SoapyHackRF
|
||||||
|
* Removed sdr.hu server listing support since the site has been shut down
|
||||||
|
* Added support for Radioberry 2 Rasbperry Pi SDR Cape
|
||||||
|
|
||||||
* Initial release.
|
-- Jakob Ketterl <jakob.ketterl@gmx.de> Mon, 01 Jun 2020 17:02:00 +0000
|
||||||
|
|
||||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 08 Dec 2019 12:35:48 +0000
|
openwebrx (0.18.0) buster; urgency=low
|
||||||
|
|
||||||
|
* Compression, resampling and filtering in the frontend have been rewritten
|
||||||
|
in javascript, sdr.js has been removed
|
||||||
|
* Decoding of Pocsag modulation is now possible
|
||||||
|
* Removed the 3D waterfall since it had no real application and required ~1MB
|
||||||
|
of javascript code to be downloaded
|
||||||
|
* Improved the frontend handling of the "too many users" scenario
|
||||||
|
* PSK63 digimode is now available (same decoding pipeline as PSK31, but with
|
||||||
|
adopted parameters)
|
||||||
|
* The frequency can now be manipulated with the mousewheel, which should
|
||||||
|
allow the user to tune more precise. The tuning step size is determined by
|
||||||
|
the digit the mouse cursor is hovering over.
|
||||||
|
* Clicking on the frequency now opens an input for direct frequency selection
|
||||||
|
* URL hashes have been fixed and improved: They are now updated
|
||||||
|
automatically, so a shared URL will include frequency and demodulator,
|
||||||
|
which allows for improved sharing and linking.
|
||||||
|
* New daylight scheduler for background decoding, allows profiles to be
|
||||||
|
selected by local sunrise / sunset times
|
||||||
|
* The owrx_connector is now the default way of communicating with sdr
|
||||||
|
devices. The old sdr types have been replaced, all `_connector` suffixes on
|
||||||
|
the type must be removed!
|
||||||
|
* The sources have been refactored, making it a lot easier to add support for
|
||||||
|
other devices
|
||||||
|
* SDR device failure handling has been improved, including user feedback
|
||||||
|
* New devices supported:
|
||||||
|
* wsjt-x updated to 2.1.2
|
||||||
|
* The rtl_tcp compatibility mode of the owrx_connector is now configurable
|
||||||
|
using the `rtltcp_compat` flag
|
||||||
|
* explicit device filter for soapy devices for multi-device setups
|
||||||
|
* compatibility fixes for safari browsers (ios and mac)
|
||||||
|
* Offset tuning using the `lfo_offset` has been reworked in a way that
|
||||||
|
`center_freq` has to be set to the frequency you actually want to listen
|
||||||
|
to. If you're using an `lfo_offset` already, you will probably need to
|
||||||
|
change its sign.
|
||||||
|
* `initial_squelch_level` can now be set on each profile.
|
||||||
|
* Part of the frontend code has been reworked
|
||||||
|
- Audio buffer minimums have been completely stripped. As a result, you
|
||||||
|
should get better latency. Unfortunately, this also means there will be
|
||||||
|
some skipping when audio starts.
|
||||||
|
- Now also supports AudioWorklets (for those browser that have it).
|
||||||
|
- Mousewheel controls for the receiver sliders
|
||||||
|
* Error handling for failed SDR devices
|
||||||
|
* One of the most-requested features is finally coming to OpenWebRX:
|
||||||
|
Bookmarks (sometimes also referred to as labels).
|
||||||
|
There's two kinds of bookmarks available:
|
||||||
|
- Serverside bookmarks that are set up by the receiver administrator.
|
||||||
|
Check the file `bookmarks.json` for examples!
|
||||||
|
- Clientside bookmarks which every user can store for themselves. They are
|
||||||
|
stored in the browser's localStorage.
|
||||||
|
* Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is
|
||||||
|
now possible. Please have a look at the configuration on how to set it up.
|
||||||
|
* Websocket communication has been overhauled in large parts. It should now
|
||||||
|
be more reliable, and failing connections should now have no impact on
|
||||||
|
other users.
|
||||||
|
* Profile scheduling allows to set up band-hopping if you are running
|
||||||
|
background services.
|
||||||
|
* APRS now has the ability to show symbols on the map, if a corresponding
|
||||||
|
symbol set has been installed. Check the config!
|
||||||
|
* Debug logging has been disabled in a handful of modules, expect vastly
|
||||||
|
reduced output on the shell.
|
||||||
|
* New set of APRS-related features
|
||||||
|
- Decode Packet transmissions using direwolf (1k2 only for now)
|
||||||
|
- APRS packets are mostly decoded and shown both in a new panel and on the
|
||||||
|
map
|
||||||
|
- APRS is also available as a background service
|
||||||
|
- direwolfs I-gate functionality can be enabled, which allows your receiver
|
||||||
|
to work as a receive-only I-gate for the APRS network in the background
|
||||||
|
* Demodulation for background services has been optimized to use less total
|
||||||
|
bandwidth, saving CPU
|
||||||
|
* More metrics have been added; they can be used together with collectd and
|
||||||
|
its curl_json plugin for now, with some limitations.
|
||||||
|
* New bandplan feature, the first thing visible is the "dial" indicator that
|
||||||
|
brings you right to the dial frequency for digital modes
|
||||||
|
* fixed some bugs in the websocket communication which broke the map
|
||||||
|
* WSJT-X integration (FT8, FT4, WSPR, JT65, JT9 using wsjt-x demodulators)
|
||||||
|
* New Map Feature that shows both decoded grid squares from FT8 and Locations
|
||||||
|
decoded from YSF digital voice
|
||||||
|
* New Feature report that will show what functionality is available
|
||||||
|
* major rework on the openwebrx core
|
||||||
|
* Support of multiple SDR devices simultaneously
|
||||||
|
* Support for multiple profiles per SDR that allow the user to listen to
|
||||||
|
different frequencies
|
||||||
|
* Support for digital voice decoding
|
||||||
|
* Feature detection that will disable functionality when dependencies are not
|
||||||
|
available (if you're missing the digital
|
||||||
|
buttons, this is probably why)
|
||||||
|
* Support added for the following SDR sources:
|
||||||
|
- LimeSDR (`"type": "lime_sdr"`)
|
||||||
|
- PlutoSDR (`"type": "pluto_sdr"`)
|
||||||
|
- RTL_SDR via Soapy (`"type": "rtl_sdr_soapy"`) on special request to allow
|
||||||
|
use of the direct sampling mode
|
||||||
|
- SoapyRemote (`"type": "soapy_remote"`)
|
||||||
|
- FiFiSDR (`"type": "fifi_sdr"`)
|
||||||
|
- airspyhf devices (Airspy HF+ / Discovery) (`"type": "airspyhf"`)
|
||||||
|
|
||||||
|
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 18 Feb 2020 20:09:00 +0000
|
||||||
|
9
debian/control
vendored
9
debian/control
vendored
@ -3,11 +3,14 @@ Maintainer: Jakob Ketterl <jakob.ketterl@gmx.de>
|
|||||||
Section: hamradio
|
Section: hamradio
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Standards-Version: 4.2.0
|
Standards-Version: 4.2.0
|
||||||
Build-Depends: debhelper (>= 10), dh-python, python3 (>= 3.5), dh-systemd (>= 1.5)
|
Build-Depends: debhelper (>= 11), dh-python, python3-all (>= 3.5), python3-setuptools
|
||||||
|
Homepage: https://www.openwebrx.de/
|
||||||
|
Vcs-Browser: https://github.com/jketterl/openwebrx
|
||||||
|
Vcs-Git: https://github.com/jketterl/openwebrx.git
|
||||||
|
|
||||||
Package: openwebrx
|
Package: openwebrx
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Depends: python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.1), ${python3:Depends}
|
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.2), python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends}
|
||||||
Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx
|
Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools
|
||||||
Description: multi-user web sdr
|
Description: multi-user web sdr
|
||||||
Open source, multi-user SDR receiver with a web interface
|
Open source, multi-user SDR receiver with a web interface
|
1
debian/openwebrx.install
vendored
1
debian/openwebrx.install
vendored
@ -1,4 +1,5 @@
|
|||||||
config_webrx.py etc/openwebrx/
|
config_webrx.py etc/openwebrx/
|
||||||
bands.json etc/openwebrx/
|
bands.json etc/openwebrx/
|
||||||
bookmarks.json etc/openwebrx/
|
bookmarks.json etc/openwebrx/
|
||||||
|
users.json etc/openwebrx/
|
||||||
systemd/openwebrx.service lib/systemd/system/
|
systemd/openwebrx.service lib/systemd/system/
|
7
debian/postinst
vendored
Executable file
7
debian/postinst
vendored
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
adduser --system --group --no-create-home --home /nonexistant openwebrx
|
||||||
|
usermod -aG plugdev openwebrx
|
||||||
|
|
||||||
|
#DEBHELPER#
|
@ -2,9 +2,7 @@ ARG ARCHTAG
|
|||||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||||
|
|
||||||
ADD docker/scripts/install-dependencies-airspy.sh /
|
ADD docker/scripts/install-dependencies-airspy.sh /
|
||||||
RUN /install-dependencies-airspy.sh
|
RUN /install-dependencies-airspy.sh &&\
|
||||||
RUN rm /install-dependencies-airspy.sh
|
rm /install-dependencies-airspy.sh
|
||||||
|
|
||||||
ADD docker/scripts/install-connectors.sh /
|
ADD . /opt/openwebrx
|
||||||
RUN /install-connectors.sh
|
|
||||||
RUN rm /install-connectors.sh
|
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
FROM alpine:3.10
|
FROM debian:buster-slim
|
||||||
|
|
||||||
RUN apk add --no-cache bash
|
ADD docker/files/js8call/js8call-hamlib.patch /
|
||||||
|
ADD docker/files/wsjtx/*.patch /
|
||||||
RUN ln -s /usr/local/lib /usr/local/lib64
|
|
||||||
|
|
||||||
ADD docker/scripts/direwolf-1.5.patch /
|
|
||||||
ADD docker/scripts/install-dependencies.sh /
|
ADD docker/scripts/install-dependencies.sh /
|
||||||
RUN /install-dependencies.sh
|
RUN /install-dependencies.sh && \
|
||||||
RUN rm /install-dependencies.sh
|
rm /install-dependencies.sh && \
|
||||||
|
rm /*.patch
|
||||||
|
|
||||||
ADD . /opt/openwebrx
|
ENTRYPOINT ["/init"]
|
||||||
|
|
||||||
WORKDIR /opt/openwebrx
|
WORKDIR /opt/openwebrx
|
||||||
|
|
||||||
VOLUME /etc/openwebrx
|
VOLUME /etc/openwebrx
|
||||||
|
|
||||||
ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ]
|
CMD [ "/opt/openwebrx/docker/scripts/run.sh" ]
|
||||||
|
|
||||||
EXPOSE 8073
|
EXPOSE 8073
|
||||||
|
@ -2,16 +2,25 @@ ARG ARCHTAG
|
|||||||
FROM openwebrx-base:$ARCHTAG
|
FROM openwebrx-base:$ARCHTAG
|
||||||
|
|
||||||
ADD docker/scripts/install-dependencies-*.sh /
|
ADD docker/scripts/install-dependencies-*.sh /
|
||||||
ADD docker/scripts/install-lib.*.patch /
|
ADD docker/files/sdrplay/install-lib.*.patch /
|
||||||
|
|
||||||
RUN /install-dependencies-rtlsdr.sh
|
RUN /install-dependencies-rtlsdr.sh &&\
|
||||||
RUN /install-dependencies-hackrf.sh
|
/install-dependencies-soapysdr.sh &&\
|
||||||
RUN /install-dependencies-soapysdr.sh
|
/install-dependencies-hackrf.sh &&\
|
||||||
RUN /install-dependencies-sdrplay.sh
|
/install-dependencies-sdrplay.sh &&\
|
||||||
RUN /install-dependencies-airspy.sh
|
/install-dependencies-airspy.sh &&\
|
||||||
RUN /install-dependencies-rtlsdr-soapy.sh
|
/install-dependencies-rtlsdr-soapy.sh &&\
|
||||||
RUN rm /install-dependencies-*.sh
|
/install-dependencies-plutosdr.sh &&\
|
||||||
|
/install-dependencies-limesdr.sh &&\
|
||||||
|
/install-dependencies-soapyremote.sh &&\
|
||||||
|
/install-dependencies-perseus.sh &&\
|
||||||
|
rm /install-dependencies-*.sh &&\
|
||||||
|
rm /install-lib.*.patch
|
||||||
|
|
||||||
ADD docker/scripts/install-connectors.sh /
|
ADD docker/scripts/install-connectors.sh /
|
||||||
RUN /install-connectors.sh
|
RUN /install-connectors.sh &&\
|
||||||
RUN rm /install-connectors.sh
|
rm /install-connectors.sh
|
||||||
|
|
||||||
|
ADD docker/files/services/sdrplay /etc/services.d/sdrplay
|
||||||
|
|
||||||
|
ADD . /opt/openwebrx
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
ARG ARCHTAG
|
ARG ARCHTAG
|
||||||
FROM openwebrx-base:$ARCHTAG
|
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||||
|
|
||||||
ADD docker/scripts/install-dependencies-hackrf.sh /
|
ADD docker/scripts/install-dependencies-hackrf.sh /
|
||||||
RUN /install-dependencies-hackrf.sh
|
RUN /install-dependencies-hackrf.sh &&\
|
||||||
RUN rm /install-dependencies-hackrf.sh
|
rm /install-dependencies-hackrf.sh
|
||||||
|
|
||||||
|
ADD . /opt/openwebrx
|
||||||
|
8
docker/Dockerfiles/Dockerfile-limesdr
Normal file
8
docker/Dockerfiles/Dockerfile-limesdr
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ARG ARCHTAG
|
||||||
|
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||||
|
|
||||||
|
ADD docker/scripts/install-dependencies-limesdr.sh /
|
||||||
|
RUN /install-dependencies-limesdr.sh &&\
|
||||||
|
rm /install-dependencies-limesdr.sh
|
||||||
|
|
||||||
|
ADD . /opt/openwebrx
|
8
docker/Dockerfiles/Dockerfile-perseus
Normal file
8
docker/Dockerfiles/Dockerfile-perseus
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ARG ARCHTAG
|
||||||
|
FROM openwebrx-base:$ARCHTAG
|
||||||
|
|
||||||
|
ADD docker/scripts/install-dependencies-perseus.sh /
|
||||||
|
RUN /install-dependencies-perseus.sh &&\
|
||||||
|
rm /install-dependencies-perseus.sh
|
||||||
|
|
||||||
|
ADD . /opt/openwebrx
|
8
docker/Dockerfiles/Dockerfile-plutosdr
Normal file
8
docker/Dockerfiles/Dockerfile-plutosdr
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ARG ARCHTAG
|
||||||
|
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||||
|
|
||||||
|
ADD docker/scripts/install-dependencies-plutosdr.sh /
|
||||||
|
RUN /install-dependencies-plutosdr.sh &&\
|
||||||
|
rm /install-dependencies-plutosdr.sh
|
||||||
|
|
||||||
|
ADD . /opt/openwebrx
|
@ -2,9 +2,11 @@ ARG ARCHTAG
|
|||||||
FROM openwebrx-base:$ARCHTAG
|
FROM openwebrx-base:$ARCHTAG
|
||||||
|
|
||||||
ADD docker/scripts/install-dependencies-rtlsdr.sh /
|
ADD docker/scripts/install-dependencies-rtlsdr.sh /
|
||||||
RUN /install-dependencies-rtlsdr.sh
|
|
||||||
RUN rm /install-dependencies-rtlsdr.sh
|
|
||||||
|
|
||||||
ADD docker/scripts/install-connectors.sh /
|
ADD docker/scripts/install-connectors.sh /
|
||||||
RUN /install-connectors.sh
|
|
||||||
RUN rm /install-connectors.sh
|
RUN /install-dependencies-rtlsdr.sh &&\
|
||||||
|
rm /install-dependencies-rtlsdr.sh &&\
|
||||||
|
/install-connectors.sh &&\
|
||||||
|
rm /install-connectors.sh
|
||||||
|
|
||||||
|
ADD . /opt/openwebrx
|
||||||
|
@ -2,9 +2,7 @@ ARG ARCHTAG
|
|||||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||||
|
|
||||||
ADD docker/scripts/install-dependencies-rtlsdr-soapy.sh /
|
ADD docker/scripts/install-dependencies-rtlsdr-soapy.sh /
|
||||||
RUN /install-dependencies-rtlsdr-soapy.sh
|
RUN /install-dependencies-rtlsdr-soapy.sh &&\
|
||||||
RUN rm /install-dependencies-rtlsdr-soapy.sh
|
rm /install-dependencies-rtlsdr-soapy.sh
|
||||||
|
|
||||||
ADD docker/scripts/install-connectors.sh /
|
ADD . /opt/openwebrx
|
||||||
RUN /install-connectors.sh
|
|
||||||
RUN rm /install-connectors.sh
|
|
||||||
|
@ -2,10 +2,11 @@ ARG ARCHTAG
|
|||||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||||
|
|
||||||
ADD docker/scripts/install-dependencies-sdrplay.sh /
|
ADD docker/scripts/install-dependencies-sdrplay.sh /
|
||||||
ADD docker/scripts/install-lib.*.patch /
|
ADD docker/files/sdrplay/install-lib.*.patch /
|
||||||
RUN /install-dependencies-sdrplay.sh
|
RUN /install-dependencies-sdrplay.sh &&\
|
||||||
RUN rm /install-dependencies-sdrplay.sh
|
rm /install-dependencies-sdrplay.sh &&\
|
||||||
|
rm /install-lib.*.patch
|
||||||
|
|
||||||
ADD docker/scripts/install-connectors.sh /
|
ADD docker/files/services/sdrplay /etc/services.d/sdrplay
|
||||||
RUN /install-connectors.sh
|
|
||||||
RUN rm /install-connectors.sh
|
ADD . /opt/openwebrx
|
||||||
|
8
docker/Dockerfiles/Dockerfile-soapyremote
Normal file
8
docker/Dockerfiles/Dockerfile-soapyremote
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ARG ARCHTAG
|
||||||
|
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||||
|
|
||||||
|
ADD docker/scripts/install-dependencies-soapyremote.sh /
|
||||||
|
RUN /install-dependencies-soapyremote.sh &&\
|
||||||
|
rm /install-dependencies-soapyremote.sh
|
||||||
|
|
||||||
|
ADD . /opt/openwebrx
|
@ -2,6 +2,8 @@ ARG ARCHTAG
|
|||||||
FROM openwebrx-base:$ARCHTAG
|
FROM openwebrx-base:$ARCHTAG
|
||||||
|
|
||||||
ADD docker/scripts/install-dependencies-soapysdr.sh /
|
ADD docker/scripts/install-dependencies-soapysdr.sh /
|
||||||
RUN /install-dependencies-soapysdr.sh
|
ADD docker/scripts/install-connectors.sh /
|
||||||
RUN rm /install-dependencies-soapysdr.sh
|
RUN /install-dependencies-soapysdr.sh &&\
|
||||||
|
rm /install-dependencies-soapysdr.sh &&\
|
||||||
|
/install-connectors.sh &&\
|
||||||
|
rm /install-connectors.sh
|
||||||
|
5
docker/env
Normal file
5
docker/env
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
ARCH=$(uname -m)
|
||||||
|
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-full openwebrx"
|
||||||
|
ALL_ARCHS="x86_64 armv7l aarch64"
|
||||||
|
TAG=${TAG:-"latest"}
|
||||||
|
ARCHTAG="$TAG-$ARCH"
|
150
docker/files/js8call/js8call-hamlib.patch
Normal file
150
docker/files/js8call/js8call-hamlib.patch
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
diff -ur js8call-orig/CMake/Modules/Findhamlib.cmake js8call/CMake/Modules/Findhamlib.cmake
|
||||||
|
--- js8call-orig/CMake/Modules/Findhamlib.cmake 2020-05-28 00:10:13.386429366 +0200
|
||||||
|
+++ js8call/CMake/Modules/Findhamlib.cmake 2020-05-28 00:10:34.339623106 +0200
|
||||||
|
@@ -78,4 +78,4 @@
|
||||||
|
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
|
||||||
|
# TRUE if all listed variables are TRUE
|
||||||
|
include (FindPackageHandleStandardArgs)
|
||||||
|
-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
|
||||||
|
+find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
|
||||||
|
diff -ur js8call-orig/CMakeLists.txt js8call/CMakeLists.txt
|
||||||
|
--- js8call-orig/CMakeLists.txt 2020-05-28 00:10:13.393095987 +0200
|
||||||
|
+++ js8call/CMakeLists.txt 2020-05-28 00:12:09.925653037 +0200
|
||||||
|
@@ -683,7 +683,7 @@
|
||||||
|
#
|
||||||
|
# libhamlib setup
|
||||||
|
#
|
||||||
|
-set (hamlib_STATIC 1)
|
||||||
|
+set (hamlib_STATIC 0)
|
||||||
|
find_package (hamlib 3 REQUIRED)
|
||||||
|
find_program (RIGCTL_EXE rigctl)
|
||||||
|
find_program (RIGCTLD_EXE rigctld)
|
||||||
|
@@ -1033,55 +1033,6 @@
|
||||||
|
target_link_libraries (js8 wsjt_fort wsjt_cxx Qt5::Core)
|
||||||
|
endif (${OPENMP_FOUND} OR APPLE)
|
||||||
|
|
||||||
|
-# build the main application
|
||||||
|
-add_executable (js8call MACOSX_BUNDLE
|
||||||
|
- ${sqlite3_CSRCS}
|
||||||
|
- ${wsjtx_CXXSRCS}
|
||||||
|
- ${wsjtx_GENUISRCS}
|
||||||
|
- wsjtx.rc
|
||||||
|
- ${WSJTX_ICON_FILE}
|
||||||
|
- ${wsjtx_RESOURCES_RCC}
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
-if (WSJT_CREATE_WINMAIN)
|
||||||
|
- set_target_properties (js8call PROPERTIES WIN32_EXECUTABLE ON)
|
||||||
|
-endif (WSJT_CREATE_WINMAIN)
|
||||||
|
-
|
||||||
|
-set_target_properties (js8call PROPERTIES
|
||||||
|
- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in"
|
||||||
|
- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}"
|
||||||
|
- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}"
|
||||||
|
- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION}
|
||||||
|
- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}"
|
||||||
|
- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}"
|
||||||
|
- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
|
||||||
|
- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
|
||||||
|
- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
|
||||||
|
- MACOSX_BUNDLE_GUI_IDENTIFIER "org.kn4crd.js8call"
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
-target_include_directories (js8call PRIVATE ${FFTW3_INCLUDE_DIRS})
|
||||||
|
-if (APPLE)
|
||||||
|
- target_link_libraries (js8call wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
|
||||||
|
-else ()
|
||||||
|
- target_link_libraries (js8call wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
|
||||||
|
- if (OpenMP_C_FLAGS)
|
||||||
|
- set_target_properties (js8call PROPERTIES
|
||||||
|
- COMPILE_FLAGS "${OpenMP_C_FLAGS}"
|
||||||
|
- LINK_FLAGS "${OpenMP_C_FLAGS}"
|
||||||
|
- )
|
||||||
|
- endif ()
|
||||||
|
- set_target_properties (js8call PROPERTIES
|
||||||
|
- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp
|
||||||
|
- )
|
||||||
|
- if (WIN32)
|
||||||
|
- set_target_properties (js8call PROPERTIES
|
||||||
|
- LINK_FLAGS -Wl,--stack,16777216
|
||||||
|
- )
|
||||||
|
- endif ()
|
||||||
|
-endif ()
|
||||||
|
-qt5_use_modules (js8call SerialPort) # not sure why the interface link library syntax above doesn't work
|
||||||
|
-
|
||||||
|
# if (UNIX)
|
||||||
|
# if (NOT WSJT_SKIP_MANPAGES)
|
||||||
|
# add_subdirectory (manpages)
|
||||||
|
@@ -1097,38 +1048,10 @@
|
||||||
|
#
|
||||||
|
# installation
|
||||||
|
#
|
||||||
|
-install (TARGETS js8call
|
||||||
|
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||||
|
- BUNDLE DESTINATION . COMPONENT runtime
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
install (TARGETS js8 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||||
|
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||||
|
)
|
||||||
|
|
||||||
|
-install (PROGRAMS
|
||||||
|
- ${RIGCTL_EXE}
|
||||||
|
- DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
- #COMPONENT runtime
|
||||||
|
- RENAME rigctl-local${CMAKE_EXECUTABLE_SUFFIX}
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
-install (PROGRAMS
|
||||||
|
- ${RIGCTLD_EXE}
|
||||||
|
- DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
- #COMPONENT runtime
|
||||||
|
- RENAME rigctld-local${CMAKE_EXECUTABLE_SUFFIX}
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
-install (FILES
|
||||||
|
- README
|
||||||
|
- COPYING
|
||||||
|
- INSTALL
|
||||||
|
- INSTALL-WSJTX
|
||||||
|
- DESTINATION ${CMAKE_INSTALL_DOCDIR}
|
||||||
|
- #COMPONENT runtime
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
install (FILES
|
||||||
|
contrib/Ephemeris/JPLEPH
|
||||||
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
|
||||||
|
@@ -1182,32 +1105,6 @@
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
|
||||||
|
)
|
||||||
|
|
||||||
|
-
|
||||||
|
-if (NOT WIN32 AND NOT APPLE)
|
||||||
|
- # install a desktop file so js8call appears in the application start
|
||||||
|
- # menu with an icon
|
||||||
|
- install (
|
||||||
|
- FILES js8call.desktop
|
||||||
|
- DESTINATION /usr/share/applications
|
||||||
|
- #COMPONENT runtime
|
||||||
|
- )
|
||||||
|
- install (
|
||||||
|
- FILES icons/Unix/js8call_icon.png
|
||||||
|
- DESTINATION /usr/share/pixmaps
|
||||||
|
- #COMPONENT runtime
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
- IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/opt/js8call")
|
||||||
|
- execute_process(COMMAND ln -s /opt/js8call/bin/js8call ljs8call)
|
||||||
|
-
|
||||||
|
- install(FILES
|
||||||
|
- ${CMAKE_BINARY_DIR}/ljs8call DESTINATION /usr/bin/ RENAME js8call
|
||||||
|
- #COMPONENT runtime
|
||||||
|
- )
|
||||||
|
- endif()
|
||||||
|
-endif (NOT WIN32 AND NOT APPLE)
|
||||||
|
-
|
||||||
|
-
|
||||||
|
#
|
||||||
|
# bundle fixup only done in Release or MinSizeRel configurations
|
||||||
|
#
|
||||||
|
Only in js8call/: .idea
|
23
docker/files/sdrplay/install-lib.aarch64.patch
Normal file
23
docker/files/sdrplay/install-lib.aarch64.patch
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
|
||||||
|
--- sdrplay-orig/install_lib.sh 2020-05-24 14:30:06.022483867 +0000
|
||||||
|
+++ sdrplay/install_lib.sh 2020-05-24 14:30:49.093435726 +0000
|
||||||
|
@@ -4,19 +4,6 @@
|
||||||
|
export MAJVERS="3"
|
||||||
|
|
||||||
|
echo "Installing SDRplay RSP API library ${VERS}..."
|
||||||
|
-read -p "Press RETURN to view the license agreement" ret
|
||||||
|
-
|
||||||
|
-more sdrplay_license.txt
|
||||||
|
-
|
||||||
|
-while true; do
|
||||||
|
- echo "Press y and RETURN to accept the license agreement and continue with"
|
||||||
|
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
|
||||||
|
- case $yn in
|
||||||
|
- [Yy]* ) break;;
|
||||||
|
- [Nn]* ) exit;;
|
||||||
|
- * ) echo "Please answer y or n";;
|
||||||
|
- esac
|
||||||
|
-done
|
||||||
|
|
||||||
|
export ARCH=`uname -m`
|
||||||
|
|
40
docker/files/sdrplay/install-lib.armv7l.patch
Normal file
40
docker/files/sdrplay/install-lib.armv7l.patch
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
|
||||||
|
--- sdrplay-orig/install_lib.sh 2020-05-24 14:13:04.561271707 +0000
|
||||||
|
+++ sdrplay/install_lib.sh 2020-05-24 14:16:20.068329040 +0000
|
||||||
|
@@ -4,19 +4,6 @@
|
||||||
|
MAJVERS="3"
|
||||||
|
|
||||||
|
echo "Installing SDRplay RSP API library ${VERS}..."
|
||||||
|
-read -p "Press RETURN to view the license agreement" ret
|
||||||
|
-
|
||||||
|
-more sdrplay_license.txt
|
||||||
|
-
|
||||||
|
-while true; do
|
||||||
|
- echo "Press y and RETURN to accept the license agreement and continue with"
|
||||||
|
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
|
||||||
|
- case $yn in
|
||||||
|
- [Yy]* ) break;;
|
||||||
|
- [Nn]* ) exit;;
|
||||||
|
- * ) echo "Please answer y or n";;
|
||||||
|
- esac
|
||||||
|
-done
|
||||||
|
|
||||||
|
ARCH=`uname -m`
|
||||||
|
|
||||||
|
@@ -141,16 +128,6 @@
|
||||||
|
echo "SDRplay API ${VERS} Installation Finished"
|
||||||
|
echo " "
|
||||||
|
|
||||||
|
-while true; do
|
||||||
|
- echo "Would you like to add SDRplay USB IDs to the local database for easier
|
||||||
|
-"
|
||||||
|
- read -p "identification in applications such as lsusb? [y/n] " yn
|
||||||
|
- case $yn in
|
||||||
|
- [Yy]* ) break;;
|
||||||
|
- [Nn]* ) exit;;
|
||||||
|
- * ) echo "Please answer y or n";;
|
||||||
|
- esac
|
||||||
|
-done
|
||||||
|
sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/.
|
||||||
|
sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh
|
||||||
|
sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/.
|
39
docker/files/sdrplay/install-lib.x86_64.patch
Normal file
39
docker/files/sdrplay/install-lib.x86_64.patch
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
|
||||||
|
--- sdrplay-orig/install_lib.sh 2020-05-24 13:56:56.622000041 +0000
|
||||||
|
+++ sdrplay/install_lib.sh 2020-05-24 13:58:51.837801559 +0000
|
||||||
|
@@ -4,19 +4,6 @@
|
||||||
|
MAJVERS="3"
|
||||||
|
|
||||||
|
echo "Installing SDRplay RSP API library ${VERS}..."
|
||||||
|
-read -p "Press RETURN to view the license agreement" ret
|
||||||
|
-
|
||||||
|
-more sdrplay_license.txt
|
||||||
|
-
|
||||||
|
-while true; do
|
||||||
|
- echo "Press y and RETURN to accept the license agreement and continue with"
|
||||||
|
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
|
||||||
|
- case $yn in
|
||||||
|
- [Yy]* ) break;;
|
||||||
|
- [Nn]* ) exit;;
|
||||||
|
- * ) echo "Please answer y or n";;
|
||||||
|
- esac
|
||||||
|
-done
|
||||||
|
|
||||||
|
ARCH=`uname -m`
|
||||||
|
OSDIST="Unknown"
|
||||||
|
@@ -157,15 +144,6 @@
|
||||||
|
echo " "
|
||||||
|
echo "SDRplay API ${VERS} Installation Finished"
|
||||||
|
echo " "
|
||||||
|
-while true; do
|
||||||
|
- echo "Would you like to add SDRplay USB IDs to the local database for easier"
|
||||||
|
- read -p "identification in applications such as lsusb? [y/n] " yn
|
||||||
|
- case $yn in
|
||||||
|
- [Yy]* ) break;;
|
||||||
|
- [Nn]* ) exit;;
|
||||||
|
- * ) echo "Please answer y or n";;
|
||||||
|
- esac
|
||||||
|
-done
|
||||||
|
sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/.
|
||||||
|
sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh
|
||||||
|
sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/.
|
2
docker/files/services/sdrplay/run
Executable file
2
docker/files/services/sdrplay/run
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/execlineb -P
|
||||||
|
/usr/local/bin/sdrplay_apiService
|
43
docker/files/wsjtx/wsjtx-hamlib.patch
Normal file
43
docker/files/wsjtx/wsjtx-hamlib.patch
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
--- CMakeLists.txt 2020-05-25 19:26:41.423517236 +0200
|
||||||
|
+++ CMakeLists.txt 2020-05-25 19:11:36.116236231 +0200
|
||||||
|
@@ -79,24 +79,6 @@
|
||||||
|
|
||||||
|
include (ExternalProject)
|
||||||
|
|
||||||
|
-
|
||||||
|
-#
|
||||||
|
-# build and install hamlib locally so it can be referenced by the
|
||||||
|
-# WSJT-X build
|
||||||
|
-#
|
||||||
|
-ExternalProject_Add (hamlib
|
||||||
|
- GIT_REPOSITORY ${hamlib_repo}
|
||||||
|
- GIT_TAG ${hamlib_TAG}
|
||||||
|
- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/hamlib.tgz
|
||||||
|
- URL_HASH MD5=${hamlib_md5sum}
|
||||||
|
- UPDATE_COMMAND ./bootstrap
|
||||||
|
- PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch
|
||||||
|
- CONFIGURE_COMMAND <SOURCE_DIR>/configure --prefix=<INSTALL_DIR> --disable-shared --enable-static --without-cxx-binding ${EXTRA_FLAGS} # LIBUSB_LIBS=${USB_LIBRARY}
|
||||||
|
- BUILD_COMMAND $(MAKE) all V=1 # $(MAKE) is ExternalProject_Add() magic to do recursive make
|
||||||
|
- INSTALL_COMMAND $(MAKE) install-strip V=1 DESTDIR=""
|
||||||
|
- STEP_TARGETS update install
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
#
|
||||||
|
# custom target to make a hamlib source tarball
|
||||||
|
#
|
||||||
|
@@ -128,7 +110,6 @@
|
||||||
|
# build and optionally install WSJT-X using the hamlib package built
|
||||||
|
# above
|
||||||
|
#
|
||||||
|
-ExternalProject_Get_Property (hamlib INSTALL_DIR)
|
||||||
|
ExternalProject_Add (wsjtx
|
||||||
|
GIT_REPOSITORY ${wsjtx_repo}
|
||||||
|
GIT_TAG ${WSJTX_TAG}
|
||||||
|
@@ -152,7 +133,6 @@
|
||||||
|
DEPENDEES build
|
||||||
|
)
|
||||||
|
|
||||||
|
-set_target_properties (hamlib PROPERTIES EXCLUDE_FROM_ALL 1)
|
||||||
|
set_target_properties (wsjtx PROPERTIES EXCLUDE_FROM_ALL 1)
|
||||||
|
|
||||||
|
add_dependencies (wsjtx-configure hamlib-install)
|
156
docker/files/wsjtx/wsjtx.patch
Normal file
156
docker/files/wsjtx/wsjtx.patch
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
diff -ur wsjtx-orig/CMake/Modules/Findhamlib.cmake wsjtx/CMake/Modules/Findhamlib.cmake
|
||||||
|
--- wsjtx-orig/CMake/Modules/Findhamlib.cmake 2020-05-27 22:41:57.774855748 +0200
|
||||||
|
+++ wsjtx/CMake/Modules/Findhamlib.cmake 2020-05-27 22:42:35.267939882 +0200
|
||||||
|
@@ -85,4 +85,4 @@
|
||||||
|
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
|
||||||
|
# TRUE if all listed variables are TRUE
|
||||||
|
include (FindPackageHandleStandardArgs)
|
||||||
|
-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
|
||||||
|
+find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
|
||||||
|
diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
|
||||||
|
--- wsjtx-orig/CMakeLists.txt 2020-05-27 22:41:57.774855748 +0200
|
||||||
|
+++ wsjtx/CMakeLists.txt 2020-05-27 22:58:18.905001618 +0200
|
||||||
|
@@ -869,7 +869,7 @@
|
||||||
|
#
|
||||||
|
# libhamlib setup
|
||||||
|
#
|
||||||
|
-set (hamlib_STATIC 1)
|
||||||
|
+set (hamlib_STATIC 0)
|
||||||
|
find_package (hamlib 3 REQUIRED)
|
||||||
|
find_program (RIGCTL_EXE rigctl)
|
||||||
|
find_program (RIGCTLD_EXE rigctld)
|
||||||
|
@@ -1326,54 +1326,10 @@
|
||||||
|
|
||||||
|
endif(WSJT_BUILD_UTILS)
|
||||||
|
|
||||||
|
-# build the main application
|
||||||
|
-add_executable (wsjtx MACOSX_BUNDLE
|
||||||
|
- ${wsjtx_CXXSRCS}
|
||||||
|
- ${wsjtx_GENUISRCS}
|
||||||
|
- wsjtx.rc
|
||||||
|
- ${WSJTX_ICON_FILE}
|
||||||
|
- ${wsjtx_RESOURCES_RCC}
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
if (WSJT_CREATE_WINMAIN)
|
||||||
|
set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON)
|
||||||
|
endif (WSJT_CREATE_WINMAIN)
|
||||||
|
|
||||||
|
-set_target_properties (wsjtx PROPERTIES
|
||||||
|
- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in"
|
||||||
|
- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}"
|
||||||
|
- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}"
|
||||||
|
- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION}
|
||||||
|
- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}"
|
||||||
|
- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}"
|
||||||
|
- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
|
||||||
|
- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
|
||||||
|
- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
|
||||||
|
- MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx"
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS})
|
||||||
|
-if (APPLE)
|
||||||
|
- target_link_libraries (wsjtx wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
|
||||||
|
-else ()
|
||||||
|
- target_link_libraries (wsjtx wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
|
||||||
|
- if (OpenMP_C_FLAGS)
|
||||||
|
- set_target_properties (wsjtx PROPERTIES
|
||||||
|
- COMPILE_FLAGS "${OpenMP_C_FLAGS}"
|
||||||
|
- LINK_FLAGS "${OpenMP_C_FLAGS}"
|
||||||
|
- )
|
||||||
|
- endif ()
|
||||||
|
- set_target_properties (wsjtx PROPERTIES
|
||||||
|
- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp
|
||||||
|
- )
|
||||||
|
- if (WIN32)
|
||||||
|
- set_target_properties (wsjtx PROPERTIES
|
||||||
|
- LINK_FLAGS -Wl,--stack,16777216
|
||||||
|
- )
|
||||||
|
- endif ()
|
||||||
|
-endif ()
|
||||||
|
-qt5_use_modules (wsjtx SerialPort) # not sure why the interface link library syntax above doesn't work
|
||||||
|
-
|
||||||
|
# make a library for WSJT-X UDP servers
|
||||||
|
# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
|
||||||
|
add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
|
||||||
|
@@ -1417,24 +1373,9 @@
|
||||||
|
set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
|
||||||
|
endif (WSJT_CREATE_WINMAIN)
|
||||||
|
|
||||||
|
-if (UNIX)
|
||||||
|
- if (NOT WSJT_SKIP_MANPAGES)
|
||||||
|
- add_subdirectory (manpages)
|
||||||
|
- add_dependencies (wsjtx manpages)
|
||||||
|
- endif (NOT WSJT_SKIP_MANPAGES)
|
||||||
|
- if (NOT APPLE)
|
||||||
|
- add_subdirectory (debian)
|
||||||
|
- add_dependencies (wsjtx debian)
|
||||||
|
- endif (NOT APPLE)
|
||||||
|
-endif (UNIX)
|
||||||
|
-
|
||||||
|
#
|
||||||
|
# installation
|
||||||
|
#
|
||||||
|
-install (TARGETS wsjtx
|
||||||
|
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||||
|
- BUNDLE DESTINATION . COMPONENT runtime
|
||||||
|
- )
|
||||||
|
|
||||||
|
# install (TARGETS wsjtx_udp EXPORT udp
|
||||||
|
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
@@ -1453,12 +1394,7 @@
|
||||||
|
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
|
||||||
|
# )
|
||||||
|
|
||||||
|
-install (TARGETS udp_daemon message_aggregator
|
||||||
|
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||||
|
- BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
-install (TARGETS jt9 wsprd fmtave fcal fmeasure
|
||||||
|
+install (TARGETS jt9 wsprd
|
||||||
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||||
|
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
|
||||||
|
)
|
||||||
|
@@ -1471,39 +1407,6 @@
|
||||||
|
)
|
||||||
|
endif(WSJT_BUILD_UTILS)
|
||||||
|
|
||||||
|
-install (PROGRAMS
|
||||||
|
- ${RIGCTL_EXE}
|
||||||
|
- DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
- #COMPONENT runtime
|
||||||
|
- RENAME rigctl-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
-install (PROGRAMS
|
||||||
|
- ${RIGCTLD_EXE}
|
||||||
|
- DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
- #COMPONENT runtime
|
||||||
|
- RENAME rigctld-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
-install (PROGRAMS
|
||||||
|
- ${RIGCTLCOM_EXE}
|
||||||
|
- DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
- #COMPONENT runtime
|
||||||
|
- RENAME rigctlcom-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
-install (FILES
|
||||||
|
- README
|
||||||
|
- COPYING
|
||||||
|
- AUTHORS
|
||||||
|
- THANKS
|
||||||
|
- NEWS
|
||||||
|
- INSTALL
|
||||||
|
- BUGS
|
||||||
|
- DESTINATION ${CMAKE_INSTALL_DOCDIR}
|
||||||
|
- #COMPONENT runtime
|
||||||
|
- )
|
||||||
|
-
|
||||||
|
install (FILES
|
||||||
|
contrib/Ephemeris/JPLEPH
|
||||||
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
|
||||||
|
Only in wsjtx: .idea
|
@ -1,241 +0,0 @@
|
|||||||
diff --git a/Makefile.linux b/Makefile.linux
|
|
||||||
index 5010833..3f61de9 100644
|
|
||||||
--- a/Makefile.linux
|
|
||||||
+++ b/Makefile.linux
|
|
||||||
@@ -585,102 +585,102 @@ install : $(APPS) direwolf.conf tocalls.txt symbols-new.txt symbolsX.txt dw-icon
|
|
||||||
# Applications, not installed with package manager, normally go in /usr/local/bin.
|
|
||||||
# /usr/bin is used instead when installing from .DEB or .RPM package.
|
|
||||||
#
|
|
||||||
- $(INSTALL) -D --mode=755 direwolf $(DESTDIR)/bin/direwolf
|
|
||||||
- $(INSTALL) -D --mode=755 decode_aprs $(DESTDIR)/bin/decode_aprs
|
|
||||||
- $(INSTALL) -D --mode=755 text2tt $(DESTDIR)/bin/text2tt
|
|
||||||
- $(INSTALL) -D --mode=755 tt2text $(DESTDIR)/bin/tt2text
|
|
||||||
- $(INSTALL) -D --mode=755 ll2utm $(DESTDIR)/bin/ll2utm
|
|
||||||
- $(INSTALL) -D --mode=755 utm2ll $(DESTDIR)/bin/utm2ll
|
|
||||||
- $(INSTALL) -D --mode=755 aclients $(DESTDIR)/bin/aclients
|
|
||||||
- $(INSTALL) -D --mode=755 log2gpx $(DESTDIR)/bin/log2gpx
|
|
||||||
- $(INSTALL) -D --mode=755 gen_packets $(DESTDIR)/bin/gen_packets
|
|
||||||
- $(INSTALL) -D --mode=755 atest $(DESTDIR)/bin/atest
|
|
||||||
- $(INSTALL) -D --mode=755 ttcalc $(DESTDIR)/bin/ttcalc
|
|
||||||
- $(INSTALL) -D --mode=755 kissutil $(DESTDIR)/bin/kissutil
|
|
||||||
- $(INSTALL) -D --mode=755 cm108 $(DESTDIR)/bin/cm108
|
|
||||||
- $(INSTALL) -D --mode=755 dwespeak.sh $(DESTDIR)/bin/dwspeak.sh
|
|
||||||
+ $(INSTALL) -D -m=755 direwolf $(DESTDIR)/bin/direwolf
|
|
||||||
+ $(INSTALL) -D -m=755 decode_aprs $(DESTDIR)/bin/decode_aprs
|
|
||||||
+ $(INSTALL) -D -m=755 text2tt $(DESTDIR)/bin/text2tt
|
|
||||||
+ $(INSTALL) -D -m=755 tt2text $(DESTDIR)/bin/tt2text
|
|
||||||
+ $(INSTALL) -D -m=755 ll2utm $(DESTDIR)/bin/ll2utm
|
|
||||||
+ $(INSTALL) -D -m=755 utm2ll $(DESTDIR)/bin/utm2ll
|
|
||||||
+ $(INSTALL) -D -m=755 aclients $(DESTDIR)/bin/aclients
|
|
||||||
+ $(INSTALL) -D -m=755 log2gpx $(DESTDIR)/bin/log2gpx
|
|
||||||
+ $(INSTALL) -D -m=755 gen_packets $(DESTDIR)/bin/gen_packets
|
|
||||||
+ $(INSTALL) -D -m=755 atest $(DESTDIR)/bin/atest
|
|
||||||
+ $(INSTALL) -D -m=755 ttcalc $(DESTDIR)/bin/ttcalc
|
|
||||||
+ $(INSTALL) -D -m=755 kissutil $(DESTDIR)/bin/kissutil
|
|
||||||
+ $(INSTALL) -D -m=755 cm108 $(DESTDIR)/bin/cm108
|
|
||||||
+ $(INSTALL) -D -m=755 dwespeak.sh $(DESTDIR)/bin/dwspeak.sh
|
|
||||||
#
|
|
||||||
# Telemetry Toolkit executables. Other .conf and .txt files will go into doc directory.
|
|
||||||
#
|
|
||||||
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-balloon.pl $(DESTDIR)/bin/telem-balloon.pl
|
|
||||||
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-bits.pl $(DESTDIR)/bin/telem-bits.pl
|
|
||||||
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-data.pl $(DESTDIR)/bin/telem-data.pl
|
|
||||||
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-data91.pl $(DESTDIR)/bin/telem-data91.pl
|
|
||||||
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-eqns.pl $(DESTDIR)/bin/telem-eqns.pl
|
|
||||||
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-parm.pl $(DESTDIR)/bin/telem-parm.pl
|
|
||||||
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-seq.sh $(DESTDIR)/bin/telem-seq.sh
|
|
||||||
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-unit.pl $(DESTDIR)/bin/telem-unit.pl
|
|
||||||
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-volts.py $(DESTDIR)/bin/telem-volts.py
|
|
||||||
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-balloon.pl $(DESTDIR)/bin/telem-balloon.pl
|
|
||||||
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-bits.pl $(DESTDIR)/bin/telem-bits.pl
|
|
||||||
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-data.pl $(DESTDIR)/bin/telem-data.pl
|
|
||||||
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-data91.pl $(DESTDIR)/bin/telem-data91.pl
|
|
||||||
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-eqns.pl $(DESTDIR)/bin/telem-eqns.pl
|
|
||||||
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-parm.pl $(DESTDIR)/bin/telem-parm.pl
|
|
||||||
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-seq.sh $(DESTDIR)/bin/telem-seq.sh
|
|
||||||
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-unit.pl $(DESTDIR)/bin/telem-unit.pl
|
|
||||||
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-volts.py $(DESTDIR)/bin/telem-volts.py
|
|
||||||
#
|
|
||||||
# Misc. data such as "tocall" to system mapping.
|
|
||||||
#
|
|
||||||
- $(INSTALL) -D --mode=644 tocalls.txt $(DESTDIR)/share/direwolf/tocalls.txt
|
|
||||||
- $(INSTALL) -D --mode=644 symbols-new.txt $(DESTDIR)/share/direwolf/symbols-new.txt
|
|
||||||
- $(INSTALL) -D --mode=644 symbolsX.txt $(DESTDIR)/share/direwolf/symbolsX.txt
|
|
||||||
+ $(INSTALL) -D -m=644 tocalls.txt $(DESTDIR)/share/direwolf/tocalls.txt
|
|
||||||
+ $(INSTALL) -D -m=644 symbols-new.txt $(DESTDIR)/share/direwolf/symbols-new.txt
|
|
||||||
+ $(INSTALL) -D -m=644 symbolsX.txt $(DESTDIR)/share/direwolf/symbolsX.txt
|
|
||||||
#
|
|
||||||
# For desktop icon.
|
|
||||||
#
|
|
||||||
- $(INSTALL) -D --mode=644 dw-icon.png $(DESTDIR)/share/direwolf/pixmaps/dw-icon.png
|
|
||||||
- $(INSTALL) -D --mode=644 direwolf.desktop $(DESTDIR)/share/applications/direwolf.desktop
|
|
||||||
+ $(INSTALL) -D -m=644 dw-icon.png $(DESTDIR)/share/direwolf/pixmaps/dw-icon.png
|
|
||||||
+ $(INSTALL) -D -m=644 direwolf.desktop $(DESTDIR)/share/applications/direwolf.desktop
|
|
||||||
#
|
|
||||||
# Documentation. Various plain text files and PDF.
|
|
||||||
#
|
|
||||||
- $(INSTALL) -D --mode=644 CHANGES.md $(DESTDIR)/share/doc/direwolf/CHANGES.md
|
|
||||||
- $(INSTALL) -D --mode=644 LICENSE-dire-wolf.txt $(DESTDIR)/share/doc/direwolf/LICENSE-dire-wolf.txt
|
|
||||||
- $(INSTALL) -D --mode=644 LICENSE-other.txt $(DESTDIR)/share/doc/direwolf/LICENSE-other.txt
|
|
||||||
+ $(INSTALL) -D -m=644 CHANGES.md $(DESTDIR)/share/doc/direwolf/CHANGES.md
|
|
||||||
+ $(INSTALL) -D -m=644 LICENSE-dire-wolf.txt $(DESTDIR)/share/doc/direwolf/LICENSE-dire-wolf.txt
|
|
||||||
+ $(INSTALL) -D -m=644 LICENSE-other.txt $(DESTDIR)/share/doc/direwolf/LICENSE-other.txt
|
|
||||||
#
|
|
||||||
# ./README.md is an overview for the project main page.
|
|
||||||
# Maybe we could stick it in some other place.
|
|
||||||
# doc/README.md contains an overview of the PDF file contents and is more useful here.
|
|
||||||
#
|
|
||||||
- $(INSTALL) -D --mode=644 doc/README.md $(DESTDIR)/share/doc/direwolf/README.md
|
|
||||||
- $(INSTALL) -D --mode=644 doc/2400-4800-PSK-for-APRS-Packet-Radio.pdf $(DESTDIR)/share/doc/direwolf/2400-4800-PSK-for-APRS-Packet-Radio.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf $(DESTDIR)/share/doc/direwolf/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/APRS-Telemetry-Toolkit.pdf $(DESTDIR)/share/doc/direwolf/APRS-Telemetry-Toolkit.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/APRStt-Implementation-Notes.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Implementation-Notes.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/APRStt-interface-for-SARTrack.pdf $(DESTDIR)/share/doc/direwolf/APRStt-interface-for-SARTrack.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/APRStt-Listening-Example.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Listening-Example.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/Bluetooth-KISS-TNC.pdf $(DESTDIR)/share/doc/direwolf/Bluetooth-KISS-TNC.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/Going-beyond-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/Going-beyond-9600-baud.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-APRS.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-APRS-Tracker.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS-Tracker.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-SDR-IGate.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-SDR-IGate.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/Successful-APRS-IGate-Operation.pdf $(DESTDIR)/share/doc/direwolf/Successful-APRS-IGate-Operation.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/User-Guide.pdf $(DESTDIR)/share/doc/direwolf/User-Guide.pdf
|
|
||||||
- $(INSTALL) -D --mode=644 doc/WA8LMF-TNC-Test-CD-Results.pdf $(DESTDIR)/share/doc/direwolf/WA8LMF-TNC-Test-CD-Results.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/README.md $(DESTDIR)/share/doc/direwolf/README.md
|
|
||||||
+ $(INSTALL) -D -m=644 doc/2400-4800-PSK-for-APRS-Packet-Radio.pdf $(DESTDIR)/share/doc/direwolf/2400-4800-PSK-for-APRS-Packet-Radio.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf $(DESTDIR)/share/doc/direwolf/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/APRS-Telemetry-Toolkit.pdf $(DESTDIR)/share/doc/direwolf/APRS-Telemetry-Toolkit.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/APRStt-Implementation-Notes.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Implementation-Notes.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/APRStt-interface-for-SARTrack.pdf $(DESTDIR)/share/doc/direwolf/APRStt-interface-for-SARTrack.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/APRStt-Listening-Example.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Listening-Example.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/Bluetooth-KISS-TNC.pdf $(DESTDIR)/share/doc/direwolf/Bluetooth-KISS-TNC.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/Going-beyond-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/Going-beyond-9600-baud.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-APRS.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-APRS-Tracker.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS-Tracker.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-SDR-IGate.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-SDR-IGate.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/Successful-APRS-IGate-Operation.pdf $(DESTDIR)/share/doc/direwolf/Successful-APRS-IGate-Operation.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/User-Guide.pdf $(DESTDIR)/share/doc/direwolf/User-Guide.pdf
|
|
||||||
+ $(INSTALL) -D -m=644 doc/WA8LMF-TNC-Test-CD-Results.pdf $(DESTDIR)/share/doc/direwolf/WA8LMF-TNC-Test-CD-Results.pdf
|
|
||||||
#
|
|
||||||
# Various sample config and other files go into examples under the doc directory.
|
|
||||||
# When building from source, these can be put in home directory with "make install-conf".
|
|
||||||
# When installed from .DEB or .RPM package, the user will need to copy these to
|
|
||||||
# the home directory or other desired location.
|
|
||||||
#
|
|
||||||
- $(INSTALL) -D --mode=644 direwolf.conf $(DESTDIR)/share/doc/direwolf/examples/direwolf.conf
|
|
||||||
- $(INSTALL) -D --mode=755 dw-start.sh $(DESTDIR)/share/doc/direwolf/examples/dw-start.sh
|
|
||||||
- $(INSTALL) -D --mode=644 sdr.conf $(DESTDIR)/share/doc/direwolf/examples/sdr.conf
|
|
||||||
- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-m0xer-3.txt $(DESTDIR)/share/doc/direwolf/examples/telem-m0xer-3.txt
|
|
||||||
- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-balloon.conf $(DESTDIR)/share/doc/direwolf/examples/telem-balloon.conf
|
|
||||||
- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-volts.conf $(DESTDIR)/share/doc/direwolf/examples/telem-volts.conf
|
|
||||||
+ $(INSTALL) -D -m=644 direwolf.conf $(DESTDIR)/share/doc/direwolf/examples/direwolf.conf
|
|
||||||
+ $(INSTALL) -D -m=755 dw-start.sh $(DESTDIR)/share/doc/direwolf/examples/dw-start.sh
|
|
||||||
+ $(INSTALL) -D -m=644 sdr.conf $(DESTDIR)/share/doc/direwolf/examples/sdr.conf
|
|
||||||
+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-m0xer-3.txt $(DESTDIR)/share/doc/direwolf/examples/telem-m0xer-3.txt
|
|
||||||
+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-balloon.conf $(DESTDIR)/share/doc/direwolf/examples/telem-balloon.conf
|
|
||||||
+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-volts.conf $(DESTDIR)/share/doc/direwolf/examples/telem-volts.conf
|
|
||||||
#
|
|
||||||
# "man" pages
|
|
||||||
#
|
|
||||||
- $(INSTALL) -D --mode=644 man1/aclients.1 $(DESTDIR)/share/man/man1/aclients.1
|
|
||||||
- $(INSTALL) -D --mode=644 man1/atest.1 $(DESTDIR)/share/man/man1/atest.1
|
|
||||||
- $(INSTALL) -D --mode=644 man1/decode_aprs.1 $(DESTDIR)/share/man/man1/decode_aprs.1
|
|
||||||
- $(INSTALL) -D --mode=644 man1/direwolf.1 $(DESTDIR)/share/man/man1/direwolf.1
|
|
||||||
- $(INSTALL) -D --mode=644 man1/gen_packets.1 $(DESTDIR)/share/man/man1/gen_packets.1
|
|
||||||
- $(INSTALL) -D --mode=644 man1/kissutil.1 $(DESTDIR)/share/man/man1/kissutil.1
|
|
||||||
- $(INSTALL) -D --mode=644 man1/ll2utm.1 $(DESTDIR)/share/man/man1/ll2utm.1
|
|
||||||
- $(INSTALL) -D --mode=644 man1/log2gpx.1 $(DESTDIR)/share/man/man1/log2gpx.1
|
|
||||||
- $(INSTALL) -D --mode=644 man1/text2tt.1 $(DESTDIR)/share/man/man1/text2tt.1
|
|
||||||
- $(INSTALL) -D --mode=644 man1/tt2text.1 $(DESTDIR)/share/man/man1/tt2text.1
|
|
||||||
- $(INSTALL) -D --mode=644 man1/utm2ll.1 $(DESTDIR)/share/man/man1/utm2ll.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/aclients.1 $(DESTDIR)/share/man/man1/aclients.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/atest.1 $(DESTDIR)/share/man/man1/atest.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/decode_aprs.1 $(DESTDIR)/share/man/man1/decode_aprs.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/direwolf.1 $(DESTDIR)/share/man/man1/direwolf.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/gen_packets.1 $(DESTDIR)/share/man/man1/gen_packets.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/kissutil.1 $(DESTDIR)/share/man/man1/kissutil.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/ll2utm.1 $(DESTDIR)/share/man/man1/ll2utm.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/log2gpx.1 $(DESTDIR)/share/man/man1/log2gpx.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/text2tt.1 $(DESTDIR)/share/man/man1/text2tt.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/tt2text.1 $(DESTDIR)/share/man/man1/tt2text.1
|
|
||||||
+ $(INSTALL) -D -m=644 man1/utm2ll.1 $(DESTDIR)/share/man/man1/utm2ll.1
|
|
||||||
#
|
|
||||||
# Set group and mode of HID devices corresponding to C-Media USB Audio adapters.
|
|
||||||
# This will allow us to use the CM108/CM119 GPIO pins for PTT.
|
|
||||||
#
|
|
||||||
- $(INSTALL) -D --mode=644 99-direwolf-cmedia.rules /etc/udev/rules.d/99-direwolf-cmedia.rules
|
|
||||||
+ $(INSTALL) -D -m=644 99-direwolf-cmedia.rules /etc/udev/rules.d/99-direwolf-cmedia.rules
|
|
||||||
#
|
|
||||||
@echo " "
|
|
||||||
@echo "If this is your first install, not an upgrade, type this to put a copy"
|
|
||||||
diff --git a/cdigipeater.c b/cdigipeater.c
|
|
||||||
index 9c40d95..94112e9 100644
|
|
||||||
--- a/cdigipeater.c
|
|
||||||
+++ b/cdigipeater.c
|
|
||||||
@@ -49,7 +49,7 @@
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <ctype.h> /* for isdigit, isupper */
|
|
||||||
#include "regex.h"
|
|
||||||
-#include <sys/unistd.h>
|
|
||||||
+#include <unistd.h>
|
|
||||||
|
|
||||||
#include "ax25_pad.h"
|
|
||||||
#include "cdigipeater.h"
|
|
||||||
diff --git a/decode_aprs.c b/decode_aprs.c
|
|
||||||
index 35c186b..a620cb3 100644
|
|
||||||
--- a/decode_aprs.c
|
|
||||||
+++ b/decode_aprs.c
|
|
||||||
@@ -3872,11 +3872,7 @@ static void decode_tocall (decode_aprs_t *A, char *dest)
|
|
||||||
* models before getting to the more generic APY.
|
|
||||||
*/
|
|
||||||
|
|
||||||
-#if defined(__WIN32__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__)
|
|
||||||
qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), tocall_cmp);
|
|
||||||
-#else
|
|
||||||
- qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), (__compar_fn_t)tocall_cmp);
|
|
||||||
-#endif
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if ( ! A->g_quiet) {
|
|
||||||
diff --git a/digipeater.c b/digipeater.c
|
|
||||||
index 36970d7..5195582 100644
|
|
||||||
--- a/digipeater.c
|
|
||||||
+++ b/digipeater.c
|
|
||||||
@@ -62,7 +62,7 @@
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <ctype.h> /* for isdigit, isupper */
|
|
||||||
#include "regex.h"
|
|
||||||
-#include <sys/unistd.h>
|
|
||||||
+#include <unistd.h>
|
|
||||||
|
|
||||||
#include "ax25_pad.h"
|
|
||||||
#include "digipeater.h"
|
|
||||||
diff --git a/direwolf.h b/direwolf.h
|
|
||||||
index 514bcc5..52f5ae9 100644
|
|
||||||
--- a/direwolf.h
|
|
||||||
+++ b/direwolf.h
|
|
||||||
@@ -274,7 +274,7 @@ char *strtok_r(char *str, const char *delim, char **saveptr);
|
|
||||||
char *strcasestr(const char *S, const char *FIND);
|
|
||||||
|
|
||||||
|
|
||||||
-#if defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__)
|
|
||||||
+#if 1
|
|
||||||
|
|
||||||
// strlcpy and strlcat should be in string.h and the C library.
|
|
||||||
|
|
||||||
diff --git a/multi_modem.c b/multi_modem.c
|
|
||||||
index 5d96c79..24261b9 100644
|
|
||||||
--- a/multi_modem.c
|
|
||||||
+++ b/multi_modem.c
|
|
||||||
@@ -80,7 +80,7 @@
|
|
||||||
#include <string.h>
|
|
||||||
#include <assert.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
-#include <sys/unistd.h>
|
|
||||||
+#include <unistd.h>
|
|
||||||
|
|
||||||
#include "ax25_pad.h"
|
|
||||||
#include "textcolor.h"
|
|
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
function cmakebuild() {
|
function cmakebuild() {
|
||||||
cd $1
|
cd $1
|
||||||
@ -17,12 +18,14 @@ function cmakebuild() {
|
|||||||
|
|
||||||
cd /tmp
|
cd /tmp
|
||||||
|
|
||||||
BUILD_PACKAGES="git cmake make gcc g++ musl-dev"
|
BUILD_PACKAGES="git cmake make gcc g++"
|
||||||
|
|
||||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get -y install --no-install-recommends $BUILD_PACKAGES
|
||||||
|
|
||||||
git clone https://github.com/jketterl/owrx_connector.git
|
git clone https://github.com/jketterl/owrx_connector.git
|
||||||
cmakebuild owrx_connector df35e33e42c2e4527853ca18bf04981848860317
|
cmakebuild owrx_connector 45ec227b38bb763b0a923a1856740f4ddf74216c
|
||||||
|
|
||||||
apk del .build-deps
|
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
function cmakebuild() {
|
function cmakebuild() {
|
||||||
cd $1
|
cd $1
|
||||||
@ -17,22 +18,24 @@ function cmakebuild() {
|
|||||||
|
|
||||||
cd /tmp
|
cd /tmp
|
||||||
|
|
||||||
STATIC_PACKAGES="libusb"
|
STATIC_PACKAGES="libusb-1.0-0"
|
||||||
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
|
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
|
||||||
|
|
||||||
apk add --no-cache $STATIC_PACKAGES
|
apt-get update
|
||||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
git clone https://github.com/airspy/airspyone_host.git
|
git clone https://github.com/airspy/airspyone_host.git
|
||||||
cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3
|
cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3
|
||||||
|
|
||||||
git clone https://github.com/pothosware/SoapyAirspy.git
|
git clone https://github.com/pothosware/SoapyAirspy.git
|
||||||
cmakebuild SoapyAirspy 99756be5c3413a2d447baf70cb5a880662452655
|
cmakebuild SoapyAirspy 10d697b209e7f1acc8b2c8d24851d46170ef77e3
|
||||||
|
|
||||||
git clone https://github.com/airspy/airspyhf.git
|
git clone https://github.com/airspy/airspyhf.git
|
||||||
cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475
|
cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475
|
||||||
|
|
||||||
git clone https://github.com/pothosware/SoapyAirspyHF.git
|
git clone https://github.com/pothosware/SoapyAirspyHF.git
|
||||||
cmakebuild SoapyAirspyHF 54f5487dd96207540b2dd562ff9e718e0588770b
|
cmakebuild SoapyAirspyHF 81ca737bb044dd930a9de738bced1e4915491f1b
|
||||||
|
|
||||||
apk del .build-deps
|
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
function cmakebuild() {
|
function cmakebuild() {
|
||||||
cd $1
|
cd $1
|
||||||
@ -17,17 +18,22 @@ function cmakebuild() {
|
|||||||
|
|
||||||
cd /tmp
|
cd /tmp
|
||||||
|
|
||||||
STATIC_PACKAGES="libusb fftw udev"
|
STATIC_PACKAGES="libusb-1.0-0 libfftw3-3 udev"
|
||||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev"
|
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config"
|
||||||
|
|
||||||
apk add --no-cache $STATIC_PACKAGES
|
apt-get update
|
||||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
git clone https://github.com/mossmann/hackrf.git
|
git clone https://github.com/mossmann/hackrf.git
|
||||||
cd hackrf
|
cd hackrf
|
||||||
git checkout 06eb9192cd348083f5f7de9c0da9ead276020011
|
git checkout 43e6f99fe8543094d18ff3a6550ed2066c398862
|
||||||
cmakebuild host
|
cmakebuild host
|
||||||
cd ..
|
cd ..
|
||||||
rm -rf hackrf
|
rm -rf hackrf
|
||||||
|
|
||||||
apk del .build-deps
|
git clone https://github.com/pothosware/SoapyHackRF.git
|
||||||
|
cmakebuild SoapyHackRF 3c514cecd3dd2fdf4794aebc96c482940c11d7ff
|
||||||
|
|
||||||
|
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
31
docker/scripts/install-dependencies-limesdr.sh
Executable file
31
docker/scripts/install-dependencies-limesdr.sh
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
|
cd /tmp
|
||||||
|
|
||||||
|
STATIC_PACKAGES="libusb-1.0-0 libatomic1"
|
||||||
|
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++"
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
|
SIMD_FLAGS=""
|
||||||
|
if [[ 'x86_64' == `uname -m` ]] ; then
|
||||||
|
SIMD_FLAGS="-DDEFAULT_SIMD_FLAGS=SSE3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git clone https://github.com/myriadrf/LimeSuite.git
|
||||||
|
cd LimeSuite
|
||||||
|
git checkout 0854a51ec06b30b01f19a562149c39461e92f24d
|
||||||
|
mkdir builddir
|
||||||
|
cd builddir
|
||||||
|
cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic" ${SIMD_FLAGS}
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
cd ../..
|
||||||
|
rm -rf LimeSuite
|
||||||
|
|
||||||
|
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
26
docker/scripts/install-dependencies-perseus.sh
Executable file
26
docker/scripts/install-dependencies-perseus.sh
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euxo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
|
cd /tmp
|
||||||
|
|
||||||
|
STATIC_PACKAGES="libusb-1.0-0 libudev1"
|
||||||
|
BUILD_PACKAGES="git make gcc autoconf automake libtool libusb-1.0-0-dev xxd"
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
|
git clone https://github.com/Microtelecom/libperseus-sdr.git
|
||||||
|
cd libperseus-sdr
|
||||||
|
git checkout 72ac67c5b7936a1991be0ec97c03a59c1a8ac8f3
|
||||||
|
./bootstrap.sh
|
||||||
|
./configure
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
ldconfig /etc/ld.so.conf.d
|
||||||
|
cd ..
|
||||||
|
rm -rf libperseus-sdr
|
||||||
|
|
||||||
|
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
38
docker/scripts/install-dependencies-plutosdr.sh
Executable file
38
docker/scripts/install-dependencies-plutosdr.sh
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
|
function cmakebuild() {
|
||||||
|
cd $1
|
||||||
|
if [[ ! -z "${2:-}" ]]; then
|
||||||
|
git checkout $2
|
||||||
|
fi
|
||||||
|
mkdir build
|
||||||
|
cd build
|
||||||
|
cmake .. ${3:-}
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
cd ../..
|
||||||
|
rm -rf $1
|
||||||
|
}
|
||||||
|
|
||||||
|
cd /tmp
|
||||||
|
|
||||||
|
STATIC_PACKAGES="libusb-1.0-0 libxml2"
|
||||||
|
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ libxml2-dev flex bison"
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
|
git clone https://github.com/analogdevicesinc/libiio.git
|
||||||
|
cmakebuild libiio 5f5af2e417129ad8f4e05fc5c1b730f0694dca12 -DCMAKE_INSTALL_PREFIX=/usr/local
|
||||||
|
|
||||||
|
git clone https://github.com/analogdevicesinc/libad9361-iio.git
|
||||||
|
cmakebuild libad9361-iio 8ac95f3325c18c2e34cd9cfd49c7b63d69a0a9d2
|
||||||
|
|
||||||
|
git clone https://github.com/pothosware/SoapyPlutoSDR.git
|
||||||
|
cmakebuild SoapyPlutoSDR c88b7f5bac1e5785f212f9a7c6ce8fef64eb719e
|
||||||
|
|
||||||
|
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
function cmakebuild() {
|
function cmakebuild() {
|
||||||
cd $1
|
cd $1
|
||||||
@ -17,16 +18,18 @@ function cmakebuild() {
|
|||||||
|
|
||||||
cd /tmp
|
cd /tmp
|
||||||
|
|
||||||
STATIC_PACKAGES="libusb"
|
STATIC_PACKAGES="libusb-1.0-0"
|
||||||
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
|
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
|
||||||
|
|
||||||
apk add --no-cache $STATIC_PACKAGES
|
apt-get update
|
||||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
git clone https://github.com/osmocom/rtl-sdr.git
|
git clone https://github.com/osmocom/rtl-sdr.git
|
||||||
cmakebuild rtl-sdr b5af355b1d833b3c898a61cf1e072b59b0ea3440
|
cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
|
||||||
|
|
||||||
git clone https://github.com/pothosware/SoapyRTLSDR.git
|
git clone https://github.com/pothosware/SoapyRTLSDR.git
|
||||||
cmakebuild SoapyRTLSDR 5c5d9503337c6d1c34b496dec6f908aab9478b0f
|
cmakebuild SoapyRTLSDR 8ba18f17d64005e43ff2a4e46611f8c710b05007
|
||||||
|
|
||||||
apk del .build-deps
|
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
function cmakebuild() {
|
function cmakebuild() {
|
||||||
cd $1
|
cd $1
|
||||||
@ -17,13 +18,15 @@ function cmakebuild() {
|
|||||||
|
|
||||||
cd /tmp
|
cd /tmp
|
||||||
|
|
||||||
STATIC_PACKAGES="libusb"
|
STATIC_PACKAGES="libusb-1.0.0"
|
||||||
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
|
BUILD_PACKAGES="git libusb-1.0.0-dev cmake make gcc g++ pkg-config"
|
||||||
|
|
||||||
apk add --no-cache $STATIC_PACKAGES
|
apt-get update
|
||||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
git clone https://github.com/osmocom/rtl-sdr.git
|
git clone https://github.com/osmocom/rtl-sdr.git
|
||||||
cmakebuild rtl-sdr b5af355b1d833b3c898a61cf1e072b59b0ea3440
|
cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
|
||||||
|
|
||||||
apk del .build-deps
|
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
function cmakebuild() {
|
function cmakebuild() {
|
||||||
cd $1
|
cd $1
|
||||||
@ -17,23 +18,23 @@ function cmakebuild() {
|
|||||||
|
|
||||||
cd /tmp
|
cd /tmp
|
||||||
|
|
||||||
STATIC_PACKAGES="libusb udev"
|
STATIC_PACKAGES="libusb-1.0.0 udev"
|
||||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev"
|
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev"
|
||||||
|
|
||||||
apk add --no-cache $STATIC_PACKAGES
|
apt-get update
|
||||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
|
|
||||||
case $ARCH in
|
case $ARCH in
|
||||||
x86_64)
|
x86_64)
|
||||||
BINARY=SDRplay_RSP_API-Linux-2.13.1.run
|
BINARY=SDRplay_RSP_API-Linux-3.07.1.run
|
||||||
;;
|
;;
|
||||||
armv*)
|
armv*)
|
||||||
BINARY=SDRplay_RSP_API-RPi-2.13.1.run
|
BINARY=SDRplay_RSP_API-ARM32-3.07.2.run
|
||||||
;;
|
;;
|
||||||
aarch64)
|
aarch64)
|
||||||
BINARY=SDRplay_RSP_API-ARM64-2.13.1.run
|
BINARY=SDRplay_RSP_API-ARM64-3.07.1.run
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@ -47,7 +48,9 @@ cd ..
|
|||||||
rm -rf sdrplay
|
rm -rf sdrplay
|
||||||
rm $BINARY
|
rm $BINARY
|
||||||
|
|
||||||
git clone https://github.com/pothosware/SoapySDRPlay.git
|
git clone https://github.com/SDRplay/SoapySDRPlay.git
|
||||||
cmakebuild SoapySDRPlay 14ec39e4ff0dab7ae7fdf1afbbd2d28b49b0ffae
|
cmakebuild SoapySDRPlay 1c2728a04db5edf8154d02f5cca87e655152d7c1
|
||||||
|
|
||||||
apk del .build-deps
|
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
32
docker/scripts/install-dependencies-soapyremote.sh
Executable file
32
docker/scripts/install-dependencies-soapyremote.sh
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
|
function cmakebuild() {
|
||||||
|
cd $1
|
||||||
|
if [[ ! -z "${2:-}" ]]; then
|
||||||
|
git checkout $2
|
||||||
|
fi
|
||||||
|
mkdir build
|
||||||
|
cd build
|
||||||
|
cmake ..
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
cd ../..
|
||||||
|
rm -rf $1
|
||||||
|
}
|
||||||
|
|
||||||
|
cd /tmp
|
||||||
|
|
||||||
|
STATIC_PACKAGES="avahi-daemon libavahi-client3"
|
||||||
|
BUILD_PACKAGES="git cmake make gcc g++ libavahi-client-dev"
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
|
git clone https://github.com/pothosware/SoapyRemote.git
|
||||||
|
cmakebuild SoapyRemote 6d9bd820da470cfe7b27b2e6946af93cfece448f
|
||||||
|
|
||||||
|
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
function cmakebuild() {
|
function cmakebuild() {
|
||||||
cd $1
|
cd $1
|
||||||
@ -17,13 +18,15 @@ function cmakebuild() {
|
|||||||
|
|
||||||
cd /tmp
|
cd /tmp
|
||||||
|
|
||||||
STATIC_PACKAGES="udev"
|
STATIC_PACKAGES="libudev1"
|
||||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++"
|
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++"
|
||||||
|
|
||||||
apk add --no-cache $STATIC_PACKAGES
|
apt-get update
|
||||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
git clone https://github.com/pothosware/SoapySDR
|
git clone https://github.com/pothosware/SoapySDR
|
||||||
cmakebuild SoapySDR f722f9ce5b629c3c44401a9bf628b3f8e67a9695
|
cmakebuild SoapySDR f722f9ce5b629c3c44401a9bf628b3f8e67a9695
|
||||||
|
|
||||||
apk del .build-deps
|
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
export MAKEFLAGS="-j4"
|
||||||
|
|
||||||
function cmakebuild() {
|
function cmakebuild() {
|
||||||
cd $1
|
cd $1
|
||||||
@ -8,7 +9,7 @@ function cmakebuild() {
|
|||||||
fi
|
fi
|
||||||
mkdir build
|
mkdir build
|
||||||
cd build
|
cd build
|
||||||
cmake ..
|
cmake ${CMAKE_ARGS:-} ..
|
||||||
make
|
make
|
||||||
make install
|
make install
|
||||||
cd ../..
|
cd ../..
|
||||||
@ -17,18 +18,46 @@ function cmakebuild() {
|
|||||||
|
|
||||||
cd /tmp
|
cd /tmp
|
||||||
|
|
||||||
STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools alsa-lib"
|
STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates"
|
||||||
BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers"
|
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev patch xsltproc"
|
||||||
|
|
||||||
apk add --no-cache $STATIC_PACKAGES
|
apt-get update
|
||||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
apt-get -y install auto-apt-proxy
|
||||||
|
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||||
|
|
||||||
|
case `uname -m` in
|
||||||
|
arm*)
|
||||||
|
PLATFORM=armhf
|
||||||
|
;;
|
||||||
|
aarch64*)
|
||||||
|
PLATFORM=aarch64
|
||||||
|
;;
|
||||||
|
x86_64*)
|
||||||
|
PLATFORM=amd64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
pushd /tmp
|
||||||
|
wget https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${PLATFORM}.tar.gz
|
||||||
|
tar xzf s6-overlay-${PLATFORM}.tar.gz -C /
|
||||||
|
rm s6-overlay-${PLATFORM}.tar.gz
|
||||||
|
popd
|
||||||
|
|
||||||
|
git clone https://github.com/jketterl/js8py.git
|
||||||
|
pushd js8py
|
||||||
|
git checkout 888e62be375316882ad2b2ac8e396c3bf857b6fc
|
||||||
|
python3 setup.py install
|
||||||
|
popd
|
||||||
|
rm -rf js8py
|
||||||
|
|
||||||
git clone https://git.code.sf.net/p/itpp/git itpp
|
git clone https://git.code.sf.net/p/itpp/git itpp
|
||||||
cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d
|
cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d
|
||||||
|
|
||||||
git clone https://github.com/jketterl/csdr.git
|
git clone https://github.com/jketterl/csdr.git
|
||||||
cd csdr
|
cd csdr
|
||||||
git checkout 43c36df5dcd92d3bdb322f9d53f99ca0c7c816a4
|
git checkout c4d8a8a5590898e8c9e94b88b96a2fdc7cd0493a
|
||||||
|
autoreconf -i
|
||||||
|
./configure
|
||||||
make
|
make
|
||||||
make install
|
make install
|
||||||
cd ..
|
cd ..
|
||||||
@ -38,20 +67,33 @@ git clone https://github.com/szechyjs/mbelib.git
|
|||||||
cmakebuild mbelib 9a04ed5c78176a9965f3d43f7aa1b1f5330e771f
|
cmakebuild mbelib 9a04ed5c78176a9965f3d43f7aa1b1f5330e771f
|
||||||
|
|
||||||
git clone https://github.com/jketterl/digiham.git
|
git clone https://github.com/jketterl/digiham.git
|
||||||
cmakebuild digiham e5e11ce9611e3d8f5f9dce7dee97f86a31af107c
|
cmakebuild digiham 95206501be89b38d0267bf6c29a6898e7c65656f
|
||||||
|
|
||||||
git clone https://github.com/f4exb/dsd.git
|
git clone https://github.com/f4exb/dsd.git
|
||||||
cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7
|
cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7
|
||||||
|
|
||||||
|
JS8CALL_VERSION=2.1.1
|
||||||
|
JS8CALL_DIR=js8call-${JS8CALL_VERSION}
|
||||||
|
JS8CALL_TGZ=${JS8CALL_DIR}.tgz
|
||||||
|
wget http://files.js8call.com/${JS8CALL_VERSION}/${JS8CALL_TGZ}
|
||||||
|
tar xfz ${JS8CALL_TGZ}
|
||||||
|
# patch allows us to build against the packaged hamlib
|
||||||
|
patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch
|
||||||
|
rm /js8call-hamlib.patch
|
||||||
|
CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_LEGACY_HAMLIB" cmakebuild ${JS8CALL_DIR}
|
||||||
|
rm ${JS8CALL_TGZ}
|
||||||
|
|
||||||
WSJT_DIR=wsjtx-2.1.2
|
WSJT_DIR=wsjtx-2.1.2
|
||||||
WSJT_TGZ=${WSJT_DIR}.tgz
|
WSJT_TGZ=${WSJT_DIR}.tgz
|
||||||
wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ
|
wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
|
||||||
tar xvfz $WSJT_TGZ
|
tar xfz ${WSJT_TGZ}
|
||||||
cmakebuild $WSJT_DIR
|
patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch
|
||||||
|
mv /wsjtx.patch ${WSJT_DIR}
|
||||||
|
cmakebuild ${WSJT_DIR}
|
||||||
|
rm ${WSJT_TGZ}
|
||||||
|
|
||||||
git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git
|
git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git
|
||||||
cd direwolf
|
cd direwolf
|
||||||
patch -Np1 < /direwolf-1.5.patch
|
|
||||||
make
|
make
|
||||||
make install
|
make install
|
||||||
cd ..
|
cd ..
|
||||||
@ -62,4 +104,6 @@ pushd /opt/aprs-symbols
|
|||||||
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
|
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
|
||||||
popd
|
popd
|
||||||
|
|
||||||
apk del .build-deps
|
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
--- sdrplay/install_lib.sh 2018-06-21 18:47:08.000000000 +0000
|
|
||||||
+++ sdrplay/install_lib_patched.sh 2019-12-15 01:49:49.477386963 +0000
|
|
||||||
@@ -3,19 +3,7 @@
|
|
||||||
|
|
||||||
echo "Installing SDRplay RSP API library 2.13..."
|
|
||||||
|
|
||||||
-more sdrplay_license.txt
|
|
||||||
-
|
|
||||||
-while true; do
|
|
||||||
- echo "Press y and RETURN to accept the license agreement and continue with"
|
|
||||||
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
|
|
||||||
- case $yn in
|
|
||||||
- [Yy]* ) break;;
|
|
||||||
- [Nn]* ) exit;;
|
|
||||||
- * ) echo "Please answer y or n";;
|
|
||||||
- esac
|
|
||||||
-done
|
|
||||||
-
|
|
||||||
-export ARCH=`arch`
|
|
||||||
+export ARCH=`uname -m`
|
|
||||||
export VERS="2.13"
|
|
||||||
|
|
||||||
echo "Architecture: ${ARCH}"
|
|
||||||
@@ -63,16 +51,6 @@
|
|
||||||
echo " "
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
-
|
|
||||||
-if /sbin/ldconfig -p | /bin/fgrep -q libusb-1.0; then
|
|
||||||
- echo "Libusb found, continuing..."
|
|
||||||
-else
|
|
||||||
- echo " "
|
|
||||||
- echo "ERROR: Libusb cannot be found. Please install libusb and then run"
|
|
||||||
- echo "the installer again. Libusb can be installed from http://libusb.info"
|
|
||||||
- echo " "
|
|
||||||
- exit 1
|
|
||||||
-fi
|
|
||||||
|
|
||||||
sudo ldconfig
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
|||||||
--- sdrplay/install_lib.sh
|
|
||||||
+++ sdrplay/install_lib_patched.sh
|
|
||||||
@@ -3,19 +3,7 @@
|
|
||||||
|
|
||||||
echo "Installing SDRplay RSP API library 2.13..."
|
|
||||||
|
|
||||||
-more sdrplay_license.txt
|
|
||||||
-
|
|
||||||
-while true; do
|
|
||||||
- echo "Press y and RETURN to accept the license agreement and continue with"
|
|
||||||
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
|
|
||||||
- case $yn in
|
|
||||||
- [Yy]* ) break;;
|
|
||||||
- [Nn]* ) exit;;
|
|
||||||
- * ) echo "Please answer y or n";;
|
|
||||||
- esac
|
|
||||||
-done
|
|
||||||
-
|
|
||||||
-export ARCH=`arch`
|
|
||||||
+export ARCH=`uname -m`
|
|
||||||
export VERS="2.13"
|
|
||||||
|
|
||||||
echo "Architecture: ${ARCH}"
|
|
||||||
@@ -60,16 +48,6 @@
|
|
||||||
echo "ERROR: udev rules directory not found, add udev support and run the"
|
|
||||||
echo "installer again. udev support can be added by running..."
|
|
||||||
echo "sudo apt-get install libudev-dev"
|
|
||||||
- echo " "
|
|
||||||
- exit 1
|
|
||||||
-fi
|
|
||||||
-
|
|
||||||
-if /sbin/ldconfig -p | /bin/fgrep -q libusb-1.0; then
|
|
||||||
- echo "Libusb found, continuing..."
|
|
||||||
-else
|
|
||||||
- echo " "
|
|
||||||
- echo "ERROR: Libusb cannot be found. Please install libusb and then run"
|
|
||||||
- echo "the installer again. Libusb can be installed from http://libusb.info"
|
|
||||||
echo " "
|
|
||||||
exit 1
|
|
||||||
fi
|
|
@ -1,40 +0,0 @@
|
|||||||
--- sdrplay/install_lib.sh 2018-06-21 01:57:02.000000000 +0200
|
|
||||||
+++ sdrplay/install_lib_patched.sh 2019-01-22 17:21:06.445804136 +0100
|
|
||||||
@@ -2,19 +2,7 @@
|
|
||||||
|
|
||||||
echo "Installing SDRplay RSP API library 2.13..."
|
|
||||||
|
|
||||||
-more sdrplay_license.txt
|
|
||||||
-
|
|
||||||
-while true; do
|
|
||||||
- echo "Press y and RETURN to accept the license agreement and continue with"
|
|
||||||
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
|
|
||||||
- case $yn in
|
|
||||||
- [Yy]* ) break;;
|
|
||||||
- [Nn]* ) exit;;
|
|
||||||
- * ) echo "Please answer y or n";;
|
|
||||||
- esac
|
|
||||||
-done
|
|
||||||
-
|
|
||||||
-export ARCH=`arch`
|
|
||||||
+export ARCH=`uname -m`
|
|
||||||
export VERS="2.13"
|
|
||||||
|
|
||||||
echo "Architecture: ${ARCH}"
|
|
||||||
@@ -60,16 +48,6 @@
|
|
||||||
echo " "
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
-
|
|
||||||
-if /sbin/ldconfig -p | /bin/fgrep -q libusb-1.0; then
|
|
||||||
- echo "Libusb found, continuing..."
|
|
||||||
-else
|
|
||||||
- echo " "
|
|
||||||
- echo "ERROR: Libusb cannot be found. Please install libusb and then run"
|
|
||||||
- echo "the installer again. Libusb can be installed from http://libusb.info"
|
|
||||||
- echo " "
|
|
||||||
- exit 1
|
|
||||||
-fi
|
|
||||||
|
|
||||||
#echo "Installing SoapySDRPlay..."
|
|
||||||
|
|
@ -12,6 +12,9 @@ fi
|
|||||||
if [[ ! -f /etc/openwebrx/bookmarks.json ]] ; then
|
if [[ ! -f /etc/openwebrx/bookmarks.json ]] ; then
|
||||||
cp bookmarks.json /etc/openwebrx/
|
cp bookmarks.json /etc/openwebrx/
|
||||||
fi
|
fi
|
||||||
|
if [[ ! -f /etc/openwebrx/users.json ]] ; then
|
||||||
|
cp users.json /etc/openwebrx/
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
_term() {
|
_term() {
|
||||||
|
14
htdocs/css/admin.css
Normal file
14
htdocs/css/admin.css
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
@import url("openwebrx-header.css");
|
||||||
|
@import url("openwebrx-globals.css");
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .map-input {
|
||||||
|
margin: 15px 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
12
htdocs/css/bootstrap.min.css
vendored
Normal file
12
htdocs/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,11 +1,6 @@
|
|||||||
@import url("openwebrx-header.css");
|
@import url("openwebrx-header.css");
|
||||||
@import url("openwebrx-globals.css");
|
@import url("openwebrx-globals.css");
|
||||||
|
|
||||||
/* expandable photo not implemented on features page */
|
|
||||||
#webrx-top-photo-clip {
|
|
||||||
max-height: 67px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 50px 0;
|
margin: 50px 0;
|
||||||
|
24
htdocs/css/login.css
Normal file
24
htdocs/css/login.css
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
@import url("openwebrx-header.css");
|
||||||
|
@import url("openwebrx-globals.css");
|
||||||
|
|
||||||
|
.login {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #575757;
|
||||||
|
box-shadow: 0 0 20px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login {
|
||||||
|
height: 50px;
|
||||||
|
}
|
@ -1,11 +1,6 @@
|
|||||||
@import url("openwebrx-header.css");
|
@import url("openwebrx-header.css");
|
||||||
@import url("openwebrx-globals.css");
|
@import url("openwebrx-globals.css");
|
||||||
|
|
||||||
/* expandable photo not implemented on map page */
|
|
||||||
#webrx-top-photo-clip {
|
|
||||||
max-height: 67px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
{
|
{
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index:1000;
|
z-index:1000;
|
||||||
|
background-color: #575757;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-top-photo
|
#webrx-top-photo
|
||||||
@ -13,7 +14,8 @@
|
|||||||
#webrx-top-photo-clip
|
#webrx-top-photo-clip
|
||||||
{
|
{
|
||||||
min-height: 67px;
|
min-height: 67px;
|
||||||
max-height: 350px;
|
max-height: 67px;
|
||||||
|
height: 350px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -41,18 +43,21 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#webrx-tob-container, #webrx-top-container * {
|
||||||
|
line-height: initial;
|
||||||
|
box-sizing: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
#webrx-top-container img {
|
||||||
|
vertical-align: initial;
|
||||||
|
}
|
||||||
|
|
||||||
#webrx-top-logo
|
#webrx-top-logo
|
||||||
{
|
{
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-ha5kfu-top-logo
|
|
||||||
{
|
|
||||||
float: right;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#webrx-rx-avatar
|
#webrx-rx-avatar
|
||||||
{
|
{
|
||||||
background-color: rgba(154, 154, 154, .5);
|
background-color: rgba(154, 154, 154, .5);
|
||||||
@ -107,46 +112,38 @@
|
|||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 470px;
|
left: 470px;
|
||||||
top: 51px;
|
top: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-rx-details-arrow a
|
#openwebrx-rx-details-arrow a
|
||||||
{
|
{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
line-height: 0;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-rx-details-arrow-down
|
#openwebrx-main-buttons .button {
|
||||||
{
|
display: block;
|
||||||
display:none;
|
width: 55px;
|
||||||
}
|
|
||||||
|
|
||||||
#openwebrx-main-buttons ul
|
|
||||||
{
|
|
||||||
display: table;
|
|
||||||
margin:0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#openwebrx-main-buttons ul li
|
|
||||||
{
|
|
||||||
display: table-cell;
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 5px;
|
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#openwebrx-main-buttons .button img {
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
#openwebrx-main-buttons a {
|
#openwebrx-main-buttons a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-main-buttons li:hover
|
#openwebrx-main-buttons .button:hover
|
||||||
{
|
{
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-main-buttons li:active
|
#openwebrx-main-buttons .button:active
|
||||||
{
|
{
|
||||||
background-color: rgba(255, 255, 255, 0.55);
|
background-color: rgba(255, 255, 255, 0.55);
|
||||||
}
|
}
|
||||||
@ -154,6 +151,9 @@
|
|||||||
|
|
||||||
#openwebrx-main-buttons
|
#openwebrx-main-buttons
|
||||||
{
|
{
|
||||||
|
padding: 5px 15px;
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
float: right;
|
float: right;
|
||||||
margin:0;
|
margin:0;
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -150,6 +150,10 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
background: #B6B6B6;
|
background: #B6B6B6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=range]:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
#webrx-page-container
|
#webrx-page-container
|
||||||
{
|
{
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -311,7 +315,7 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-actual-freq {
|
.webrx-actual-freq {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -320,11 +324,11 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-actual-freq > * {
|
.webrx-actual-freq > * {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-actual-freq input {
|
.webrx-actual-freq input {
|
||||||
font-family: 'roboto-mono';
|
font-family: 'roboto-mono';
|
||||||
width: 0;
|
width: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -334,14 +338,13 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-actual-freq, #webrx-actual-freq input {
|
.webrx-actual-freq, .webrx-actual-freq input {
|
||||||
font-size: 16pt;
|
font-size: 16pt;
|
||||||
font-family: 'roboto-mono';
|
font-family: 'roboto-mono';
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webrx-mouse-freq
|
.webrx-mouse-freq {
|
||||||
{
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
@ -381,6 +384,7 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
-moz-border-radius: 15px;
|
-moz-border-radius: 15px;
|
||||||
margin: 5.9px;
|
margin: 5.9px;
|
||||||
|
box-sizing: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openwebrx-panel a
|
.openwebrx-panel a
|
||||||
@ -435,6 +439,10 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openwebrx-button.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.openwebrx-demodulator-button
|
.openwebrx-demodulator-button
|
||||||
{
|
{
|
||||||
width: 38px;
|
width: 38px;
|
||||||
@ -445,6 +453,10 @@ input[type=range]:focus::-ms-fill-upper
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openwebrx-demodulator-button.same-mod {
|
||||||
|
color: #FFC;
|
||||||
|
}
|
||||||
|
|
||||||
.openwebrx-square-button img
|
.openwebrx-square-button img
|
||||||
{
|
{
|
||||||
height: 27px;
|
height: 27px;
|
||||||
@ -723,8 +735,7 @@ img.openwebrx-mirror-img
|
|||||||
color: White;
|
color: White;
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-secondary-demod-listbox
|
.openwebrx-secondary-demod-listbox {
|
||||||
{
|
|
||||||
width: 173px;
|
width: 173px;
|
||||||
height: 27px;
|
height: 27px;
|
||||||
padding-left:3px;
|
padding-left:3px;
|
||||||
@ -923,37 +934,23 @@ img.openwebrx-mirror-img
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-panel-wsjt-message,
|
.openwebrx-message-panel {
|
||||||
#openwebrx-panel-packet-message,
|
|
||||||
#openwebrx-panel-pocsag-message
|
|
||||||
{
|
|
||||||
height: 180px;
|
height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-panel-wsjt-message tbody,
|
.openwebrx-message-panel tbody {
|
||||||
#openwebrx-panel-packet-message tbody,
|
|
||||||
#openwebrx-panel-pocsag-message tbody
|
|
||||||
{
|
|
||||||
display: block;
|
display: block;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-panel-wsjt-message thead tr,
|
.openwebrx-message-panel thead tr {
|
||||||
#openwebrx-panel-packet-message thead tr,
|
|
||||||
#openwebrx-panel-pocsag-message thead tr
|
|
||||||
{
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#openwebrx-panel-wsjt-message th,
|
.openwebrx-message-panel th,
|
||||||
#openwebrx-panel-wsjt-message td,
|
.openwebrx-message-panel td {
|
||||||
#openwebrx-panel-packet-message th,
|
|
||||||
#openwebrx-panel-packet-message td,
|
|
||||||
#openwebrx-panel-pocsag-message th,
|
|
||||||
#openwebrx-panel-pocsag-message td
|
|
||||||
{
|
|
||||||
width: 50px;
|
width: 50px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 1px 3px;
|
padding: 1px 3px;
|
||||||
@ -972,6 +969,31 @@ img.openwebrx-mirror-img
|
|||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#openwebrx-panel-js8-message .message {
|
||||||
|
width: 465px;
|
||||||
|
max-width: 465px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#openwebrx-panel-js8-message td.message {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
#openwebrx-panel-js8-message .message div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#openwebrx-panel-js8-message .decimal {
|
||||||
|
text-align: right;
|
||||||
|
width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#openwebrx-panel-js8-message .decimal.freq {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
#openwebrx-panel-packet-message .message {
|
#openwebrx-panel-packet-message .message {
|
||||||
width: 410px;
|
width: 410px;
|
||||||
max-width: 410px;
|
max-width: 410px;
|
||||||
@ -1078,13 +1100,15 @@ img.openwebrx-mirror-img
|
|||||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container,
|
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container,
|
||||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
|
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
|
||||||
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container,
|
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container,
|
||||||
|
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container,
|
||||||
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
|
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
|
||||||
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
|
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
|
||||||
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
|
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
|
||||||
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel,
|
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel,
|
||||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
|
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
|
||||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel,
|
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel,
|
||||||
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel
|
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel,
|
||||||
|
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel
|
||||||
{
|
{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -1095,7 +1119,8 @@ img.openwebrx-mirror-img
|
|||||||
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container,
|
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container,
|
||||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
|
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
|
||||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container,
|
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container,
|
||||||
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container
|
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container,
|
||||||
|
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container
|
||||||
{
|
{
|
||||||
height: 200px;
|
height: 200px;
|
||||||
margin: -10px;
|
margin: -10px;
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<HTML><HEAD>
|
<HTML><HEAD>
|
||||||
<TITLE>OpenWebRX Feature report</TITLE>
|
<TITLE>OpenWebRX Feature report</TITLE>
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
|
||||||
<link rel="stylesheet" href="static/css/features.css">
|
<link rel="stylesheet" href="static/css/features.css">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script>
|
||||||
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
||||||
|
<script src="static/lib/Header.js"></script>
|
||||||
<script src="static/features.js"></script>
|
<script src="static/features.js"></script>
|
||||||
</HEAD><BODY>
|
</HEAD><BODY>
|
||||||
${header}
|
${header}
|
||||||
|
20
htdocs/generalsettings.html
Normal file
20
htdocs/generalsettings.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OpenWebRX Settings</title>
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
|
||||||
|
<script src="https://unpkg.com/location-picker/dist/location-picker.min.js"></script>
|
||||||
|
<script src="compiled/settings.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${header}
|
||||||
|
<div class="container">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>General settings</h1>
|
||||||
|
</div>
|
||||||
|
${sections}
|
||||||
|
</div>
|
||||||
|
</body>
|
Binary file not shown.
Before Width: | Height: | Size: 2.0 KiB |
BIN
htdocs/gfx/openwebrx-panel-settings.png
Normal file
BIN
htdocs/gfx/openwebrx-panel-settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
@ -1,25 +1,23 @@
|
|||||||
<div id="webrx-top-container">
|
<div id="webrx-top-container">
|
||||||
<div id="webrx-top-photo-clip">
|
<div id="webrx-top-photo-clip">
|
||||||
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/>
|
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo" alt="Receiver panorama"/>
|
||||||
<div id="webrx-top-bar" class="webrx-top-bar-parts">
|
<div id="webrx-top-bar" class="webrx-top-bar-parts">
|
||||||
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
|
<a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a>
|
||||||
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
|
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
|
||||||
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png"/>
|
|
||||||
<div id="webrx-rx-texts">
|
<div id="webrx-rx-texts">
|
||||||
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
|
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
|
||||||
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
|
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="openwebrx-rx-details-arrow">
|
<div id="openwebrx-rx-details-arrow">
|
||||||
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a>
|
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger" style="display: none;"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a>
|
||||||
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a>
|
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a>
|
||||||
</div>
|
</div>
|
||||||
<section id="openwebrx-main-buttons">
|
<section id="openwebrx-main-buttons">
|
||||||
<ul>
|
<div class="button" data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" alt="Status"/><br/>Status</div>
|
||||||
<li data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" /><br/>Status</li>
|
<div class="button" data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" alt="Log"/><br/>Log</div>
|
||||||
<li data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" /><br/>Log</li>
|
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" alt="Receiver"/><br/>Receiver</div>
|
||||||
<li data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li>
|
<a class="button" href="map" target="openwebrx-map"><img src="static/gfx/openwebrx-panel-map.png" alt="Map"/><br/>Map</a>
|
||||||
<li><a href="map" target="_blank"><img src="static/gfx/openwebrx-panel-map.png" /><br/>Map</a></li>
|
${settingslink}
|
||||||
</ul>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div id="webrx-rx-photo-title"></div>
|
<div id="webrx-rx-photo-title"></div>
|
||||||
|
@ -24,14 +24,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
|
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||||
<script src="static/openwebrx.js"></script>
|
<script src="compiled/receiver.js"></script>
|
||||||
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
|
||||||
<script src="static/lib/jquery.nanoscroller.js"></script>
|
|
||||||
<script src="static/lib/BookmarkBar.js"></script>
|
|
||||||
<script src="static/lib/AudioEngine.js"></script>
|
|
||||||
<script src="static/lib/ProgressBar.js"></script>
|
|
||||||
<script src="static/lib/Measurement.js"></script>
|
|
||||||
<script src="static/lib/FrequencyDisplay.js"></script>
|
|
||||||
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
|
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
|
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@ -52,10 +45,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="openwebrx-panels-container">
|
<div id="openwebrx-panels-container">
|
||||||
<div id="openwebrx-panels-container-left">
|
<div id="openwebrx-panels-container-left">
|
||||||
<div class="openwebrx-panel" data-panel-name="client-under-devel" style="width: 245px; background-color: Red;">
|
|
||||||
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
|
|
||||||
<br />We're working on the code right now, so the application might fail.
|
|
||||||
</div>
|
|
||||||
<div class="openwebrx-panel" id="openwebrx-panel-digimodes" style="display: none; width: 619px;" data-panel-name="digimodes">
|
<div class="openwebrx-panel" id="openwebrx-panel-digimodes" style="display: none; width: 619px;" data-panel-name="digimodes">
|
||||||
<div id="openwebrx-digimode-canvas-container">
|
<div id="openwebrx-digimode-canvas-container">
|
||||||
<div id="openwebrx-digimode-select-channel"></div>
|
<div id="openwebrx-digimode-select-channel"></div>
|
||||||
@ -67,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="openwebrx-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message">
|
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th>UTC</th>
|
<th>UTC</th>
|
||||||
<th class="decimal">dB</th>
|
<th class="decimal">dB</th>
|
||||||
@ -77,7 +66,15 @@
|
|||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
<table class="openwebrx-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message">
|
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message">
|
||||||
|
<thead><tr>
|
||||||
|
<th>UTC</th>
|
||||||
|
<th class="decimal freq">Freq</th>
|
||||||
|
<th class="message">Message</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th>UTC</th>
|
<th>UTC</th>
|
||||||
<th class="callsign">Callsign</th>
|
<th class="callsign">Callsign</th>
|
||||||
@ -86,7 +83,7 @@
|
|||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
<table class="openwebrx-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message">
|
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th class="address">Address</th>
|
<th class="address">Address</th>
|
||||||
<th class="message">Message</th>
|
<th class="message">Message</th>
|
||||||
@ -126,26 +123,30 @@
|
|||||||
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
|
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
|
||||||
<div class="nano-content">
|
<div class="nano-content">
|
||||||
<div id="openwebrx-client-log-title">OpenWebRX client log</div>
|
<div id="openwebrx-client-log-title">OpenWebRX client log</div>
|
||||||
<div>Author contact: <a href="http://www.justjakob.de/" target="_blank">Jakob Ketterl, DD5JFK</a></div>
|
<div>
|
||||||
|
Author contact: <a href="http://www.justjakob.de/" target="_blank">Jakob Ketterl, DD5JFK</a> |
|
||||||
|
<a href="https://www.openwebrx.de" target="_blank">OpenWebRX homepage</a>
|
||||||
|
</div>
|
||||||
|
<div>Support and information: <a href="https://groups.io/g/openwebrx" target="_blank">Groups.io Mailinglist</a></div>
|
||||||
<div id="openwebrx-debugdiv"></div>
|
<div id="openwebrx-debugdiv"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" style="width: 615px;" data-panel-transparent="true">
|
<div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" style="width: 615px;" data-panel-transparent="true">
|
||||||
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer"> <span class="openwebrx-progressbar-text">Audio buffer [0 ms]</span><div class="openwebrx-progressbar-bar"></div></div>
|
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer" data-type="audiobuffer"></div>
|
||||||
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output"> <span class="openwebrx-progressbar-text">Audio output [0 sps]</span><div class="openwebrx-progressbar-bar"></div></div>
|
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output" data-type="audiooutput"></div>
|
||||||
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed"> <span class="openwebrx-progressbar-text">Audio stream [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
|
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed" data-type="audiospeed"></div>
|
||||||
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed"> <span class="openwebrx-progressbar-text">Network usage [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
|
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed" data-type="networkspeed"></div>
|
||||||
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div>
|
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu" data-type="cpu"></div>
|
||||||
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div>
|
<div class="openwebrx-progressbar" id="openwebrx-bar-clients" data-type="clients"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="openwebrx-panels-container-right">
|
<div id="openwebrx-panels-container-right">
|
||||||
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" style="width: 259px;">
|
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" style="width: 259px;">
|
||||||
<div class="openwebrx-panel-line frequencies-container">
|
<div class="openwebrx-panel-line frequencies-container">
|
||||||
<div class="frequencies">
|
<div class="frequencies">
|
||||||
<div id="webrx-actual-freq"></div>
|
<div class="webrx-actual-freq"></div>
|
||||||
<div id="webrx-mouse-freq"></div>
|
<div class="webrx-mouse-freq"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark...">
|
<div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark...">
|
||||||
<img src="static/gfx/openwebrx-bookmark.png">
|
<img src="static/gfx/openwebrx-bookmark.png">
|
||||||
@ -155,47 +156,7 @@
|
|||||||
<select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();">
|
<select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
|
<div class="openwebrx-modes openwebrx-panel-line"></div>
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
|
|
||||||
onclick="demodulator_analog_replace('nfm');">FM</div>
|
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-am"
|
|
||||||
onclick="demodulator_analog_replace('am');">AM</div>
|
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-lsb"
|
|
||||||
onclick="demodulator_analog_replace('lsb');">LSB</div>
|
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-usb"
|
|
||||||
onclick="demodulator_analog_replace('usb');">USB</div>
|
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
|
|
||||||
onclick="demodulator_analog_replace('cw');">CW</div>
|
|
||||||
</div>
|
|
||||||
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
|
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dmr"
|
|
||||||
style="display:none;" data-feature="digital_voice_digiham"
|
|
||||||
onclick="demodulator_analog_replace('dmr');">DMR</div>
|
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dstar"
|
|
||||||
style="display:none;" data-feature="digital_voice_dsd"
|
|
||||||
onclick="demodulator_analog_replace('dstar');">DStar</div>
|
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nxdn"
|
|
||||||
style="display:none;" data-feature="digital_voice_dsd"
|
|
||||||
onclick="demodulator_analog_replace('nxdn');">NXDN</div>
|
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-ysf"
|
|
||||||
style="display:none;" data-feature="digital_voice_digiham"
|
|
||||||
onclick="demodulator_analog_replace('ysf');">YSF</div>
|
|
||||||
</div>
|
|
||||||
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
|
|
||||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dig" onclick="demodulator_digital_replace_last();">DIG</div>
|
|
||||||
<select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();">
|
|
||||||
<option value="none"></option>
|
|
||||||
<option value="bpsk31">BPSK31</option>
|
|
||||||
<option value="bpsk63">BPSK63</option>
|
|
||||||
<option value="ft8" data-feature="wsjt-x">FT8</option>
|
|
||||||
<option value="wspr" data-feature="wsjt-x">WSPR</option>
|
|
||||||
<option value="jt65" data-feature="wsjt-x">JT65</option>
|
|
||||||
<option value="jt9" data-feature="wsjt-x">JT9</option>
|
|
||||||
<option value="ft4" data-feature="wsjt-x">FT4</option>
|
|
||||||
<option value="packet" data-feature="packet">Packet</option>
|
|
||||||
<option value="pocsag" data-feature="pocsag">Pocsag</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="openwebrx-panel-line">
|
<div class="openwebrx-panel-line">
|
||||||
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
|
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
|
||||||
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
|
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
|
||||||
@ -203,8 +164,8 @@
|
|||||||
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
|
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
|
||||||
</div>
|
</div>
|
||||||
<div class="openwebrx-panel-line">
|
<div class="openwebrx-panel-line">
|
||||||
<div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
|
<div title="Auto-set squelch level" class="openwebrx-squelch-default openwebrx-button"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
|
||||||
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()">
|
<input title="Squelch" class="openwebrx-squelch-slider openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1">
|
||||||
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
|
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
|
||||||
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
|
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
|
||||||
</div>
|
</div>
|
||||||
@ -249,17 +210,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="modulation">Modulation:</label>
|
<label for="modulation">Modulation:</label>
|
||||||
<select name="modulation" id="modulation">
|
<select name="modulation" id="modulation"></select>
|
||||||
<option value="nfm">FM</option>
|
|
||||||
<option value="am">AM</option>
|
|
||||||
<option value="usb">USB</option>
|
|
||||||
<option value="lsb">LSB</option>
|
|
||||||
<option value="cw">CW</option>
|
|
||||||
<option value="dmr">DMR</option>
|
|
||||||
<option value="dstar">D-Star</option>
|
|
||||||
<option value="nxdn">NXDN</option>
|
|
||||||
<option value="ysf">YSF</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<div class="openwebrx-button" data-action="cancel">Cancel</div>
|
<div class="openwebrx-button" data-action="cancel">Cancel</div>
|
||||||
|
@ -8,12 +8,10 @@ function BookmarkBar() {
|
|||||||
var $bookmark = $(e.target).closest('.bookmark');
|
var $bookmark = $(e.target).closest('.bookmark');
|
||||||
me.$container.find('.bookmark').removeClass('selected');
|
me.$container.find('.bookmark').removeClass('selected');
|
||||||
var b = $bookmark.data();
|
var b = $bookmark.data();
|
||||||
if (!b || !b.frequency || (!b.modulation && !b.digital_modulation)) return;
|
if (!b || !b.frequency || !b.modulation) return;
|
||||||
demodulators[0].set_offset_frequency(b.frequency - center_freq);
|
me.getDemodulator().set_offset_frequency(b.frequency - center_freq);
|
||||||
if (b.modulation) {
|
if (b.modulation) {
|
||||||
demodulator_analog_replace(b.modulation);
|
me.getDemodulatorPanel().setMode(b.modulation);
|
||||||
} else if (b.digital_modulation) {
|
|
||||||
demodulator_digital_replace(b.digital_modulation);
|
|
||||||
}
|
}
|
||||||
$bookmark.addClass('selected');
|
$bookmark.addClass('selected');
|
||||||
});
|
});
|
||||||
@ -104,40 +102,26 @@ BookmarkBar.prototype.render = function(){
|
|||||||
};
|
};
|
||||||
|
|
||||||
BookmarkBar.prototype.showEditDialog = function(bookmark) {
|
BookmarkBar.prototype.showEditDialog = function(bookmark) {
|
||||||
var $form = this.$dialog.find("form");
|
|
||||||
if (!bookmark) {
|
if (!bookmark) {
|
||||||
bookmark = {
|
bookmark = {
|
||||||
name: "",
|
name: "",
|
||||||
frequency: center_freq + demodulators[0].offset_frequency,
|
frequency: center_freq + this.getDemodulator().get_offset_frequency(),
|
||||||
modulation: demodulators[0].subtype
|
modulation: this.getDemodulator().get_secondary_demod() || this.getDemodulator().get_modulation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
['name', 'frequency', 'modulation'].forEach(function(key){
|
this.$dialog.bookmarkDialog().setValues(bookmark);
|
||||||
$form.find('#' + key).val(bookmark[key]);
|
|
||||||
});
|
|
||||||
this.$dialog.data('id', bookmark.id);
|
|
||||||
this.$dialog.show();
|
this.$dialog.show();
|
||||||
this.$dialog.find('#name').focus();
|
this.$dialog.find('#name').focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
BookmarkBar.prototype.storeBookmark = function() {
|
BookmarkBar.prototype.storeBookmark = function() {
|
||||||
var me = this;
|
var me = this;
|
||||||
var bookmark = {};
|
var bookmark = this.$dialog.bookmarkDialog().getValues();
|
||||||
var valid = true;
|
if (!bookmark) return;
|
||||||
['name', 'frequency', 'modulation'].forEach(function(key){
|
|
||||||
var $input = me.$dialog.find('#' + key);
|
|
||||||
valid = valid && $input[0].checkValidity();
|
|
||||||
bookmark[key] = $input.val();
|
|
||||||
});
|
|
||||||
if (!valid) {
|
|
||||||
me.$dialog.find("form :submit").click();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bookmark.frequency = Number(bookmark.frequency);
|
bookmark.frequency = Number(bookmark.frequency);
|
||||||
|
|
||||||
var bookmarks = me.localBookmarks.getBookmarks();
|
var bookmarks = me.localBookmarks.getBookmarks();
|
||||||
|
|
||||||
bookmark.id = me.$dialog.data('id');
|
|
||||||
if (!bookmark.id) {
|
if (!bookmark.id) {
|
||||||
if (bookmarks.length) {
|
if (bookmarks.length) {
|
||||||
bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; }));
|
bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; }));
|
||||||
@ -154,6 +138,14 @@ BookmarkBar.prototype.storeBookmark = function() {
|
|||||||
me.$dialog.hide();
|
me.$dialog.hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BookmarkBar.prototype.getDemodulatorPanel = function() {
|
||||||
|
return $('#openwebrx-panel-receiver').demodulatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
BookmarkBar.prototype.getDemodulator = function() {
|
||||||
|
return this.getDemodulatorPanel().getDemodulator();
|
||||||
|
};
|
||||||
|
|
||||||
BookmarkLocalStorage = function(){
|
BookmarkLocalStorage = function(){
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -171,7 +163,3 @@ BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
|
|||||||
bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
|
bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
|
||||||
this.setBookmarks(bookmarks);
|
this.setBookmarks(bookmarks);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
36
htdocs/lib/BookmarkDialog.js
Normal file
36
htdocs/lib/BookmarkDialog.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
$.fn.bookmarkDialog = function() {
|
||||||
|
var $el = this;
|
||||||
|
return {
|
||||||
|
setModes: function(modes) {
|
||||||
|
$el.find('#modulation').html(modes.filter(function(m){
|
||||||
|
return m.isAvailable();
|
||||||
|
}).map(function(m) {
|
||||||
|
return '<option value="' + m.modulation + '">' + m.name + '</option>';
|
||||||
|
}).join(''));
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
setValues: function(bookmark) {
|
||||||
|
var $form = $el.find('form');
|
||||||
|
['name', 'frequency', 'modulation'].forEach(function(key){
|
||||||
|
$form.find('#' + key).val(bookmark[key]);
|
||||||
|
});
|
||||||
|
$el.data('id', bookmark.id || false);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
getValues: function() {
|
||||||
|
var bookmark = {};
|
||||||
|
var valid = true;
|
||||||
|
['name', 'frequency', 'modulation'].forEach(function(key){
|
||||||
|
var $input = $el.find('#' + key);
|
||||||
|
valid = valid && $input[0].checkValidity();
|
||||||
|
bookmark[key] = $input.val();
|
||||||
|
});
|
||||||
|
if (!valid) {
|
||||||
|
$el.find("form :submit").click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bookmark.id = $el.data('id');
|
||||||
|
return bookmark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
356
htdocs/lib/Demodulator.js
Normal file
356
htdocs/lib/Demodulator.js
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
function Filter(demodulator) {
|
||||||
|
this.demodulator = demodulator;
|
||||||
|
this.min_passband = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
Filter.prototype.getLimits = function() {
|
||||||
|
var max_bw;
|
||||||
|
if (this.demodulator.get_secondary_demod() === 'pocsag') {
|
||||||
|
max_bw = 12500;
|
||||||
|
} else {
|
||||||
|
max_bw = (audioEngine.getOutputRate() / 2) - 1;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
high: max_bw,
|
||||||
|
low: -max_bw
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function Envelope(demodulator) {
|
||||||
|
this.demodulator = demodulator;
|
||||||
|
this.dragged_range = Demodulator.draggable_ranges.none;
|
||||||
|
}
|
||||||
|
|
||||||
|
Envelope.prototype.draw = function(visible_range){
|
||||||
|
this.visible_range = visible_range;
|
||||||
|
var line = center_freq + this.demodulator.offset_frequency;
|
||||||
|
|
||||||
|
// ____
|
||||||
|
// Draws a standard filter envelope like this: _/ \_
|
||||||
|
// Parameters are given in offset frequency (Hz).
|
||||||
|
// Envelope is drawn on the scale canvas.
|
||||||
|
// A "drag range" object is returned, containing information about the draggable areas of the envelope
|
||||||
|
// (beginning, ending and the line showing the offset frequency).
|
||||||
|
var env_bounding_line_w = 5; //
|
||||||
|
var env_att_w = 5; // _______ ___env_h2 in px ___|_____
|
||||||
|
var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_
|
||||||
|
var env_h2 = 5; // |||env_att_line_w |_env_lineplus
|
||||||
|
var env_lineplus = 1; // ||env_bounding_line_w
|
||||||
|
var env_line_click_area = 6;
|
||||||
|
//range=get_visible_freq_range();
|
||||||
|
var from = center_freq + this.demodulator.offset_frequency + this.demodulator.low_cut;
|
||||||
|
var from_px = scale_px_from_freq(from, range);
|
||||||
|
var to = center_freq + this.demodulator.offset_frequency + this.demodulator.high_cut;
|
||||||
|
var to_px = scale_px_from_freq(to, range);
|
||||||
|
if (to_px < from_px) /* swap'em */ {
|
||||||
|
var temp_px = to_px;
|
||||||
|
to_px = from_px;
|
||||||
|
from_px = temp_px;
|
||||||
|
}
|
||||||
|
|
||||||
|
from_px -= (env_att_w + env_bounding_line_w);
|
||||||
|
to_px += (env_att_w + env_bounding_line_w);
|
||||||
|
// do drawing:
|
||||||
|
scale_ctx.lineWidth = 3;
|
||||||
|
var color = this.color || '#ffff00'; // yellow
|
||||||
|
scale_ctx.strokeStyle = color;
|
||||||
|
scale_ctx.fillStyle = color;
|
||||||
|
var drag_ranges = {envelope_on_screen: false, line_on_screen: false};
|
||||||
|
if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen?
|
||||||
|
{
|
||||||
|
drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w};
|
||||||
|
drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px};
|
||||||
|
drag_ranges.whole_envelope = {x1: from_px, x2: to_px};
|
||||||
|
drag_ranges.envelope_on_screen = true;
|
||||||
|
scale_ctx.beginPath();
|
||||||
|
scale_ctx.moveTo(from_px, env_h1);
|
||||||
|
scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1);
|
||||||
|
scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2);
|
||||||
|
scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2);
|
||||||
|
scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1);
|
||||||
|
scale_ctx.lineTo(to_px, env_h1);
|
||||||
|
scale_ctx.globalAlpha = 0.3;
|
||||||
|
scale_ctx.fill();
|
||||||
|
scale_ctx.globalAlpha = 1;
|
||||||
|
scale_ctx.stroke();
|
||||||
|
}
|
||||||
|
if (typeof line !== "undefined") // out of screen?
|
||||||
|
{
|
||||||
|
var line_px = scale_px_from_freq(line, range);
|
||||||
|
if (!(line_px < 0 || line_px > window.innerWidth)) {
|
||||||
|
drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2};
|
||||||
|
drag_ranges.line_on_screen = true;
|
||||||
|
scale_ctx.moveTo(line_px, env_h1 + env_lineplus);
|
||||||
|
scale_ctx.lineTo(line_px, env_h2 - env_lineplus);
|
||||||
|
scale_ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.drag_ranges = drag_ranges;
|
||||||
|
};
|
||||||
|
|
||||||
|
Envelope.prototype.drag_start = function(x, key_modifiers){
|
||||||
|
this.key_modifiers = key_modifiers;
|
||||||
|
this.dragged_range = this.where_clicked(x, this.drag_ranges, key_modifiers);
|
||||||
|
this.drag_origin = {
|
||||||
|
x: x,
|
||||||
|
low_cut: this.demodulator.low_cut,
|
||||||
|
high_cut: this.demodulator.high_cut,
|
||||||
|
offset_frequency: this.demodulator.offset_frequency
|
||||||
|
};
|
||||||
|
return this.dragged_range !== Demodulator.draggable_ranges.none;
|
||||||
|
};
|
||||||
|
|
||||||
|
Envelope.prototype.where_clicked = function(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by envelope_draw().
|
||||||
|
var in_range = function (x, range) {
|
||||||
|
return range.x1 <= x && range.x2 >= x;
|
||||||
|
};
|
||||||
|
var dr = Demodulator.draggable_ranges;
|
||||||
|
|
||||||
|
if (key_modifiers.shiftKey) {
|
||||||
|
//Check first: shift + center drag emulates BFO knob
|
||||||
|
if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo;
|
||||||
|
//Check second: shift + envelope drag emulates PBF knob
|
||||||
|
if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs;
|
||||||
|
}
|
||||||
|
if (drag_ranges.envelope_on_screen) {
|
||||||
|
// For low and high cut:
|
||||||
|
if (in_range(x, drag_ranges.beginning)) return dr.beginning;
|
||||||
|
if (in_range(x, drag_ranges.ending)) return dr.ending;
|
||||||
|
// Last priority: having clicked anything else on the envelope, without holding the shift key
|
||||||
|
if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else;
|
||||||
|
}
|
||||||
|
return dr.none; //User doesn't drag the envelope for this demodulator
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Envelope.prototype.drag_move = function(x) {
|
||||||
|
var dr = Demodulator.draggable_ranges;
|
||||||
|
var new_value;
|
||||||
|
if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all
|
||||||
|
var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x));
|
||||||
|
|
||||||
|
//dragging the line in the middle of the filter envelope while holding Shift does emulate
|
||||||
|
//the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged
|
||||||
|
//Filter passband moves in the opposite direction than dragged, hence the minus below.
|
||||||
|
var minus = (this.dragged_range === dr.bfo) ? -1 : 1;
|
||||||
|
//dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob
|
||||||
|
//(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset
|
||||||
|
//frequency.
|
||||||
|
if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) {
|
||||||
|
//we don't let low_cut go beyond its limits
|
||||||
|
if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.demodulator.filter.getLimits().low) return true;
|
||||||
|
//nor the filter passband be too small
|
||||||
|
if (this.demodulator.high_cut - new_value < this.demodulator.filter.min_passband) return true;
|
||||||
|
//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
|
||||||
|
if (new_value >= this.demodulator.high_cut) return true;
|
||||||
|
this.demodulator.setLowCut(new_value);
|
||||||
|
}
|
||||||
|
if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) {
|
||||||
|
//we don't let high_cut go beyond its limits
|
||||||
|
if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.demodulator.filter.getLimits().high) return true;
|
||||||
|
//nor the filter passband be too small
|
||||||
|
if (new_value - this.demodulator.low_cut < this.demodulator.filter.min_passband) return true;
|
||||||
|
//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
|
||||||
|
if (new_value <= this.demodulator.low_cut) return true;
|
||||||
|
this.demodulator.setHighCut(new_value);
|
||||||
|
}
|
||||||
|
if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) {
|
||||||
|
//when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it)
|
||||||
|
new_value = this.drag_origin.offset_frequency + freq_change;
|
||||||
|
if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-)
|
||||||
|
this.demodulator.set_offset_frequency(new_value);
|
||||||
|
}
|
||||||
|
//now do the actual modifications:
|
||||||
|
//mkenvelopes(this.visible_range);
|
||||||
|
//this.demodulator.set();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Envelope.prototype.drag_end = function(){
|
||||||
|
var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset
|
||||||
|
this.dragged_range = Demodulator.draggable_ranges.none;
|
||||||
|
return to_return;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//******* class Demodulator_default_analog *******
|
||||||
|
// This can be used as a base for basic audio demodulators.
|
||||||
|
// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB
|
||||||
|
|
||||||
|
function Demodulator(offset_frequency, modulation) {
|
||||||
|
this.offset_frequency = offset_frequency;
|
||||||
|
this.envelope = new Envelope(this);
|
||||||
|
this.color = Demodulator.get_next_color();
|
||||||
|
this.modulation = modulation;
|
||||||
|
this.filter = new Filter(this);
|
||||||
|
this.squelch_level = -150;
|
||||||
|
this.dmr_filter = 3;
|
||||||
|
this.started = false;
|
||||||
|
this.state = {};
|
||||||
|
this.secondary_demod = false;
|
||||||
|
var mode = Modes.findByModulation(modulation);
|
||||||
|
if (mode) {
|
||||||
|
this.low_cut = mode.bandpass.low_cut;
|
||||||
|
this.high_cut = mode.bandpass.high_cut;
|
||||||
|
}
|
||||||
|
this.listeners = {
|
||||||
|
"frequencychange": [],
|
||||||
|
"squelchchange": []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//ranges on filter envelope that can be dragged:
|
||||||
|
Demodulator.draggable_ranges = {
|
||||||
|
none: 0,
|
||||||
|
beginning: 1 /*from*/,
|
||||||
|
ending: 2 /*to*/,
|
||||||
|
anything_else: 3,
|
||||||
|
bfo: 4 /*line (while holding shift)*/,
|
||||||
|
pbs: 5
|
||||||
|
}; //to which parameter these correspond in envelope_draw()
|
||||||
|
|
||||||
|
Demodulator.color_index = 0;
|
||||||
|
Demodulator.colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"];
|
||||||
|
|
||||||
|
Demodulator.get_next_color = function() {
|
||||||
|
if (this.color_index >= this.colors.length) this.color_index = 0;
|
||||||
|
return (this.colors[this.color_index++]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Demodulator.prototype.on = function(event, handler) {
|
||||||
|
this.listeners[event].push(handler);
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.emit = function(event, params) {
|
||||||
|
this.listeners[event].forEach(function(fn) {
|
||||||
|
fn(params);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.set_offset_frequency = function(to_what) {
|
||||||
|
if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
|
||||||
|
to_what = Math.round(to_what);
|
||||||
|
if (this.offset_frequency === to_what) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.offset_frequency = to_what;
|
||||||
|
this.set();
|
||||||
|
this.emit("frequencychange", to_what);
|
||||||
|
mkenvelopes(get_visible_freq_range());
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.get_offset_frequency = function() {
|
||||||
|
return this.offset_frequency;
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.get_modulation = function() {
|
||||||
|
return this.modulation;
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.start = function() {
|
||||||
|
this.started = true;
|
||||||
|
this.set();
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
"type": "dspcontrol",
|
||||||
|
"action": "start"
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO check if this is actually used
|
||||||
|
Demodulator.prototype.stop = function() {
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.send = function(params) {
|
||||||
|
ws.send(JSON.stringify({"type": "dspcontrol", "params": params}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Demodulator.prototype.set = function () { //this function sends demodulator parameters to the server
|
||||||
|
if (!this.started) return;
|
||||||
|
var params = {
|
||||||
|
"low_cut": this.low_cut,
|
||||||
|
"high_cut": this.high_cut,
|
||||||
|
"offset_freq": this.offset_frequency,
|
||||||
|
"mod": this.modulation,
|
||||||
|
"dmr_filter": this.dmr_filter,
|
||||||
|
"squelch_level": this.squelch_level,
|
||||||
|
"secondary_mod": this.secondary_demod,
|
||||||
|
"secondary_offset_freq": this.secondary_offset_freq
|
||||||
|
};
|
||||||
|
var to_send = {};
|
||||||
|
for (var key in params) {
|
||||||
|
if (!(key in this.state) || params[key] !== this.state[key]) {
|
||||||
|
to_send[key] = params[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(to_send).length > 0) {
|
||||||
|
this.send(to_send);
|
||||||
|
for (var key in to_send) {
|
||||||
|
this.state[key] = to_send[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mkenvelopes(get_visible_freq_range());
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.setSquelch = function(squelch) {
|
||||||
|
if (this.squelch_level == squelch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.squelch_level = squelch;
|
||||||
|
this.set();
|
||||||
|
this.emit("squelchchange", squelch);
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.getSquelch = function() {
|
||||||
|
return this.squelch_level;
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.setDmrFilter = function(dmr_filter) {
|
||||||
|
this.dmr_filter = dmr_filter;
|
||||||
|
this.set();
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.setBandpass = function(bandpass) {
|
||||||
|
this.bandpass = bandpass;
|
||||||
|
this.low_cut = bandpass.low_cut;
|
||||||
|
this.high_cut = bandpass.high_cut;
|
||||||
|
this.set();
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.setLowCut = function(low_cut) {
|
||||||
|
this.low_cut = low_cut;
|
||||||
|
this.set();
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.setHighCut = function(high_cut) {
|
||||||
|
this.high_cut = high_cut;
|
||||||
|
this.set();
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.getBandpass = function() {
|
||||||
|
return {
|
||||||
|
low_cut: this.low_cut,
|
||||||
|
high_cut: this.high_cut
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.set_secondary_demod = function(secondary_demod) {
|
||||||
|
if (this.secondary_demod === secondary_demod) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.secondary_demod = secondary_demod;
|
||||||
|
this.set();
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.get_secondary_demod = function() {
|
||||||
|
return this.secondary_demod;
|
||||||
|
};
|
||||||
|
|
||||||
|
Demodulator.prototype.set_secondary_offset_freq = function(secondary_offset) {
|
||||||
|
if (this.secondary_offset_freq === secondary_offset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.secondary_offset_freq = secondary_offset;
|
||||||
|
this.set();
|
||||||
|
};
|
333
htdocs/lib/DemodulatorPanel.js
Normal file
333
htdocs/lib/DemodulatorPanel.js
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
function DemodulatorPanel(el) {
|
||||||
|
var self = this;
|
||||||
|
self.el = el;
|
||||||
|
self.demodulator = null;
|
||||||
|
self.mode = null;
|
||||||
|
|
||||||
|
var displayEl = el.find('.webrx-actual-freq')
|
||||||
|
this.tuneableFrequencyDisplay = displayEl.tuneableFrequencyDisplay();
|
||||||
|
displayEl.on('frequencychange', function(event, freq) {
|
||||||
|
self.getDemodulator().set_offset_frequency(freq - self.center_freq);
|
||||||
|
});
|
||||||
|
|
||||||
|
Modes.registerModePanel(this);
|
||||||
|
el.on('click', '.openwebrx-demodulator-button', function() {
|
||||||
|
var modulation = $(this).data('modulation');
|
||||||
|
if (modulation) {
|
||||||
|
self.setMode(modulation);
|
||||||
|
} else {
|
||||||
|
self.disableDigiMode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
el.on('change', '.openwebrx-secondary-demod-listbox', function() {
|
||||||
|
var value = $(this).val();
|
||||||
|
if (value === 'none') {
|
||||||
|
self.disableDigiMode();
|
||||||
|
} else {
|
||||||
|
self.setMode(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
el.on('click', '.openwebrx-squelch-default', function() {
|
||||||
|
if (!self.squelchAvailable()) return;
|
||||||
|
el.find('.openwebrx-squelch-slider').val(getLogSmeterValue(smeter_level) + 10);
|
||||||
|
self.updateSquelch();
|
||||||
|
});
|
||||||
|
el.on('change', '.openwebrx-squelch-slider', function() {
|
||||||
|
self.updateSquelch();
|
||||||
|
});
|
||||||
|
window.addEventListener('hashchange', function() {
|
||||||
|
self.onHashChange();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.render = function() {
|
||||||
|
var available = Modes.getModes().filter(function(m){ return m.isAvailable(); });
|
||||||
|
var normalModes = available.filter(function(m){ return m.type === 'analog'; });
|
||||||
|
var digiModes = available.filter(function(m){ return m.type === 'digimode'; });
|
||||||
|
|
||||||
|
var html = []
|
||||||
|
|
||||||
|
var buttons = normalModes.map(function(m){
|
||||||
|
return $(
|
||||||
|
'<div ' +
|
||||||
|
'class="openwebrx-button openwebrx-demodulator-button" ' +
|
||||||
|
'data-modulation="' + m.modulation + '" ' +
|
||||||
|
'id="openwebrx-button-' + m.modulation + '" r' +
|
||||||
|
'>' + m.name + '</div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
var arrayLength = buttons.length;
|
||||||
|
var chunks = [];
|
||||||
|
|
||||||
|
for (index = 0; index < arrayLength; index += 5) {
|
||||||
|
chunks.push(buttons.slice(index, index + 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
html.push.apply(html, chunks.map(function(chunk){
|
||||||
|
$line = $('<div class="openwebrx-panel-line openwebrx-panel-flex-line"></div>');
|
||||||
|
$line.append.apply($line, chunk);
|
||||||
|
return $line
|
||||||
|
}));
|
||||||
|
|
||||||
|
html.push($(
|
||||||
|
'<div class="openwebrx-panel-line openwebrx-panel-flex-line">' +
|
||||||
|
'<div class="openwebrx-button openwebrx-demodulator-button openwebrx-button-dig">DIG</div>' +
|
||||||
|
'<select class="openwebrx-secondary-demod-listbox">' +
|
||||||
|
'<option value="none"></option>' +
|
||||||
|
digiModes.map(function(m){
|
||||||
|
return '<option value="' + m.modulation + '">' + m.name + '</option>';
|
||||||
|
}).join('') +
|
||||||
|
'</select>' +
|
||||||
|
'</div>'
|
||||||
|
));
|
||||||
|
|
||||||
|
this.el.find(".openwebrx-modes").html(html);
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.setMode = function(requestedModulation) {
|
||||||
|
var mode = Modes.findByModulation(requestedModulation);
|
||||||
|
if (!mode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.mode === mode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mode.isAvailable()) {
|
||||||
|
divlog('Modulation "' + mode.name + '" not supported. Please check requirements', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode.type === 'digimode') {
|
||||||
|
modulation = mode.underlying[0];
|
||||||
|
} else {
|
||||||
|
if (this.mode && this.mode.type === 'digimode' && this.mode.underlying.indexOf(requestedModulation) >= 0) {
|
||||||
|
// keep the mode, just switch underlying modulation
|
||||||
|
mode = this.mode;
|
||||||
|
modulation = requestedModulation;
|
||||||
|
} else {
|
||||||
|
modulation = mode.modulation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var current = this.collectParams();
|
||||||
|
if (this.demodulator) {
|
||||||
|
current.offset_frequency = this.demodulator.get_offset_frequency();
|
||||||
|
current.squelch_level = this.demodulator.getSquelch();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stopDemodulator();
|
||||||
|
this.demodulator = new Demodulator(current.offset_frequency, modulation);
|
||||||
|
this.demodulator.setSquelch(current.squelch_level);
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var updateFrequency = function(freq) {
|
||||||
|
self.tuneableFrequencyDisplay.setFrequency(self.center_freq + freq);
|
||||||
|
self.updateHash();
|
||||||
|
};
|
||||||
|
this.demodulator.on("frequencychange", updateFrequency);
|
||||||
|
updateFrequency(this.demodulator.get_offset_frequency());
|
||||||
|
var updateSquelch = function(squelch) {
|
||||||
|
self.el.find('.openwebrx-squelch-slider').val(squelch);
|
||||||
|
self.updateHash();
|
||||||
|
};
|
||||||
|
this.demodulator.on('squelchchange', updateSquelch);
|
||||||
|
updateSquelch(this.demodulator.getSquelch());
|
||||||
|
|
||||||
|
if (mode.type === 'digimode') {
|
||||||
|
this.demodulator.set_secondary_demod(mode.modulation);
|
||||||
|
if (mode.bandpass) {
|
||||||
|
this.demodulator.setBandpass(mode.bandpass);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.demodulator.set_secondary_demod(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.demodulator.start();
|
||||||
|
this.mode = mode;
|
||||||
|
|
||||||
|
this.updateButtons();
|
||||||
|
this.updatePanels();
|
||||||
|
this.updateHash();
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.disableDigiMode = function() {
|
||||||
|
// just a little trick to get out of the digimode
|
||||||
|
delete this.mode;
|
||||||
|
this.setMode(this.getDemodulator().get_modulation());
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.updatePanels = function() {
|
||||||
|
var modulation = this.getDemodulator().get_secondary_demod();
|
||||||
|
$('#openwebrx-panel-digimodes').attr('data-mode', modulation);
|
||||||
|
toggle_panel("openwebrx-panel-digimodes", !!modulation);
|
||||||
|
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(modulation) >= 0);
|
||||||
|
toggle_panel("openwebrx-panel-js8-message", modulation == "js8");
|
||||||
|
toggle_panel("openwebrx-panel-packet-message", modulation === "packet");
|
||||||
|
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");
|
||||||
|
|
||||||
|
modulation = this.getDemodulator().get_modulation();
|
||||||
|
var showing = 'openwebrx-panel-metadata-' + modulation;
|
||||||
|
$(".openwebrx-meta-panel").each(function (_, p) {
|
||||||
|
toggle_panel(p.id, p.id === showing);
|
||||||
|
});
|
||||||
|
clear_metadata();
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.getDemodulator = function() {
|
||||||
|
return this.demodulator;
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.collectParams = function() {
|
||||||
|
var defaults = {
|
||||||
|
offset_frequency: 0,
|
||||||
|
squelch_level: -150,
|
||||||
|
mod: 'nfm'
|
||||||
|
}
|
||||||
|
return $.extend(new Object(), defaults, this.initialParams || {}, this.transformHashParams(this.parseHash()));
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.startDemodulator = function() {
|
||||||
|
if (!Modes.initComplete()) return;
|
||||||
|
var params = this.collectParams();
|
||||||
|
this._apply(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.stopDemodulator = function() {
|
||||||
|
if (!this.demodulator) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.demodulator.stop();
|
||||||
|
this.demodulator = null;
|
||||||
|
this.mode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype._apply = function(params) {
|
||||||
|
this.setMode(params.mod);
|
||||||
|
this.getDemodulator().set_offset_frequency(params.offset_frequency);
|
||||||
|
this.getDemodulator().setSquelch(params.squelch_level);
|
||||||
|
this.updateButtons();
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.setInitialParams = function(params) {
|
||||||
|
this.initialParams = params;
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.onHashChange = function() {
|
||||||
|
this._apply(this.transformHashParams(this.parseHash()));
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.transformHashParams = function(params) {
|
||||||
|
var ret = {
|
||||||
|
mod: params.secondary_mod || params.mod
|
||||||
|
};
|
||||||
|
if (typeof(params.offset_frequency) !== 'undefined') ret.offset_frequency = params.offset_frequency;
|
||||||
|
if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.squelchAvailable = function () {
|
||||||
|
return this.mode && this.mode.squelch;
|
||||||
|
}
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.updateButtons = function() {
|
||||||
|
var $buttons = this.el.find(".openwebrx-demodulator-button");
|
||||||
|
$buttons.removeClass("highlighted").removeClass('same-mod');
|
||||||
|
var demod = this.getDemodulator()
|
||||||
|
if (!demod) return;
|
||||||
|
this.el.find('[data-modulation=' + demod.get_modulation() + ']').addClass("highlighted");
|
||||||
|
var secondary_demod = demod.get_secondary_demod()
|
||||||
|
if (secondary_demod) {
|
||||||
|
this.el.find(".openwebrx-button-dig").addClass("highlighted");
|
||||||
|
this.el.find('.openwebrx-secondary-demod-listbox').val(secondary_demod);
|
||||||
|
var mode = Modes.findByModulation(secondary_demod);
|
||||||
|
if (mode) {
|
||||||
|
var self = this;
|
||||||
|
mode.underlying.filter(function(m) {
|
||||||
|
return m !== demod.get_modulation();
|
||||||
|
}).forEach(function(m) {
|
||||||
|
self.el.find('[data-modulation=' + m + ']').addClass('same-mod')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.el.find('.openwebrx-secondary-demod-listbox').val('none');
|
||||||
|
}
|
||||||
|
var squelch_disabled = !this.squelchAvailable();
|
||||||
|
this.el.find('.openwebrx-squelch-slider').prop('disabled', squelch_disabled);
|
||||||
|
this.el.find('.openwebrx-squelch-default')[squelch_disabled ? 'addClass' : 'removeClass']('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.setCenterFrequency = function(center_freq) {
|
||||||
|
if (this.center_freq === center_freq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.stopDemodulator();
|
||||||
|
this.center_freq = center_freq;
|
||||||
|
this.startDemodulator();
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.parseHash = function() {
|
||||||
|
if (!window.location.hash) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
var params = window.location.hash.substring(1).split(",").map(function(x) {
|
||||||
|
var harr = x.split('=');
|
||||||
|
return [harr[0], harr.slice(1).join('=')];
|
||||||
|
}).reduce(function(params, p){
|
||||||
|
params[p[0]] = p[1];
|
||||||
|
return params;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return this.validateHash(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.validateHash = function(params) {
|
||||||
|
var self = this;
|
||||||
|
params = Object.keys(params).filter(function(key) {
|
||||||
|
if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') {
|
||||||
|
return params.freq && Math.abs(params.freq - self.center_freq) < bandwidth;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).reduce(function(p, key) {
|
||||||
|
p[key] = params[key];
|
||||||
|
return p;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
if (params['freq']) {
|
||||||
|
params['offset_frequency'] = params['freq'] - self.center_freq;
|
||||||
|
delete params['freq'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.updateHash = function() {
|
||||||
|
var demod = this.getDemodulator();
|
||||||
|
if (!demod) return;
|
||||||
|
var self = this;
|
||||||
|
window.location.hash = $.map({
|
||||||
|
freq: demod.get_offset_frequency() + self.center_freq,
|
||||||
|
mod: demod.get_modulation(),
|
||||||
|
secondary_mod: demod.get_secondary_demod(),
|
||||||
|
sql: demod.getSquelch(),
|
||||||
|
}, function(value, key){
|
||||||
|
if (typeof(value) === 'undefined' || value === false) return undefined;
|
||||||
|
return key + '=' + value;
|
||||||
|
}).filter(function(v) {
|
||||||
|
return !!v;
|
||||||
|
}).join(',');
|
||||||
|
};
|
||||||
|
|
||||||
|
DemodulatorPanel.prototype.updateSquelch = function() {
|
||||||
|
var sliderValue = parseInt(this.el.find(".openwebrx-squelch-slider").val());
|
||||||
|
var demod = this.getDemodulator();
|
||||||
|
if (demod) demod.setSquelch(sliderValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.demodulatorPanel = function(){
|
||||||
|
if (!this.data('panel')) {
|
||||||
|
this.data('panel', new DemodulatorPanel(this));
|
||||||
|
};
|
||||||
|
return this.data('panel');
|
||||||
|
};
|
@ -50,7 +50,6 @@ TuneableFrequencyDisplay.prototype.setupElements = function() {
|
|||||||
|
|
||||||
TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
||||||
var me = this;
|
var me = this;
|
||||||
me.listeners = [];
|
|
||||||
|
|
||||||
me.element.on('wheel', function(e){
|
me.element.on('wheel', function(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -63,17 +62,13 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
|||||||
if (e.originalEvent.deltaY > 0) delta *= -1;
|
if (e.originalEvent.deltaY > 0) delta *= -1;
|
||||||
var newFrequency = me.frequency + delta;
|
var newFrequency = me.frequency + delta;
|
||||||
|
|
||||||
me.listeners.forEach(function(l) {
|
me.element.trigger('frequencychange', newFrequency);
|
||||||
l(newFrequency);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var submit = function(){
|
var submit = function(){
|
||||||
var freq = parseInt(me.input.val());
|
var freq = parseInt(me.input.val());
|
||||||
if (!isNaN(freq)) {
|
if (!isNaN(freq)) {
|
||||||
me.listeners.forEach(function(l) {
|
me.element.trigger('frequencychange', freq);
|
||||||
l(freq);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
me.input.hide();
|
me.input.hide();
|
||||||
me.displayContainer.show();
|
me.displayContainer.show();
|
||||||
@ -96,6 +91,16 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
TuneableFrequencyDisplay.prototype.onFrequencyChange = function(listener){
|
$.fn.frequencyDisplay = function() {
|
||||||
this.listeners.push(listener);
|
if (!this.data('frequencyDisplay')) {
|
||||||
};
|
this.data('frequencyDisplay', new FrequencyDisplay(this));
|
||||||
|
}
|
||||||
|
return this.data('frequencyDisplay');
|
||||||
|
}
|
||||||
|
|
||||||
|
$.fn.tuneableFrequencyDisplay = function() {
|
||||||
|
if (!this.data('frequencyDisplay')) {
|
||||||
|
this.data('frequencyDisplay', new TuneableFrequencyDisplay(this));
|
||||||
|
}
|
||||||
|
return this.data('frequencyDisplay');
|
||||||
|
}
|
||||||
|
77
htdocs/lib/Header.js
Normal file
77
htdocs/lib/Header.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
function Header(el) {
|
||||||
|
this.el = el;
|
||||||
|
|
||||||
|
this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () {
|
||||||
|
toggle_panel($(this).data('toggle-panel'));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.init_rx_photo();
|
||||||
|
this.download_details();
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.prototype.setDetails = function(details) {
|
||||||
|
this.el.find('#webrx-rx-title').html(details['receiver_name']);
|
||||||
|
var query = encodeURIComponent(details['receiver_gps']['lat'] + ',' + details['receiver_gps']['lon']);
|
||||||
|
this.el.find('#webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m, <a href="https://www.google.com/maps/search/?api=1&query=' + query + '" target="_blank">[maps]</a>');
|
||||||
|
this.el.find('#webrx-rx-photo-title').html(details['photo_title']);
|
||||||
|
this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']);
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.prototype.init_rx_photo = function() {
|
||||||
|
this.rx_photo_state = 0;
|
||||||
|
|
||||||
|
$.extend($.easing, {
|
||||||
|
easeOutCubic:function(x) {
|
||||||
|
return 1 - Math.pow( 1 - x, 3 );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this));
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.prototype.close_rx_photo = function() {
|
||||||
|
this.rx_photo_state = 0;
|
||||||
|
this.el.find("#webrx-rx-photo-desc").animate({opacity: 0});
|
||||||
|
this.el.find("#webrx-rx-photo-title").animate({opacity: 0});
|
||||||
|
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'});
|
||||||
|
this.el.find("#openwebrx-rx-details-arrow-down").show();
|
||||||
|
this.el.find("#openwebrx-rx-details-arrow-up").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.prototype.open_rx_photo = function() {
|
||||||
|
this.rx_photo_state = 1;
|
||||||
|
this.el.find("#webrx-rx-photo-desc").animate({opacity: 1});
|
||||||
|
this.el.find("#webrx-rx-photo-title").animate({opacity: 1});
|
||||||
|
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 350}, {duration: 1000, easing: 'easeOutCubic'});
|
||||||
|
this.el.find("#openwebrx-rx-details-arrow-down").hide();
|
||||||
|
this.el.find("#openwebrx-rx-details-arrow-up").show();
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.prototype.toggle_rx_photo = function(ev) {
|
||||||
|
if (ev && ev.target && ev.target.tagName == 'A') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.rx_photo_state) {
|
||||||
|
this.close_rx_photo();
|
||||||
|
} else {
|
||||||
|
this.open_rx_photo();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.prototype.download_details = function() {
|
||||||
|
var self = this;
|
||||||
|
$.ajax('api/receiverdetails').done(function(data){
|
||||||
|
self.setDetails(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.header = function() {
|
||||||
|
if (!this.data('header')) {
|
||||||
|
this.data('header', new Header(this));
|
||||||
|
}
|
||||||
|
return this.data('header');
|
||||||
|
};
|
||||||
|
|
||||||
|
$(function(){
|
||||||
|
$('#webrx-top-container').header();
|
||||||
|
});
|
150
htdocs/lib/Js8Threads.js
Normal file
150
htdocs/lib/Js8Threads.js
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
Js8Thread = function(el){
|
||||||
|
this.messages = [];
|
||||||
|
this.el = el;
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.getAverageFrequency = function(){
|
||||||
|
var total = this.messages.map(function(message){
|
||||||
|
return message.freq;
|
||||||
|
}).reduce(function(t, f){
|
||||||
|
return t + f;
|
||||||
|
}, 0);
|
||||||
|
return total / this.messages.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.pushMessage = function(message) {
|
||||||
|
this.messages.push(message);
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.render = function() {
|
||||||
|
this.el.html(
|
||||||
|
'<td>' + this.renderTimestamp(this.getLatestTimestamp()) + '</td>' +
|
||||||
|
'<td class="decimal freq">' + Math.round(this.getAverageFrequency()) + '</td>' +
|
||||||
|
'<td class="message"><div>' + this.renderMessages() + '</div></td>'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.getLatestTimestamp = function() {
|
||||||
|
return this.messages[0].timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.isOpen = function() {
|
||||||
|
if (!this.messages.length) return true;
|
||||||
|
var last_message = this.messages[this.messages.length - 1];
|
||||||
|
return (last_message.thread_type & 2) === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.renderMessages = function() {
|
||||||
|
var res = [];
|
||||||
|
for (var i = 0; i < this.messages.length; i++) {
|
||||||
|
var msg = this.messages[i];
|
||||||
|
if (msg.thread_type & 1) {
|
||||||
|
res.push('[ ');
|
||||||
|
} else if (i === 0 || msg.timestamp - this.messages[i - 1].timestamp > this.getMessageDuration()) {
|
||||||
|
res.push(' ... ');
|
||||||
|
}
|
||||||
|
res.push(msg.msg);
|
||||||
|
if (msg.thread_type & 2) {
|
||||||
|
res.push(' ]');
|
||||||
|
} else if (i === this.messages.length -1) {
|
||||||
|
res.push(' ... ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.getMessageDuration = function() {
|
||||||
|
switch (this.getMode()) {
|
||||||
|
case 'A':
|
||||||
|
return 15000;
|
||||||
|
case 'E':
|
||||||
|
return 30000;
|
||||||
|
case 'B':
|
||||||
|
return 10000;
|
||||||
|
case 'C':
|
||||||
|
return 6000;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.getMode = function() {
|
||||||
|
// we filter messages by mode, so the first one is as good as any
|
||||||
|
if (!this.messages.length) return;
|
||||||
|
return this.messages[0].mode;
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.acceptsMode = function(mode) {
|
||||||
|
var currentMode = this.getMode();
|
||||||
|
return typeof(currentMode) === 'undefined' || currentMode === mode;
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.renderTimestamp = function(timestamp) {
|
||||||
|
var t = new Date(timestamp);
|
||||||
|
var pad = function (i) {
|
||||||
|
return ('' + i).padStart(2, "0");
|
||||||
|
};
|
||||||
|
return pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds());
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Thread.prototype.purgeOldMessages = function() {
|
||||||
|
var now = new Date().getTime();
|
||||||
|
this.messages = this.messages.filter(function(m) {
|
||||||
|
// keep messages around for 20 minutes
|
||||||
|
return now - m.timestamp < 20 * 60 * 1000;
|
||||||
|
});
|
||||||
|
if (!this.messages.length) {
|
||||||
|
this.el.remove();
|
||||||
|
} else {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
return this.messages.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Threader = function(el){
|
||||||
|
this.threads = [];
|
||||||
|
this.tbody = $(el).find('tbody');
|
||||||
|
var me = this;
|
||||||
|
this.interval = setInterval(function(){
|
||||||
|
me.purgeOldMessages();
|
||||||
|
}, 15000);
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Threader.prototype.purgeOldMessages = function() {
|
||||||
|
this.threads = this.threads.filter(function(t) {
|
||||||
|
return t.purgeOldMessages();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Threader.prototype.findThread = function(freq, mode) {
|
||||||
|
var matching = this.threads.filter(function(thread) {
|
||||||
|
// max frequency deviation: 5 Hz. this may be a little tight.
|
||||||
|
return thread.isOpen() && thread.acceptsMode(mode) && Math.abs(thread.getAverageFrequency() - freq) <= 5;
|
||||||
|
});
|
||||||
|
matching.sort(function(a, b){
|
||||||
|
return b.getLatestTimestamp() - a.getLatestTimestamp();
|
||||||
|
});
|
||||||
|
return matching[0] || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
Js8Threader.prototype.pushMessage = function(message) {
|
||||||
|
var thread;
|
||||||
|
// only look for exising threads if the message is not a starting message
|
||||||
|
if ((message.thread_type & 1) === 0) {
|
||||||
|
thread = this.findThread(message.freq, message.mode);
|
||||||
|
}
|
||||||
|
if (!thread) {
|
||||||
|
var line = $("<tr></tr>");
|
||||||
|
this.tbody.append(line);
|
||||||
|
thread = new Js8Thread(line);
|
||||||
|
this.threads.push(thread);
|
||||||
|
}
|
||||||
|
thread.pushMessage(message);
|
||||||
|
this.tbody.scrollTop(this.tbody[0].scrollHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.js8 = function() {
|
||||||
|
if (!this.data('threader')) {
|
||||||
|
this.data('threader', new Js8Threader(this));
|
||||||
|
}
|
||||||
|
return this.data('threader');
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
function Measurement() {
|
function Measurement() {
|
||||||
|
this.reporters = [];
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,10 +22,13 @@ Measurement.prototype.getRate = function() {
|
|||||||
Measurement.prototype.reset = function() {
|
Measurement.prototype.reset = function() {
|
||||||
this.value = 0;
|
this.value = 0;
|
||||||
this.start = new Date();
|
this.start = new Date();
|
||||||
|
this.reporters.forEach(function(r){ r.reset(); });
|
||||||
};
|
};
|
||||||
|
|
||||||
Measurement.prototype.report = function(range, interval, callback) {
|
Measurement.prototype.report = function(range, interval, callback) {
|
||||||
return new Reporter(this, range, interval, callback);
|
var reporter = new Reporter(this, range, interval, callback);
|
||||||
|
this.reporters.push(reporter);
|
||||||
|
return reporter;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Reporter(measurement, range, interval, callback) {
|
function Reporter(measurement, range, interval, callback) {
|
||||||
@ -60,3 +64,7 @@ Reporter.prototype.report = function(){
|
|||||||
// we want rate per second, but our time is in milliseconds... compensate by 1000
|
// we want rate per second, but our time is in milliseconds... compensate by 1000
|
||||||
this.callback(accumulated * 1000 / elapsed);
|
this.callback(accumulated * 1000 / elapsed);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reporter.prototype.reset = function(){
|
||||||
|
this.samples = [];
|
||||||
|
};
|
55
htdocs/lib/Modes.js
Normal file
55
htdocs/lib/Modes.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
var Modes = {
|
||||||
|
modes: [],
|
||||||
|
features: {},
|
||||||
|
panels: [],
|
||||||
|
setModes:function(json){
|
||||||
|
this.modes = json.map(function(m){ return new Mode(m); });
|
||||||
|
this.updatePanels();
|
||||||
|
$('#openwebrx-dialog-bookmark').bookmarkDialog().setModes(this.modes);
|
||||||
|
},
|
||||||
|
getModes:function(){
|
||||||
|
return this.modes;
|
||||||
|
},
|
||||||
|
setFeatures:function(features){
|
||||||
|
this.features = features;
|
||||||
|
this.updatePanels();
|
||||||
|
},
|
||||||
|
findByModulation:function(modulation){
|
||||||
|
matches = this.modes.filter(function(m) { return m.modulation === modulation; });
|
||||||
|
if (matches.length) return matches[0]
|
||||||
|
},
|
||||||
|
registerModePanel: function(el) {
|
||||||
|
this.panels.push(el);
|
||||||
|
},
|
||||||
|
initComplete: function() {
|
||||||
|
return this.modes.length && Object.keys(this.features).length;
|
||||||
|
},
|
||||||
|
updatePanels: function() {
|
||||||
|
this.panels.forEach(function(p) {
|
||||||
|
p.render();
|
||||||
|
p.startDemodulator();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var Mode = function(json){
|
||||||
|
this.modulation = json.modulation;
|
||||||
|
this.name = json.name;
|
||||||
|
this.type = json.type;
|
||||||
|
this.requirements = json.requirements;
|
||||||
|
this.squelch = json.squelch;
|
||||||
|
if (json.bandpass) {
|
||||||
|
this.bandpass = json.bandpass;
|
||||||
|
}
|
||||||
|
if (this.type === 'digimode') {
|
||||||
|
this.underlying = json.underlying;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Mode.prototype.isAvailable = function(){
|
||||||
|
return this.requirements.map(function(r){
|
||||||
|
return Modes.features[r];
|
||||||
|
}).reduce(function(a, b){
|
||||||
|
return a && b;
|
||||||
|
}, true);
|
||||||
|
};
|
@ -1,10 +1,15 @@
|
|||||||
ProgressBar = function(el) {
|
ProgressBar = function(el) {
|
||||||
this.$el = $(el);
|
this.$el = $(el);
|
||||||
this.$innerText = this.$el.find('.openwebrx-progressbar-text');
|
this.$innerText = $('<span class="openwebrx-progressbar-text">' + this.getDefaultText() + '</span>');
|
||||||
this.$innerBar = this.$el.find('.openwebrx-progressbar-bar');
|
this.$innerBar = $('<div class="openwebrx-progressbar-bar"></div>');
|
||||||
|
this.$el.empty().append(this.$innerText, this.$innerBar);
|
||||||
this.$innerBar.css('width', '0%');
|
this.$innerBar.css('width', '0%');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ProgressBar.prototype.getDefaultText = function() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
ProgressBar.prototype.set = function(val, text, over) {
|
ProgressBar.prototype.set = function(val, text, over) {
|
||||||
this.setValue(val);
|
this.setValue(val);
|
||||||
this.setText(text);
|
this.setText(text);
|
||||||
@ -25,13 +30,20 @@ ProgressBar.prototype.setOver = function(over) {
|
|||||||
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
|
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
|
||||||
};
|
};
|
||||||
|
|
||||||
AudioBufferProgressBar = function(el, sampleRate) {
|
AudioBufferProgressBar = function(el) {
|
||||||
ProgressBar.call(this, el);
|
ProgressBar.call(this, el);
|
||||||
this.sampleRate = sampleRate;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AudioBufferProgressBar.prototype = new ProgressBar();
|
AudioBufferProgressBar.prototype = new ProgressBar();
|
||||||
|
|
||||||
|
AudioBufferProgressBar.prototype.getDefaultText = function() {
|
||||||
|
return 'Audio buffer [0 ms]';
|
||||||
|
};
|
||||||
|
|
||||||
|
AudioBufferProgressBar.prototype.setSampleRate = function(sampleRate) {
|
||||||
|
this.sampleRate = sampleRate;
|
||||||
|
};
|
||||||
|
|
||||||
AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) {
|
AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) {
|
||||||
var audio_buffer_value = buffersize / this.sampleRate;
|
var audio_buffer_value = buffersize / this.sampleRate;
|
||||||
var overrun = audio_buffer_value > audio_buffer_maximal_length_sec;
|
var overrun = audio_buffer_value > audio_buffer_maximal_length_sec;
|
||||||
@ -53,6 +65,10 @@ NetworkSpeedProgressBar = function(el) {
|
|||||||
|
|
||||||
NetworkSpeedProgressBar.prototype = new ProgressBar();
|
NetworkSpeedProgressBar.prototype = new ProgressBar();
|
||||||
|
|
||||||
|
NetworkSpeedProgressBar.prototype.getDefaultText = function() {
|
||||||
|
return 'Network usage [0 kbps]';
|
||||||
|
};
|
||||||
|
|
||||||
NetworkSpeedProgressBar.prototype.setSpeed = function(speed) {
|
NetworkSpeedProgressBar.prototype.setSpeed = function(speed) {
|
||||||
var speedInKilobits = speed * 8 / 1000;
|
var speedInKilobits = speed * 8 / 1000;
|
||||||
this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false);
|
this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false);
|
||||||
@ -64,18 +80,29 @@ AudioSpeedProgressBar = function(el) {
|
|||||||
|
|
||||||
AudioSpeedProgressBar.prototype = new ProgressBar();
|
AudioSpeedProgressBar.prototype = new ProgressBar();
|
||||||
|
|
||||||
|
AudioSpeedProgressBar.prototype.getDefaultText = function() {
|
||||||
|
return 'Audio stream [0 kbps]';
|
||||||
|
};
|
||||||
|
|
||||||
AudioSpeedProgressBar.prototype.setSpeed = function(speed) {
|
AudioSpeedProgressBar.prototype.setSpeed = function(speed) {
|
||||||
this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false);
|
this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false);
|
||||||
};
|
};
|
||||||
|
|
||||||
AudioOutputProgressBar = function(el, sampleRate) {
|
AudioOutputProgressBar = function(el, sampleRate) {
|
||||||
ProgressBar.call(this, el);
|
ProgressBar.call(this, el);
|
||||||
this.maxRate = sampleRate * 1.25;
|
|
||||||
this.minRate = sampleRate * .25;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AudioOutputProgressBar.prototype = new ProgressBar();
|
AudioOutputProgressBar.prototype = new ProgressBar();
|
||||||
|
|
||||||
|
AudioOutputProgressBar.prototype.getDefaultText = function() {
|
||||||
|
return 'Audio output [0 sps]';
|
||||||
|
};
|
||||||
|
|
||||||
|
AudioOutputProgressBar.prototype.setSampleRate = function(sampleRate) {
|
||||||
|
this.maxRate = sampleRate * 1.25;
|
||||||
|
this.minRate = sampleRate * .25;
|
||||||
|
};
|
||||||
|
|
||||||
AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) {
|
AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) {
|
||||||
this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate);
|
this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate);
|
||||||
};
|
};
|
||||||
@ -88,6 +115,10 @@ ClientsProgressBar = function(el) {
|
|||||||
|
|
||||||
ClientsProgressBar.prototype = new ProgressBar();
|
ClientsProgressBar.prototype = new ProgressBar();
|
||||||
|
|
||||||
|
ClientsProgressBar.prototype.getDefaultText = function() {
|
||||||
|
return 'Clients [1]';
|
||||||
|
};
|
||||||
|
|
||||||
ClientsProgressBar.prototype.setClients = function(clients) {
|
ClientsProgressBar.prototype.setClients = function(clients) {
|
||||||
this.clients = clients;
|
this.clients = clients;
|
||||||
this.render();
|
this.render();
|
||||||
@ -108,6 +139,27 @@ CpuProgressBar = function(el) {
|
|||||||
|
|
||||||
CpuProgressBar.prototype = new ProgressBar();
|
CpuProgressBar.prototype = new ProgressBar();
|
||||||
|
|
||||||
|
CpuProgressBar.prototype.getDefaultText = function() {
|
||||||
|
return 'Server CPU [0%]';
|
||||||
|
};
|
||||||
|
|
||||||
CpuProgressBar.prototype.setUsage = function(usage) {
|
CpuProgressBar.prototype.setUsage = function(usage) {
|
||||||
this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85);
|
this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ProgressBar.types = {
|
||||||
|
cpu: CpuProgressBar,
|
||||||
|
audiobuffer: AudioBufferProgressBar,
|
||||||
|
audiospeed: AudioSpeedProgressBar,
|
||||||
|
audiooutput: AudioOutputProgressBar,
|
||||||
|
clients: ClientsProgressBar,
|
||||||
|
networkspeed: NetworkSpeedProgressBar
|
||||||
|
}
|
||||||
|
|
||||||
|
$.fn.progressbar = function() {
|
||||||
|
if (!this.data('progressbar')) {
|
||||||
|
var constructor = ProgressBar.types[this.data('type')] || ProgressBar;
|
||||||
|
this.data('progressbar', new constructor(this));
|
||||||
|
}
|
||||||
|
return this.data('progressbar');
|
||||||
|
};
|
||||||
|
138
htdocs/lib/settings/Input.js
Normal file
138
htdocs/lib/settings/Input.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
function Input(name, value, options) {
|
||||||
|
this.name = name;
|
||||||
|
this.value = value;
|
||||||
|
this.options = options;
|
||||||
|
this.label = options && options.label || name;
|
||||||
|
};
|
||||||
|
|
||||||
|
Input.prototype.getClasses = function() {
|
||||||
|
return ['form-control', 'form-control-sm'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Input.prototype.bootstrapify = function(input) {
|
||||||
|
this.getClasses().forEach(input.addClass.bind(input));
|
||||||
|
return [
|
||||||
|
'<div class="form-group row">',
|
||||||
|
'<label class="col-form-label col-form-label-sm col-3" for="' + this.name + '">' + this.label + '</label>',
|
||||||
|
'<div class="col-9">',
|
||||||
|
$.map(input, function(el) {
|
||||||
|
return el.outerHTML;
|
||||||
|
}).join(''),
|
||||||
|
'</div>',
|
||||||
|
'</div>'
|
||||||
|
].join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
function TextInput() {
|
||||||
|
Input.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
TextInput.prototype = new Input();
|
||||||
|
|
||||||
|
TextInput.prototype.render = function() {
|
||||||
|
return this.bootstrapify($('<input type="text" name="' + this.name + '" value="' + this.value + '">'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumberInput() {
|
||||||
|
Input.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
NumberInput.prototype = new Input();
|
||||||
|
|
||||||
|
NumberInput.prototype.render = function() {
|
||||||
|
return this.bootstrapify($('<input type="number" name="' + this.name + '" value="' + this.value + '">'));
|
||||||
|
};
|
||||||
|
|
||||||
|
function SoapyGainInput() {
|
||||||
|
Input.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
SoapyGainInput.prototype = new Input();
|
||||||
|
|
||||||
|
SoapyGainInput.prototype.getClasses = function() {
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
SoapyGainInput.prototype.render = function(){
|
||||||
|
var markup = $(
|
||||||
|
'<div class="row form-group">' +
|
||||||
|
'<div class="col-4">Gain mode</div>' +
|
||||||
|
'<div class="col-8">' +
|
||||||
|
'<select class="form-control form-control-sm">' +
|
||||||
|
'<option value="auto">automatic gain</option>' +
|
||||||
|
'<option value="single">single gain value</option>' +
|
||||||
|
'<option value="separate">separate gain values</option>' +
|
||||||
|
'</select>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="row option form-group gain-mode-single">' +
|
||||||
|
'<div class="col-4">Gain</div>' +
|
||||||
|
'<div class="col-8">' +
|
||||||
|
'<input class="form-control form-control-sm" type="number">' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
this.options.gains.map(function(g){
|
||||||
|
return '<div class="row option form-group gain-mode-separate">' +
|
||||||
|
'<div class="col-4">' + g + '</div>' +
|
||||||
|
'<div class="col-8">' +
|
||||||
|
'<input class="form-control form-control-sm" data-gain="' + g + '" type="number">' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}).join('')
|
||||||
|
);
|
||||||
|
var el = $(this.bootstrapify(markup))
|
||||||
|
var setMode = function(mode){
|
||||||
|
el.find('select').val(mode);
|
||||||
|
el.find('.option').hide();
|
||||||
|
el.find('.gain-mode-' + mode).show();
|
||||||
|
};
|
||||||
|
el.on('change', 'select', function(){
|
||||||
|
var mode = $(this).val();
|
||||||
|
setMode(mode);
|
||||||
|
});
|
||||||
|
if (typeof(this.value) === 'number') {
|
||||||
|
setMode('single');
|
||||||
|
el.find('.gain-mode-single input').val(this.value);
|
||||||
|
} else if (typeof(this.value) === 'string') {
|
||||||
|
if (this.value === 'auto') {
|
||||||
|
setMode('auto');
|
||||||
|
} else {
|
||||||
|
setMode('separate');
|
||||||
|
values = $.extend.apply($, this.value.split(',').map(function(seg){
|
||||||
|
var split = seg.split('=');
|
||||||
|
if (split.length < 2) return;
|
||||||
|
var res = {};
|
||||||
|
res[split[0]] = parseInt(split[1]);
|
||||||
|
return res;
|
||||||
|
}));
|
||||||
|
el.find('.gain-mode-separate input').each(function(){
|
||||||
|
var $input = $(this);
|
||||||
|
var g = $input.data('gain');
|
||||||
|
$input.val(g in values ? values[g] : 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setMode('auto');
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ProfileInput() {
|
||||||
|
Input.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfileInput.prototype = new Input();
|
||||||
|
|
||||||
|
ProfileInput.prototype.render = function() {
|
||||||
|
return $('<div><h3>Profiles</h3></div>');
|
||||||
|
};
|
||||||
|
|
||||||
|
function SchedulerInput() {
|
||||||
|
Input.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
SchedulerInput.prototype = new Input();
|
||||||
|
|
||||||
|
SchedulerInput.prototype.render = function() {
|
||||||
|
return $('<div><h3>Scheduler</h3></div>');
|
||||||
|
};
|
252
htdocs/lib/settings/SdrDevice.js
Normal file
252
htdocs/lib/settings/SdrDevice.js
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
function SdrDevice(el, data) {
|
||||||
|
this.el = el;
|
||||||
|
this.data = data;
|
||||||
|
this.inputs = {};
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
el.on('click', '.fieldselector .btn', function() {
|
||||||
|
var key = el.find('.fieldselector select').val();
|
||||||
|
self.data[key] = self.getInitialValue(key);
|
||||||
|
self.render();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.create = function(el) {
|
||||||
|
var data = JSON.parse(decodeURIComponent(el.data('config')));
|
||||||
|
var type = data.type;
|
||||||
|
var constructor = SdrDevice.types[type] || SdrDevice;
|
||||||
|
return new constructor(el, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.getData = function() {
|
||||||
|
return $.extend(new Object(), this.getDefaults(), this.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.getDefaults = function() {
|
||||||
|
var defaults = {}
|
||||||
|
$.each(this.getMappings(), function(k, v) {
|
||||||
|
if (!v.includeInDefault) return;
|
||||||
|
defaults[k] = 'initialValue' in v ? v['initialValue'] : false;
|
||||||
|
});
|
||||||
|
return defaults;
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.getMappings = function() {
|
||||||
|
return {
|
||||||
|
"name": {
|
||||||
|
constructor: TextInput,
|
||||||
|
inputOptions: {
|
||||||
|
label: "Name"
|
||||||
|
},
|
||||||
|
initialValue: "",
|
||||||
|
includeInDefault: true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
constructor: TextInput,
|
||||||
|
inputOptions: {
|
||||||
|
label: "Type"
|
||||||
|
},
|
||||||
|
initialValue: '',
|
||||||
|
includeInDefault: true
|
||||||
|
},
|
||||||
|
"ppm": {
|
||||||
|
constructor: NumberInput,
|
||||||
|
inputOptions: {
|
||||||
|
label: "PPM"
|
||||||
|
},
|
||||||
|
initialValue: 0
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
constructor: ProfileInput,
|
||||||
|
inputOptions: {
|
||||||
|
label: "Profiles"
|
||||||
|
},
|
||||||
|
initialValue: [],
|
||||||
|
includeInDefault: true,
|
||||||
|
position: 100
|
||||||
|
},
|
||||||
|
"scheduler": {
|
||||||
|
constructor: SchedulerInput,
|
||||||
|
inputOptions: {
|
||||||
|
label: "Scheduler",
|
||||||
|
},
|
||||||
|
initialValue: {},
|
||||||
|
position: 101
|
||||||
|
},
|
||||||
|
"rf_gain": {
|
||||||
|
constructor: TextInput,
|
||||||
|
inputOptions: {
|
||||||
|
label: "Gain",
|
||||||
|
},
|
||||||
|
initialValue: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.getMapping = function(key) {
|
||||||
|
var mappings = this.getMappings();
|
||||||
|
return mappings[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.getInputClass = function(key) {
|
||||||
|
var mapping = this.getMapping(key);
|
||||||
|
return mapping && mapping.constructor || TextInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.getInitialValue = function(key) {
|
||||||
|
var mapping = this.getMapping(key);
|
||||||
|
return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.getPosition = function(key) {
|
||||||
|
var mapping = this.getMapping(key);
|
||||||
|
return mapping && mapping.position || 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.getInputOptions = function(key) {
|
||||||
|
var mapping = this.getMapping(key);
|
||||||
|
return mapping && mapping.inputOptions || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.getLabel = function(key) {
|
||||||
|
var options = this.getInputOptions(key);
|
||||||
|
return options && options.label || key;
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.render = function() {
|
||||||
|
var self = this;
|
||||||
|
self.el.empty();
|
||||||
|
var data = this.getData();
|
||||||
|
Object.keys(data).sort(function(a, b){
|
||||||
|
return self.getPosition(a) - self.getPosition(b);
|
||||||
|
}).forEach(function(key){
|
||||||
|
var value = data[key];
|
||||||
|
var inputClass = self.getInputClass(key);
|
||||||
|
var input = new inputClass(key, value, self.getInputOptions(key));
|
||||||
|
self.inputs[key] = input;
|
||||||
|
self.el.append(input.render());
|
||||||
|
});
|
||||||
|
self.el.append(this.renderFieldSelector());
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.prototype.renderFieldSelector = function() {
|
||||||
|
var self = this;
|
||||||
|
return '<div class="fieldselector">' +
|
||||||
|
'<h3>Add new configuration options<h3>' +
|
||||||
|
'<div class="form-group row">' +
|
||||||
|
'<div class="col-3"><select class="form-control form-control-sm">' +
|
||||||
|
Object.keys(self.getMappings()).filter(function(m){
|
||||||
|
return !(m in self.data);
|
||||||
|
}).map(function(m) {
|
||||||
|
return '<option value="' + m + '">' + self.getLabel(m) + '</option>';
|
||||||
|
}).join('') +
|
||||||
|
'</select></div>' +
|
||||||
|
'<div class="col-2">' +
|
||||||
|
'<div class="btn btn-primary">Add to config</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
};
|
||||||
|
|
||||||
|
RtlSdrDevice = function() {
|
||||||
|
SdrDevice.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
RtlSdrDevice.prototype = Object.create(SdrDevice.prototype);
|
||||||
|
RtlSdrDevice.prototype.constructor = RtlSdrDevice;
|
||||||
|
|
||||||
|
RtlSdrDevice.prototype.getMappings = function() {
|
||||||
|
var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
|
||||||
|
return $.extend(new Object(), mappings, {
|
||||||
|
"device": {
|
||||||
|
constructor: TextInput,
|
||||||
|
inputOptions:{
|
||||||
|
label: "Serial number"
|
||||||
|
},
|
||||||
|
initialValue: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
SoapySdrDevice = function() {
|
||||||
|
SdrDevice.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
SoapySdrDevice.prototype = Object.create(SdrDevice.prototype);
|
||||||
|
SoapySdrDevice.prototype.constructor = SoapySdrDevice;
|
||||||
|
|
||||||
|
SoapySdrDevice.prototype.getMappings = function() {
|
||||||
|
var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
|
||||||
|
return $.extend(new Object(), mappings, {
|
||||||
|
"device": {
|
||||||
|
constructor: TextInput,
|
||||||
|
inputOptions:{
|
||||||
|
label: "Soapy device selector"
|
||||||
|
},
|
||||||
|
initialValue: ""
|
||||||
|
},
|
||||||
|
"rf_gain": {
|
||||||
|
constructor: SoapyGainInput,
|
||||||
|
initialValue: 0,
|
||||||
|
inputOptions: {
|
||||||
|
label: "Gain",
|
||||||
|
gains: this.getGains()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
SoapySdrDevice.prototype.getGains = function() {
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrplaySdrDevice = function() {
|
||||||
|
SoapySdrDevice.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
|
||||||
|
SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice;
|
||||||
|
|
||||||
|
SdrplaySdrDevice.prototype.getGains = function() {
|
||||||
|
return ['RFGR', 'IFGR'];
|
||||||
|
};
|
||||||
|
|
||||||
|
AirspyHfSdrDevice = function() {
|
||||||
|
SoapySdrDevice.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
AirspyHfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
|
||||||
|
AirspyHfSdrDevice.prototype.constructor = AirspyHfSdrDevice;
|
||||||
|
|
||||||
|
AirspyHfSdrDevice.prototype.getGains = function() {
|
||||||
|
return ['RF', 'VGA'];
|
||||||
|
};
|
||||||
|
|
||||||
|
HackRfSdrDevice = function() {
|
||||||
|
SoapySdrDevice.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
HackRfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
|
||||||
|
HackRfSdrDevice.prototype.constructor = HackRfSdrDevice;
|
||||||
|
|
||||||
|
HackRfSdrDevice.prototype.getGains = function() {
|
||||||
|
return ['LNA', 'VGA', 'AMP'];
|
||||||
|
};
|
||||||
|
|
||||||
|
SdrDevice.types = {
|
||||||
|
'rtl_sdr': RtlSdrDevice,
|
||||||
|
'sdrplay': SdrplaySdrDevice,
|
||||||
|
'airspyhf': AirspyHfSdrDevice,
|
||||||
|
'hackrf': HackRfSdrDevice
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.sdrdevice = function() {
|
||||||
|
return this.map(function(){
|
||||||
|
var el = $(this);
|
||||||
|
if (!el.data('sdrdevice')) {
|
||||||
|
el.data('sdrdevice', SdrDevice.create(el));
|
||||||
|
}
|
||||||
|
return el.data('sdrdevice');
|
||||||
|
});
|
||||||
|
};
|
27
htdocs/login.html
Normal file
27
htdocs/login.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OpenWebRX Login</title>
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="static/css/login.css" />
|
||||||
|
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
||||||
|
<script src="static/lib/Header.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${header}
|
||||||
|
<div class="login">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="user">Username</label>
|
||||||
|
<input type="text" class="form-control" id="user" name="user" autofocus="autofocus" placeholder="Username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-secondary btn-login">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
@ -3,9 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>OpenWebRX Map</title>
|
<title>OpenWebRX Map</title>
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||||
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
<script src="compiled/map.js"></script>
|
||||||
<script src="static/lib/chroma.min.js"></script>
|
|
||||||
<script src="static/map.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||||
<link rel="stylesheet" type="text/css" href="static/css/map.css" />
|
<link rel="stylesheet" type="text/css" href="static/css/map.css" />
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
@ -135,7 +135,11 @@
|
|||||||
if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
|
if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
|
||||||
map.panTo(pos);
|
map.panTo(pos);
|
||||||
showMarkerInfoWindow(update.callsign, pos);
|
showMarkerInfoWindow(update.callsign, pos);
|
||||||
delete(expectedCallsign);
|
expectedCallsign = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign.trim()) {
|
||||||
|
showMarkerInfoWindow(infowindow.callsign, pos);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'locator':
|
case 'locator':
|
||||||
@ -176,7 +180,11 @@
|
|||||||
if (expectedLocator && expectedLocator == update.location.locator) {
|
if (expectedLocator && expectedLocator == update.location.locator) {
|
||||||
map.panTo(center);
|
map.panTo(center);
|
||||||
showLocatorInfoWindow(expectedLocator, center);
|
showLocatorInfoWindow(expectedLocator, center);
|
||||||
delete(expectedLocator);
|
expectedLocator = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infowindow && infowindow.locator && infowindow.locator == update.location.locator) {
|
||||||
|
showLocatorInfoWindow(infowindow.locator, center);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -215,13 +223,26 @@
|
|||||||
case "config":
|
case "config":
|
||||||
var config = json.value;
|
var config = json.value;
|
||||||
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
|
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
|
||||||
|
var mapTypeId = config.google_maps_api_key ? 'roadmap' : 'OSM';
|
||||||
|
|
||||||
map = new google.maps.Map($('.openwebrx-map')[0], {
|
map = new google.maps.Map($('.openwebrx-map')[0], {
|
||||||
center: {
|
center: {
|
||||||
lat: config.receiver_gps[0],
|
lat: config.receiver_gps.lat,
|
||||||
lng: config.receiver_gps[1]
|
lng: config.receiver_gps.lon
|
||||||
},
|
},
|
||||||
zoom: 5
|
zoom: 5,
|
||||||
|
mapTypeId: mapTypeId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
map.mapTypes.set("OSM", new google.maps.ImageMapType({
|
||||||
|
getTileUrl: function(coord, zoom) {
|
||||||
|
return "https://maps.wikimedia.org/osm-intl/" + zoom + "/" + coord.x + "/" + coord.y + ".png";
|
||||||
|
},
|
||||||
|
tileSize: new google.maps.Size(256, 256),
|
||||||
|
name: "OpenStreetMap",
|
||||||
|
maxZoom: 18
|
||||||
|
}));
|
||||||
|
|
||||||
$.getScript("static/lib/nite-overlay.js").done(function(){
|
$.getScript("static/lib/nite-overlay.js").done(function(){
|
||||||
nite.init(map);
|
nite.init(map);
|
||||||
setInterval(function() { nite.refresh() }, 10000); // every 10s
|
setInterval(function() { nite.refresh() }, 10000); // every 10s
|
||||||
@ -237,6 +258,11 @@
|
|||||||
case "update":
|
case "update":
|
||||||
processUpdates(json.value);
|
processUpdates(json.value);
|
||||||
break;
|
break;
|
||||||
|
case 'receiver_details':
|
||||||
|
$('#webrx-top-container').header().setDetails(json['value']);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn('received message of unknown type: ' + json['type']);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// don't lose exception
|
// don't lose exception
|
||||||
@ -269,9 +295,21 @@
|
|||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
|
var getInfoWindow = function() {
|
||||||
|
if (!infowindow) {
|
||||||
|
infowindow = new google.maps.InfoWindow();
|
||||||
|
google.maps.event.addListener(infowindow, 'closeclick', function() {
|
||||||
|
delete infowindow.locator;
|
||||||
|
delete infowindow.callsign;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return infowindow;
|
||||||
|
}
|
||||||
|
|
||||||
var infowindow;
|
var infowindow;
|
||||||
var showLocatorInfoWindow = function(locator, pos) {
|
var showLocatorInfoWindow = function(locator, pos) {
|
||||||
if (!infowindow) infowindow = new google.maps.InfoWindow();
|
var infowindow = getInfoWindow();
|
||||||
|
infowindow.locator = locator;
|
||||||
var inLocator = $.map(rectangles, function(r, callsign) {
|
var inLocator = $.map(rectangles, function(r, callsign) {
|
||||||
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
|
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
|
||||||
}).filter(function(d) {
|
}).filter(function(d) {
|
||||||
@ -297,7 +335,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
var showMarkerInfoWindow = function(callsign, pos) {
|
var showMarkerInfoWindow = function(callsign, pos) {
|
||||||
if (!infowindow) infowindow = new google.maps.InfoWindow();
|
var infowindow = getInfoWindow();
|
||||||
|
infowindow.callsign = callsign;
|
||||||
var marker = markers[callsign];
|
var marker = markers[callsign];
|
||||||
var timestring = moment(marker.lastseen).fromNow();
|
var timestring = moment(marker.lastseen).fromNow();
|
||||||
var commentString = "";
|
var commentString = "";
|
||||||
|
File diff suppressed because it is too large
Load Diff
21
htdocs/sdrsettings.html
Normal file
21
htdocs/sdrsettings.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OpenWebRX Settings</title>
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
|
||||||
|
<script src="compiled/settings.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${header}
|
||||||
|
<div class="container">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>SDR device settings</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
${devices}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
27
htdocs/settings.html
Normal file
27
htdocs/settings.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OpenWebRX Settings</title>
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
|
||||||
|
<script src="compiled/settings.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${header}
|
||||||
|
<div class="container">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>Settings</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<a href="generalsettings">General settings</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<a href="sdrsettings">SDR device settings</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<a href="features">Feature report</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
25
htdocs/settings.js
Normal file
25
htdocs/settings.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
$(function(){
|
||||||
|
$(".map-input").each(function(el) {
|
||||||
|
var $el = $(this);
|
||||||
|
var field_id = $el.attr("for");
|
||||||
|
var $lat = $('#' + field_id + '-lat');
|
||||||
|
var $lon = $('#' + field_id + '-lon');
|
||||||
|
$.getScript("https://maps.googleapis.com/maps/api/js?key=" + $el.data("key")).done(function(){
|
||||||
|
$el.css("height", "200px");
|
||||||
|
var lp = new locationPicker($el.get(0), {
|
||||||
|
lat: parseFloat($lat.val()),
|
||||||
|
lng: parseFloat($lon.val())
|
||||||
|
}, {
|
||||||
|
zoom: 7
|
||||||
|
});
|
||||||
|
|
||||||
|
google.maps.event.addListener(lp.map, 'idle', function(event){
|
||||||
|
var pos = lp.getMarkerPosition();
|
||||||
|
$lat.val(pos.lat);
|
||||||
|
$lon.val(pos.lng);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".sdrdevice").sdrdevice();
|
||||||
|
});
|
15
manifest.sh
Executable file
15
manifest.sh
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euxo pipefail
|
||||||
|
. docker/env
|
||||||
|
|
||||||
|
for image in ${IMAGES}; do
|
||||||
|
# there's no docker manifest rm command, and the create --amend does not work, so we have to clean up manually
|
||||||
|
rm -rf "${HOME}/.docker/manifests/docker.io_jketterl_${image}-${TAG}"
|
||||||
|
IMAGE_LIST=""
|
||||||
|
for a in $ALL_ARCHS; do
|
||||||
|
IMAGE_LIST="$IMAGE_LIST jketterl/$image:$TAG-$a"
|
||||||
|
done
|
||||||
|
docker manifest create jketterl/$image:$TAG $IMAGE_LIST
|
||||||
|
docker manifest push --purge jketterl/$image:$TAG
|
||||||
|
docker pull jketterl/$image:$TAG
|
||||||
|
done
|
@ -1,17 +1,18 @@
|
|||||||
from http.server import HTTPServer
|
|
||||||
from owrx.http import RequestHandler
|
|
||||||
from owrx.config import PropertyManager
|
|
||||||
from owrx.feature import FeatureDetector
|
|
||||||
from owrx.sdr import SdrService
|
|
||||||
from socketserver import ThreadingMixIn
|
|
||||||
from owrx.sdrhu import SdrHuUpdater
|
|
||||||
from owrx.service import Services
|
|
||||||
from owrx.websocket import WebSocketConnection
|
|
||||||
from owrx.pskreporter import PskReporter
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
from http.server import HTTPServer
|
||||||
|
from owrx.http import RequestHandler
|
||||||
|
from owrx.config import Config
|
||||||
|
from owrx.feature import FeatureDetector
|
||||||
|
from owrx.sdr import SdrService
|
||||||
|
from socketserver import ThreadingMixIn
|
||||||
|
from owrx.service import Services
|
||||||
|
from owrx.websocket import WebSocketConnection
|
||||||
|
from owrx.pskreporter import PskReporter
|
||||||
|
from owrx.version import openwebrx_version
|
||||||
|
|
||||||
|
|
||||||
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
|
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
|
||||||
@ -26,32 +27,41 @@ OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE fil
|
|||||||
_________________________________________________________________________________________________
|
_________________________________________________________________________________________________
|
||||||
|
|
||||||
Author contact info: Jakob Ketterl, DD5JFK <dd5jfk@darc.de>
|
Author contact info: Jakob Ketterl, DD5JFK <dd5jfk@darc.de>
|
||||||
|
Documentation: https://github.com/jketterl/openwebrx/wiki
|
||||||
|
Support and info: https://groups.io/g/openwebrx
|
||||||
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
pm = PropertyManager.getSharedInstance().loadConfig()
|
logger.info("OpenWebRX version {0} starting up...".format(openwebrx_version))
|
||||||
|
|
||||||
|
pm = Config.get()
|
||||||
|
|
||||||
|
configErrors = Config.validateConfig()
|
||||||
|
if configErrors:
|
||||||
|
logger.error(
|
||||||
|
"your configuration contains errors. please address the following errors:"
|
||||||
|
)
|
||||||
|
for e in configErrors:
|
||||||
|
logger.error(e)
|
||||||
|
return
|
||||||
|
|
||||||
featureDetector = FeatureDetector()
|
featureDetector = FeatureDetector()
|
||||||
if not featureDetector.is_available("core"):
|
if not featureDetector.is_available("core"):
|
||||||
print(
|
logger.error(
|
||||||
"you are missing required dependencies to run openwebrx. "
|
"you are missing required dependencies to run openwebrx. "
|
||||||
"please check that the following core requirements are installed:"
|
"please check that the following core requirements are installed:"
|
||||||
)
|
)
|
||||||
print(", ".join(featureDetector.get_requirements("core")))
|
logger.error(", ".join(featureDetector.get_requirements("core")))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get error messages about unknown / unavailable features as soon as possible
|
# Get error messages about unknown / unavailable features as soon as possible
|
||||||
SdrService.loadProps()
|
SdrService.loadProps()
|
||||||
|
|
||||||
if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
|
|
||||||
updater = SdrHuUpdater()
|
|
||||||
updater.start()
|
|
||||||
|
|
||||||
Services.start()
|
Services.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler)
|
server = ThreadedHttpServer(("0.0.0.0", pm["web_port"]), RequestHandler)
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
WebSocketConnection.closeAll()
|
WebSocketConnection.closeAll()
|
||||||
|
244
owrx/audio.py
Normal file
244
owrx/audio.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
from abc import ABC, ABCMeta, abstractmethod
|
||||||
|
from owrx.config import Config
|
||||||
|
from owrx.metrics import Metrics, CounterMetric, DirectMetric
|
||||||
|
import threading
|
||||||
|
import wave
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from multiprocessing.connection import Pipe, wait
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from queue import Queue, Full
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
class QueueJob(object):
|
||||||
|
def __init__(self, decoder, file, freq):
|
||||||
|
self.decoder = decoder
|
||||||
|
self.file = file
|
||||||
|
self.freq = freq
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.decoder.decode(self)
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
try:
|
||||||
|
os.unlink(self.file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QueueWorker(threading.Thread):
|
||||||
|
def __init__(self, queue):
|
||||||
|
self.queue = queue
|
||||||
|
self.doRun = True
|
||||||
|
super().__init__(daemon=True)
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
while self.doRun:
|
||||||
|
job = self.queue.get()
|
||||||
|
try:
|
||||||
|
job.run()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("failed to decode job")
|
||||||
|
self.queue.onError()
|
||||||
|
finally:
|
||||||
|
job.unlink()
|
||||||
|
|
||||||
|
self.queue.task_done()
|
||||||
|
|
||||||
|
|
||||||
|
class DecoderQueue(Queue):
|
||||||
|
sharedInstance = None
|
||||||
|
creationLock = threading.Lock()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getSharedInstance():
|
||||||
|
with DecoderQueue.creationLock:
|
||||||
|
if DecoderQueue.sharedInstance is None:
|
||||||
|
pm = Config.get()
|
||||||
|
DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"])
|
||||||
|
return DecoderQueue.sharedInstance
|
||||||
|
|
||||||
|
def __init__(self, maxsize, workers):
|
||||||
|
super().__init__(maxsize)
|
||||||
|
metrics = Metrics.getSharedInstance()
|
||||||
|
metrics.addMetric("decoding.queue.length", DirectMetric(self.qsize))
|
||||||
|
self.inCounter = CounterMetric()
|
||||||
|
metrics.addMetric("decoding.queue.in", self.inCounter)
|
||||||
|
self.outCounter = CounterMetric()
|
||||||
|
metrics.addMetric("decoding.queue.out", self.outCounter)
|
||||||
|
self.overflowCounter = CounterMetric()
|
||||||
|
metrics.addMetric("decoding.queue.overflow", self.overflowCounter)
|
||||||
|
self.errorCounter = CounterMetric()
|
||||||
|
metrics.addMetric("decoding.queue.error", self.errorCounter)
|
||||||
|
self.workers = [self.newWorker() for _ in range(0, workers)]
|
||||||
|
|
||||||
|
def put(self, item, **kwars):
|
||||||
|
self.inCounter.inc()
|
||||||
|
try:
|
||||||
|
super(DecoderQueue, self).put(item, block=False)
|
||||||
|
except Full:
|
||||||
|
self.overflowCounter.inc()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get(self, **kwargs):
|
||||||
|
# super.get() is blocking, so it would mess up the stats to inc() first
|
||||||
|
out = super(DecoderQueue, self).get(**kwargs)
|
||||||
|
self.outCounter.inc()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def newWorker(self):
|
||||||
|
worker = QueueWorker(self)
|
||||||
|
worker.start()
|
||||||
|
return worker
|
||||||
|
|
||||||
|
def onError(self):
|
||||||
|
self.errorCounter.inc()
|
||||||
|
|
||||||
|
|
||||||
|
class AudioChopperProfile(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def getInterval(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getFileTimestampFormat(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def decoder_commandline(self, file):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AudioWriter(object):
|
||||||
|
def __init__(self, dsp, source, profile: AudioChopperProfile):
|
||||||
|
self.dsp = dsp
|
||||||
|
self.source = source
|
||||||
|
self.profile = profile
|
||||||
|
self.tmp_dir = Config.get()["temporary_directory"]
|
||||||
|
self.wavefile = None
|
||||||
|
self.wavefilename = None
|
||||||
|
self.switchingLock = threading.Lock()
|
||||||
|
self.timer = None
|
||||||
|
(self.outputReader, self.outputWriter) = Pipe()
|
||||||
|
|
||||||
|
def getWaveFile(self):
|
||||||
|
filename = "{tmp_dir}/openwebrx-audiochopper-{id}-{timestamp}.wav".format(
|
||||||
|
tmp_dir=self.tmp_dir,
|
||||||
|
id=id(self),
|
||||||
|
timestamp=datetime.utcnow().strftime(self.profile.getFileTimestampFormat()),
|
||||||
|
)
|
||||||
|
wavefile = wave.open(filename, "wb")
|
||||||
|
wavefile.setnchannels(1)
|
||||||
|
wavefile.setsampwidth(2)
|
||||||
|
wavefile.setframerate(12000)
|
||||||
|
return filename, wavefile
|
||||||
|
|
||||||
|
def getNextDecodingTime(self):
|
||||||
|
t = datetime.utcnow()
|
||||||
|
zeroed = t.replace(minute=0, second=0, microsecond=0)
|
||||||
|
delta = t - zeroed
|
||||||
|
interval = self.profile.getInterval()
|
||||||
|
seconds = (int(delta.total_seconds() / interval) + 1) * interval
|
||||||
|
t = zeroed + timedelta(seconds=seconds)
|
||||||
|
logger.debug("scheduling: {0}".format(t))
|
||||||
|
return t
|
||||||
|
|
||||||
|
def cancelTimer(self):
|
||||||
|
if self.timer:
|
||||||
|
self.timer.cancel()
|
||||||
|
self.timer = None
|
||||||
|
|
||||||
|
def _scheduleNextSwitch(self):
|
||||||
|
self.cancelTimer()
|
||||||
|
delta = self.getNextDecodingTime() - datetime.utcnow()
|
||||||
|
self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
|
||||||
|
self.timer.start()
|
||||||
|
|
||||||
|
def switchFiles(self):
|
||||||
|
self.switchingLock.acquire()
|
||||||
|
file = self.wavefile
|
||||||
|
filename = self.wavefilename
|
||||||
|
(self.wavefilename, self.wavefile) = self.getWaveFile()
|
||||||
|
self.switchingLock.release()
|
||||||
|
|
||||||
|
file.close()
|
||||||
|
job = QueueJob(self, filename, self.dsp.get_operating_freq())
|
||||||
|
try:
|
||||||
|
DecoderQueue.getSharedInstance().put(job)
|
||||||
|
except Full:
|
||||||
|
logger.warning("decoding queue overflow; dropping one file")
|
||||||
|
job.unlink()
|
||||||
|
self._scheduleNextSwitch()
|
||||||
|
|
||||||
|
def decode(self, job: QueueJob):
|
||||||
|
logger.debug("processing file %s", job.file)
|
||||||
|
decoder = subprocess.Popen(
|
||||||
|
["nice", "-n", "10"] + self.profile.decoder_commandline(job.file),
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
cwd=self.tmp_dir,
|
||||||
|
close_fds=True,
|
||||||
|
)
|
||||||
|
for line in decoder.stdout:
|
||||||
|
self.outputWriter.send((job.freq, line))
|
||||||
|
try:
|
||||||
|
rc = decoder.wait(timeout=10)
|
||||||
|
if rc != 0:
|
||||||
|
logger.warning("decoder return code: %i", rc)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
|
||||||
|
decoder.kill()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
(self.wavefilename, self.wavefile) = self.getWaveFile()
|
||||||
|
self._scheduleNextSwitch()
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self.switchingLock.acquire()
|
||||||
|
self.wavefile.writeframes(data)
|
||||||
|
self.switchingLock.release()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.outputReader.close()
|
||||||
|
self.outputWriter.close()
|
||||||
|
self.cancelTimer()
|
||||||
|
try:
|
||||||
|
os.unlink(self.wavefilename)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("error removing undecoded file")
|
||||||
|
|
||||||
|
|
||||||
|
class AudioChopper(threading.Thread, metaclass=ABCMeta):
|
||||||
|
def __init__(self, dsp, source, *profiles: AudioChopperProfile):
|
||||||
|
self.source = source
|
||||||
|
self.writers = [AudioWriter(dsp, source, p) for p in profiles]
|
||||||
|
self.doRun = True
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
logger.debug("Audio chopper starting up")
|
||||||
|
for w in self.writers:
|
||||||
|
w.start()
|
||||||
|
while self.doRun:
|
||||||
|
data = self.source.read(256)
|
||||||
|
if data is None or (isinstance(data, bytes) and len(data) == 0):
|
||||||
|
self.doRun = False
|
||||||
|
else:
|
||||||
|
for w in self.writers:
|
||||||
|
w.write(data)
|
||||||
|
|
||||||
|
logger.debug("Audio chopper shutting down")
|
||||||
|
for w in self.writers:
|
||||||
|
w.stop()
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
try:
|
||||||
|
readers = wait([w.outputReader for w in self.writers])
|
||||||
|
return [r.recv() for r in readers]
|
||||||
|
except EOFError:
|
||||||
|
return None
|
@ -1,4 +1,4 @@
|
|||||||
from owrx.config import PropertyManager
|
from owrx.config import Config
|
||||||
from owrx.metrics import Metrics, DirectMetric
|
from owrx.metrics import Metrics, DirectMetric
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ class ClientRegistry(object):
|
|||||||
c.write_clients(n)
|
c.write_clients(n)
|
||||||
|
|
||||||
def addClient(self, client):
|
def addClient(self, client):
|
||||||
pm = PropertyManager.getSharedInstance()
|
pm = Config.get()
|
||||||
if len(self.clients) >= pm["max_clients"]:
|
if len(self.clients) >= pm["max_clients"]:
|
||||||
raise TooManyClientsException()
|
raise TooManyClientsException()
|
||||||
self.clients.append(client)
|
self.clients.append(client)
|
||||||
|
@ -33,6 +33,9 @@ class CommandMapper(object):
|
|||||||
self.static = static
|
self.static = static
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return self.mappings.keys()
|
||||||
|
|
||||||
|
|
||||||
class CommandMapping(ABC):
|
class CommandMapping(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
241
owrx/config.py
241
owrx/config.py
@ -1,149 +1,134 @@
|
|||||||
|
from owrx.property import PropertyManager, PropertyLayer
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Subscription(object):
|
|
||||||
def __init__(self, subscriptee, subscriber):
|
|
||||||
self.subscriptee = subscriptee
|
|
||||||
self.subscriber = subscriber
|
|
||||||
|
|
||||||
def call(self, *args, **kwargs):
|
|
||||||
self.subscriber(*args, **kwargs)
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
self.subscriptee.unwire(self)
|
|
||||||
|
|
||||||
|
|
||||||
class Property(object):
|
|
||||||
def __init__(self, value=None):
|
|
||||||
self.value = value
|
|
||||||
self.subscribers = []
|
|
||||||
|
|
||||||
def getValue(self):
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
def setValue(self, value):
|
|
||||||
if self.value == value:
|
|
||||||
return self
|
|
||||||
self.value = value
|
|
||||||
for c in self.subscribers:
|
|
||||||
try:
|
|
||||||
c.call(self.value)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(e)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def wire(self, callback):
|
|
||||||
sub = Subscription(self, callback)
|
|
||||||
self.subscribers.append(sub)
|
|
||||||
if not self.value is None:
|
|
||||||
sub.call(self.value)
|
|
||||||
return sub
|
|
||||||
|
|
||||||
def unwire(self, sub):
|
|
||||||
try:
|
|
||||||
self.subscribers.remove(sub)
|
|
||||||
except ValueError:
|
|
||||||
# happens when already removed before
|
|
||||||
pass
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigNotFoundException(Exception):
|
class ConfigNotFoundException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PropertyManager(object):
|
class ConfigError(object):
|
||||||
sharedInstance = None
|
def __init__(self, key, message):
|
||||||
|
self.key = key
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Configuration Error (key: {0}): {1}".format(self.key, self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigMigrator(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def migrate(self, config):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def renameKey(self, config, old, new):
|
||||||
|
if old in config and not new in config:
|
||||||
|
config[new] = config[old]
|
||||||
|
del config[old]
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigMigratorVersion1(ConfigMigrator):
|
||||||
|
def migrate(self, config):
|
||||||
|
if "receiver_gps" in config:
|
||||||
|
gps = config["receiver_gps"]
|
||||||
|
config["receiver_gps"] = {"lat": gps[0], "lon": gps[1]}
|
||||||
|
|
||||||
|
if "waterfall_auto_level_margin" in config:
|
||||||
|
levels = config["waterfall_auto_level_margin"]
|
||||||
|
config["waterfall_auto_level_margin"] = {"min": levels[0], "max": levels[1]}
|
||||||
|
|
||||||
|
self.renameKey(config, "wsjt_queue_workers", "decoding_queue_workers")
|
||||||
|
self.renameKey(config, "wsjt_queue_length", "decoding_queue_length")
|
||||||
|
|
||||||
|
config["version"] = 2
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
sharedConfig = None
|
||||||
|
currentVersion = 2
|
||||||
|
migrators = {
|
||||||
|
1: ConfigMigratorVersion1()
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getSharedInstance():
|
def _loadPythonFile(file):
|
||||||
if PropertyManager.sharedInstance is None:
|
|
||||||
PropertyManager.sharedInstance = PropertyManager()
|
|
||||||
return PropertyManager.sharedInstance
|
|
||||||
|
|
||||||
def collect(self, *props):
|
|
||||||
return PropertyManager(
|
|
||||||
{name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, properties=None):
|
|
||||||
self.properties = {}
|
|
||||||
self.subscribers = []
|
|
||||||
if properties is not None:
|
|
||||||
for (name, prop) in properties.items():
|
|
||||||
self.add(name, prop)
|
|
||||||
|
|
||||||
def add(self, name, prop):
|
|
||||||
self.properties[name] = prop
|
|
||||||
|
|
||||||
def fireCallbacks(value):
|
|
||||||
for c in self.subscribers:
|
|
||||||
try:
|
|
||||||
c.call(name, value)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(e)
|
|
||||||
|
|
||||||
prop.wire(fireCallbacks)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __contains__(self, name):
|
|
||||||
return self.hasProperty(name)
|
|
||||||
|
|
||||||
def __getitem__(self, name):
|
|
||||||
return self.getPropertyValue(name)
|
|
||||||
|
|
||||||
def __setitem__(self, name, value):
|
|
||||||
if not self.hasProperty(name):
|
|
||||||
self.add(name, Property())
|
|
||||||
self.getProperty(name).setValue(value)
|
|
||||||
|
|
||||||
def __dict__(self):
|
|
||||||
return {k: v.getValue() for k, v in self.properties.items()}
|
|
||||||
|
|
||||||
def hasProperty(self, name):
|
|
||||||
return name in self.properties
|
|
||||||
|
|
||||||
def getProperty(self, name):
|
|
||||||
if not self.hasProperty(name):
|
|
||||||
self.add(name, Property())
|
|
||||||
return self.properties[name]
|
|
||||||
|
|
||||||
def getPropertyValue(self, name):
|
|
||||||
return self.getProperty(name).getValue()
|
|
||||||
|
|
||||||
def wire(self, callback):
|
|
||||||
sub = Subscription(self, callback)
|
|
||||||
self.subscribers.append(sub)
|
|
||||||
return sub
|
|
||||||
|
|
||||||
def unwire(self, sub):
|
|
||||||
try:
|
|
||||||
self.subscribers.remove(sub)
|
|
||||||
except ValueError:
|
|
||||||
# happens when already removed before
|
|
||||||
pass
|
|
||||||
return self
|
|
||||||
|
|
||||||
def defaults(self, other_pm):
|
|
||||||
for (key, p) in self.properties.items():
|
|
||||||
if p.getValue() is None:
|
|
||||||
p.setValue(other_pm[key])
|
|
||||||
return self
|
|
||||||
|
|
||||||
def loadConfig(self):
|
|
||||||
for file in ["/etc/openwebrx/config_webrx.py", "./config_webrx.py"]:
|
|
||||||
try:
|
|
||||||
spec = importlib.util.spec_from_file_location("config_webrx", file)
|
spec = importlib.util.spec_from_file_location("config_webrx", file)
|
||||||
cfg = importlib.util.module_from_spec(spec)
|
cfg = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(cfg)
|
spec.loader.exec_module(cfg)
|
||||||
|
pm = PropertyLayer()
|
||||||
for name, value in cfg.__dict__.items():
|
for name, value in cfg.__dict__.items():
|
||||||
if name.startswith("__"):
|
if name.startswith("__"):
|
||||||
continue
|
continue
|
||||||
self[name] = value
|
pm[name] = value
|
||||||
return self
|
return pm
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _loadJsonFile(file):
|
||||||
|
with open(file, "r") as f:
|
||||||
|
pm = PropertyLayer()
|
||||||
|
for k, v in json.load(f).items():
|
||||||
|
pm[k] = v
|
||||||
|
return pm
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _loadConfig():
|
||||||
|
for file in ["./settings.json", "/etc/openwebrx/config_webrx.py", "./config_webrx.py"]:
|
||||||
|
try:
|
||||||
|
if file.endswith(".py"):
|
||||||
|
return Config._loadPythonFile(file)
|
||||||
|
elif file.endswith(".json"):
|
||||||
|
return Config._loadJsonFile(file)
|
||||||
|
else:
|
||||||
|
logger.warning("unsupported file type: %s", file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
raise ConfigNotFoundException("no usable config found! please make sure you have a valid configuration file!")
|
raise ConfigNotFoundException("no usable config found! please make sure you have a valid configuration file!")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get():
|
||||||
|
if Config.sharedConfig is None:
|
||||||
|
Config.sharedConfig = Config._migrate(Config._loadConfig())
|
||||||
|
return Config.sharedConfig
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def store():
|
||||||
|
with open("settings.json", "w") as file:
|
||||||
|
json.dump(Config.get().__dict__(), file, indent=4)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validateConfig():
|
||||||
|
pm = Config.get()
|
||||||
|
errors = [
|
||||||
|
Config.checkTempDirectory(pm)
|
||||||
|
]
|
||||||
|
|
||||||
|
return [e for e in errors if e is not None]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkTempDirectory(pm: PropertyManager):
|
||||||
|
key = "temporary_directory"
|
||||||
|
if key not in pm or pm[key] is None:
|
||||||
|
return ConfigError(key, "temporary directory is not set")
|
||||||
|
if not os.path.exists(pm[key]):
|
||||||
|
return ConfigError(key, "temporary directory doesn't exist")
|
||||||
|
if not os.path.isdir(pm[key]):
|
||||||
|
return ConfigError(key, "temporary directory path is not a directory")
|
||||||
|
if not os.access(pm[key], os.W_OK):
|
||||||
|
return ConfigError(key, "temporary directory is not writable")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _migrate(config):
|
||||||
|
version = config["version"] if "version" in config else 1
|
||||||
|
if version == Config.currentVersion:
|
||||||
|
return config
|
||||||
|
|
||||||
|
logger.debug("migrating config from version %i", version)
|
||||||
|
migrator = Config.migrators[version]
|
||||||
|
return migrator.migrate(config)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from owrx.config import PropertyManager
|
from owrx.config import Config
|
||||||
|
from owrx.details import ReceiverDetails
|
||||||
from owrx.dsp import DspManager
|
from owrx.dsp import DspManager
|
||||||
from owrx.cpu import CpuUsageThread
|
from owrx.cpu import CpuUsageThread
|
||||||
from owrx.sdr import SdrService
|
from owrx.sdr import SdrService
|
||||||
@ -9,9 +10,12 @@ from owrx.version import openwebrx_version
|
|||||||
from owrx.bands import Bandplan
|
from owrx.bands import Bandplan
|
||||||
from owrx.bookmarks import Bookmarks
|
from owrx.bookmarks import Bookmarks
|
||||||
from owrx.map import Map
|
from owrx.map import Map
|
||||||
from owrx.locator import Locator
|
from owrx.property import PropertyStack
|
||||||
|
from owrx.modes import Modes, DigitalMode
|
||||||
from multiprocessing import Queue
|
from multiprocessing import Queue
|
||||||
from queue import Full
|
from queue import Full
|
||||||
|
from js8py import Js8Frame
|
||||||
|
from abc import ABC, ABCMeta, abstractmethod
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@ -20,7 +24,7 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Client(object):
|
class Client(ABC):
|
||||||
def __init__(self, conn):
|
def __init__(self, conn):
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.multiprocessingPipe = Queue(100)
|
self.multiprocessingPipe = Queue(100)
|
||||||
@ -49,6 +53,7 @@ class Client(object):
|
|||||||
except Full:
|
except Full:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
def handleTextMessage(self, conn, message):
|
def handleTextMessage(self, conn, message):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -59,7 +64,25 @@ class Client(object):
|
|||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
class OpenWebRxReceiverClient(Client):
|
class OpenWebRxClient(Client, metaclass=ABCMeta):
|
||||||
|
def __init__(self, conn):
|
||||||
|
super().__init__(conn)
|
||||||
|
|
||||||
|
receiver_details = ReceiverDetails()
|
||||||
|
|
||||||
|
def send_receiver_info(*args):
|
||||||
|
receiver_info = receiver_details.__dict__()
|
||||||
|
self.write_receiver_details(receiver_info)
|
||||||
|
|
||||||
|
# TODO unsubscribe
|
||||||
|
receiver_details.wire(send_receiver_info)
|
||||||
|
send_receiver_info()
|
||||||
|
|
||||||
|
def write_receiver_details(self, details):
|
||||||
|
self.send({"type": "receiver_details", "value": details})
|
||||||
|
|
||||||
|
|
||||||
|
class OpenWebRxReceiverClient(OpenWebRxClient):
|
||||||
config_keys = [
|
config_keys = [
|
||||||
"waterfall_colors",
|
"waterfall_colors",
|
||||||
"waterfall_min_level",
|
"waterfall_min_level",
|
||||||
@ -67,7 +90,6 @@ class OpenWebRxReceiverClient(Client):
|
|||||||
"waterfall_auto_level_margin",
|
"waterfall_auto_level_margin",
|
||||||
"samp_rate",
|
"samp_rate",
|
||||||
"fft_size",
|
"fft_size",
|
||||||
"fft_fps",
|
|
||||||
"audio_compression",
|
"audio_compression",
|
||||||
"fft_compression",
|
"fft_compression",
|
||||||
"max_clients",
|
"max_clients",
|
||||||
@ -93,28 +115,16 @@ class OpenWebRxReceiverClient(Client):
|
|||||||
self.close()
|
self.close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
pm = PropertyManager.getSharedInstance()
|
|
||||||
|
|
||||||
self.setSdr()
|
self.setSdr()
|
||||||
|
|
||||||
# send receiver info
|
|
||||||
receiver_keys = [
|
|
||||||
"receiver_name",
|
|
||||||
"receiver_location",
|
|
||||||
"receiver_asl",
|
|
||||||
"receiver_gps",
|
|
||||||
"photo_title",
|
|
||||||
"photo_desc",
|
|
||||||
]
|
|
||||||
receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys)
|
|
||||||
receiver_details["locator"] = Locator.fromCoordinates(receiver_details["receiver_gps"])
|
|
||||||
self.write_receiver_details(receiver_details)
|
|
||||||
|
|
||||||
self.__sendProfiles()
|
|
||||||
|
|
||||||
features = FeatureDetector().feature_availability()
|
features = FeatureDetector().feature_availability()
|
||||||
self.write_features(features)
|
self.write_features(features)
|
||||||
|
|
||||||
|
modes = Modes.getModes()
|
||||||
|
self.write_modes(modes)
|
||||||
|
|
||||||
|
self.__sendProfiles()
|
||||||
|
|
||||||
CpuUsageThread.getSharedInstance().add_client(self)
|
CpuUsageThread.getSharedInstance().add_client(self)
|
||||||
|
|
||||||
def __sendProfiles(self):
|
def __sendProfiles(self):
|
||||||
@ -128,14 +138,19 @@ class OpenWebRxReceiverClient(Client):
|
|||||||
def handleTextMessage(self, conn, message):
|
def handleTextMessage(self, conn, message):
|
||||||
try:
|
try:
|
||||||
message = json.loads(message)
|
message = json.loads(message)
|
||||||
|
logger.debug(message)
|
||||||
if "type" in message:
|
if "type" in message:
|
||||||
if message["type"] == "dspcontrol":
|
if message["type"] == "dspcontrol":
|
||||||
if "action" in message and message["action"] == "start":
|
if "action" in message and message["action"] == "start":
|
||||||
self.startDsp()
|
self.startDsp()
|
||||||
|
|
||||||
if "params" in message:
|
if "params" in message:
|
||||||
|
dsp = self.getDsp()
|
||||||
|
if dsp is None:
|
||||||
|
logger.warning("DSP not available; discarding client data")
|
||||||
|
else:
|
||||||
params = message["params"]
|
params = message["params"]
|
||||||
self.setDspProperties(params)
|
dsp.setProperties(params)
|
||||||
|
|
||||||
elif message["type"] == "config":
|
elif message["type"] == "config":
|
||||||
if "params" in message:
|
if "params" in message:
|
||||||
@ -152,7 +167,7 @@ class OpenWebRxReceiverClient(Client):
|
|||||||
if "params" in message:
|
if "params" in message:
|
||||||
self.connectionProperties = message["params"]
|
self.connectionProperties = message["params"]
|
||||||
if self.dsp:
|
if self.dsp:
|
||||||
self.setDspProperties(self.connectionProperties)
|
self.getDsp().setProperties(self.connectionProperties)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.warning("received message without type: {0}".format(message))
|
logger.warning("received message without type: {0}".format(message))
|
||||||
@ -169,6 +184,7 @@ class OpenWebRxReceiverClient(Client):
|
|||||||
next = SdrService.getFirstSource()
|
next = SdrService.getFirstSource()
|
||||||
if next is None:
|
if next is None:
|
||||||
# exit condition: no sdrs available
|
# exit condition: no sdrs available
|
||||||
|
logger.warning("no more SDR devices available")
|
||||||
self.handleNoSdrsAvailable()
|
self.handleNoSdrsAvailable()
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -184,25 +200,25 @@ class OpenWebRxReceiverClient(Client):
|
|||||||
|
|
||||||
self.sdr = next
|
self.sdr = next
|
||||||
|
|
||||||
self.startDsp()
|
self.getDsp()
|
||||||
|
|
||||||
# keep trying until we find a suitable SDR
|
# found a working sdr, exit the loop
|
||||||
if self.sdr.getState() == SdrSource.STATE_FAILED:
|
if self.sdr.getState() != SdrSource.STATE_FAILED:
|
||||||
self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
|
|
||||||
else:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# send initial config
|
logger.warning('SDR device "%s" has failed, selecing new device', self.sdr.getName())
|
||||||
self.setDspProperties(self.connectionProperties)
|
self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
|
||||||
|
|
||||||
configProps = (
|
# send initial config
|
||||||
self.sdr.getProps()
|
self.getDsp().setProperties(self.connectionProperties)
|
||||||
.collect(*OpenWebRxReceiverClient.config_keys)
|
|
||||||
.defaults(PropertyManager.getSharedInstance())
|
stack = PropertyStack()
|
||||||
)
|
stack.addLayer(0, self.sdr.getProps())
|
||||||
|
stack.addLayer(1, Config.get())
|
||||||
|
configProps = stack.filter(*OpenWebRxReceiverClient.config_keys)
|
||||||
|
|
||||||
def sendConfig(key, value):
|
def sendConfig(key, value):
|
||||||
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
|
config = configProps.__dict__()
|
||||||
# TODO mathematical properties? hmmmm
|
# TODO mathematical properties? hmmmm
|
||||||
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
|
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
|
||||||
# TODO this is a hack to support multiple sdrs
|
# TODO this is a hack to support multiple sdrs
|
||||||
@ -226,9 +242,7 @@ class OpenWebRxReceiverClient(Client):
|
|||||||
self.write_sdr_error("No SDR Devices available")
|
self.write_sdr_error("No SDR Devices available")
|
||||||
|
|
||||||
def startDsp(self):
|
def startDsp(self):
|
||||||
if self.dsp is None and self.sdr is not None:
|
self.getDsp().start()
|
||||||
self.dsp = DspManager(self, self.sdr)
|
|
||||||
self.dsp.start()
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.stopDsp()
|
self.stopDsp()
|
||||||
@ -247,18 +261,28 @@ class OpenWebRxReceiverClient(Client):
|
|||||||
self.sdr.removeSpectrumClient(self)
|
self.sdr.removeSpectrumClient(self)
|
||||||
|
|
||||||
def setParams(self, params):
|
def setParams(self, params):
|
||||||
|
config = Config.get()
|
||||||
|
# allow direct configuration only if enabled in the config
|
||||||
|
if "configurable_keys" not in config:
|
||||||
|
return
|
||||||
|
keys = config["configurable_keys"]
|
||||||
|
if not keys:
|
||||||
|
return
|
||||||
# only the keys in the protected property manager can be overridden from the web
|
# only the keys in the protected property manager can be overridden from the web
|
||||||
protected = (
|
stack = PropertyStack()
|
||||||
self.sdr.getProps()
|
stack.addLayer(0, self.sdr.getProps())
|
||||||
.collect("samp_rate", "center_freq", "rf_gain", "type")
|
stack.addLayer(1, config)
|
||||||
.defaults(PropertyManager.getSharedInstance())
|
protected = stack.filter(*keys)
|
||||||
)
|
|
||||||
for key, value in params.items():
|
for key, value in params.items():
|
||||||
|
try:
|
||||||
protected[key] = value
|
protected[key] = value
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
def setDspProperties(self, params):
|
def getDsp(self):
|
||||||
for key, value in params.items():
|
if self.dsp is None and self.sdr is not None:
|
||||||
self.dsp.setProperty(key, value)
|
self.dsp = DspManager(self, self.sdr)
|
||||||
|
return self.dsp
|
||||||
|
|
||||||
def write_spectrum_data(self, data):
|
def write_spectrum_data(self, data):
|
||||||
self.mp_send(bytes([0x01]) + data)
|
self.mp_send(bytes([0x01]) + data)
|
||||||
@ -288,9 +312,6 @@ class OpenWebRxReceiverClient(Client):
|
|||||||
def write_config(self, cfg):
|
def write_config(self, cfg):
|
||||||
self.send({"type": "config", "value": cfg})
|
self.send({"type": "config", "value": cfg})
|
||||||
|
|
||||||
def write_receiver_details(self, details):
|
|
||||||
self.send({"type": "receiver_details", "value": details})
|
|
||||||
|
|
||||||
def write_profiles(self, profiles):
|
def write_profiles(self, profiles):
|
||||||
self.send({"type": "profiles", "value": profiles})
|
self.send({"type": "profiles", "value": profiles})
|
||||||
|
|
||||||
@ -324,13 +345,44 @@ class OpenWebRxReceiverClient(Client):
|
|||||||
def write_backoff_message(self, reason):
|
def write_backoff_message(self, reason):
|
||||||
self.send({"type": "backoff", "reason": reason})
|
self.send({"type": "backoff", "reason": reason})
|
||||||
|
|
||||||
|
def write_js8_message(self, frame: Js8Frame, freq: int):
|
||||||
|
self.send({"type": "js8_message", "value": {
|
||||||
|
"msg": str(frame),
|
||||||
|
"timestamp": frame.timestamp,
|
||||||
|
"db": frame.db,
|
||||||
|
"dt": frame.dt,
|
||||||
|
"freq": freq + frame.freq,
|
||||||
|
"thread_type": frame.thread_type,
|
||||||
|
"mode": frame.mode
|
||||||
|
}})
|
||||||
|
|
||||||
class MapConnection(Client):
|
def write_modes(self, modes):
|
||||||
|
def to_json(m):
|
||||||
|
res = {
|
||||||
|
"modulation": m.modulation,
|
||||||
|
"name": m.name,
|
||||||
|
"type": "digimode" if isinstance(m, DigitalMode) else "analog",
|
||||||
|
"requirements": m.requirements,
|
||||||
|
"squelch": m.squelch,
|
||||||
|
}
|
||||||
|
if m.bandpass is not None:
|
||||||
|
res["bandpass"] = {
|
||||||
|
"low_cut": m.bandpass.low_cut,
|
||||||
|
"high_cut": m.bandpass.high_cut
|
||||||
|
}
|
||||||
|
if isinstance(m, DigitalMode):
|
||||||
|
res["underlying"] = m.underlying
|
||||||
|
return res
|
||||||
|
|
||||||
|
self.send({"type": "modes", "value": [to_json(m) for m in modes]})
|
||||||
|
|
||||||
|
|
||||||
|
class MapConnection(OpenWebRxClient):
|
||||||
def __init__(self, conn):
|
def __init__(self, conn):
|
||||||
super().__init__(conn)
|
super().__init__(conn)
|
||||||
|
|
||||||
pm = PropertyManager.getSharedInstance()
|
pm = Config.get()
|
||||||
self.write_config(pm.collect("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__())
|
self.write_config(pm.filter("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__())
|
||||||
|
|
||||||
Map.getSharedInstance().addClient(self)
|
Map.getSharedInstance().addClient(self)
|
||||||
|
|
||||||
|
@ -1,168 +0,0 @@
|
|||||||
import os
|
|
||||||
import mimetypes
|
|
||||||
import json
|
|
||||||
import pkg_resources
|
|
||||||
from datetime import datetime
|
|
||||||
from string import Template
|
|
||||||
from owrx.websocket import WebSocketConnection
|
|
||||||
from owrx.config import PropertyManager
|
|
||||||
from owrx.client import ClientRegistry
|
|
||||||
from owrx.connection import WebSocketMessageHandler
|
|
||||||
from owrx.version import openwebrx_version
|
|
||||||
from owrx.feature import FeatureDetector
|
|
||||||
from owrx.metrics import Metrics
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Controller(object):
|
|
||||||
def __init__(self, handler, request):
|
|
||||||
self.handler = handler
|
|
||||||
self.request = request
|
|
||||||
|
|
||||||
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None):
|
|
||||||
self.handler.send_response(code)
|
|
||||||
if content_type is not None:
|
|
||||||
self.handler.send_header("Content-Type", content_type)
|
|
||||||
if last_modified is not None:
|
|
||||||
self.handler.send_header("Last-Modified", last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT"))
|
|
||||||
if max_age is not None:
|
|
||||||
self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age))
|
|
||||||
self.handler.end_headers()
|
|
||||||
if type(content) == str:
|
|
||||||
content = content.encode()
|
|
||||||
self.handler.wfile.write(content)
|
|
||||||
|
|
||||||
|
|
||||||
class StatusController(Controller):
|
|
||||||
def handle_request(self):
|
|
||||||
pm = PropertyManager.getSharedInstance()
|
|
||||||
# TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna
|
|
||||||
vars = {
|
|
||||||
"status": "active",
|
|
||||||
"name": pm["receiver_name"],
|
|
||||||
"op_email": pm["receiver_admin"],
|
|
||||||
"users": ClientRegistry.getSharedInstance().clientCount(),
|
|
||||||
"users_max": pm["max_clients"],
|
|
||||||
"gps": pm["receiver_gps"],
|
|
||||||
"asl": pm["receiver_asl"],
|
|
||||||
"loc": pm["receiver_location"],
|
|
||||||
"sw_version": openwebrx_version,
|
|
||||||
"avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png"),
|
|
||||||
}
|
|
||||||
self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()]))
|
|
||||||
|
|
||||||
|
|
||||||
class AssetsController(Controller):
|
|
||||||
def getModified(self, file):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def openFile(self, file):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def serve_file(self, file, content_type=None):
|
|
||||||
try:
|
|
||||||
modified = self.getModified(file)
|
|
||||||
|
|
||||||
if modified is not None and "If-Modified-Since" in self.handler.headers:
|
|
||||||
client_modified = datetime.strptime(
|
|
||||||
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
|
|
||||||
)
|
|
||||||
if modified <= client_modified:
|
|
||||||
self.send_response("", code=304)
|
|
||||||
return
|
|
||||||
|
|
||||||
f = self.openFile(file)
|
|
||||||
data = f.read()
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
if content_type is None:
|
|
||||||
(content_type, encoding) = mimetypes.MimeTypes().guess_type(file)
|
|
||||||
self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600)
|
|
||||||
except FileNotFoundError:
|
|
||||||
self.send_response("file not found", code=404)
|
|
||||||
|
|
||||||
def handle_request(self):
|
|
||||||
filename = self.request.matches.group(1)
|
|
||||||
self.serve_file(filename)
|
|
||||||
|
|
||||||
|
|
||||||
class OwrxAssetsController(AssetsController):
|
|
||||||
def openFile(self, file):
|
|
||||||
return pkg_resources.resource_stream("htdocs", file)
|
|
||||||
|
|
||||||
|
|
||||||
class AprsSymbolsController(AssetsController):
|
|
||||||
def __init__(self, handler, request):
|
|
||||||
pm = PropertyManager.getSharedInstance()
|
|
||||||
path = pm["aprs_symbols_path"]
|
|
||||||
if not path.endswith("/"):
|
|
||||||
path += "/"
|
|
||||||
self.path = path
|
|
||||||
super().__init__(handler, request)
|
|
||||||
|
|
||||||
def getFilePath(self, file):
|
|
||||||
return self.path + file
|
|
||||||
|
|
||||||
def getModified(self, file):
|
|
||||||
return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)))
|
|
||||||
|
|
||||||
def openFile(self, file):
|
|
||||||
return open(self.getFilePath(file), "rb")
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateController(Controller):
|
|
||||||
def render_template(self, file, **vars):
|
|
||||||
file_content = pkg_resources.resource_string("htdocs", file).decode("utf-8")
|
|
||||||
template = Template(file_content)
|
|
||||||
|
|
||||||
return template.safe_substitute(**vars)
|
|
||||||
|
|
||||||
def serve_template(self, file, **vars):
|
|
||||||
self.send_response(self.render_template(file, **vars), content_type="text/html")
|
|
||||||
|
|
||||||
def default_variables(self):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
class WebpageController(TemplateController):
|
|
||||||
def template_variables(self):
|
|
||||||
header = self.render_template("include/header.include.html")
|
|
||||||
return {"header": header}
|
|
||||||
|
|
||||||
|
|
||||||
class IndexController(WebpageController):
|
|
||||||
def handle_request(self):
|
|
||||||
self.serve_template("index.html", **self.template_variables())
|
|
||||||
|
|
||||||
|
|
||||||
class MapController(WebpageController):
|
|
||||||
def handle_request(self):
|
|
||||||
# TODO check if we have a google maps api key first?
|
|
||||||
self.serve_template("map.html", **self.template_variables())
|
|
||||||
|
|
||||||
|
|
||||||
class FeatureController(WebpageController):
|
|
||||||
def handle_request(self):
|
|
||||||
self.serve_template("features.html", **self.template_variables())
|
|
||||||
|
|
||||||
|
|
||||||
class ApiController(Controller):
|
|
||||||
def handle_request(self):
|
|
||||||
data = json.dumps(FeatureDetector().feature_report())
|
|
||||||
self.send_response(data, content_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
class MetricsController(Controller):
|
|
||||||
def handle_request(self):
|
|
||||||
data = json.dumps(Metrics.getSharedInstance().getMetrics())
|
|
||||||
self.send_response(data, content_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketController(Controller):
|
|
||||||
def handle_request(self):
|
|
||||||
conn = WebSocketConnection(self.handler, WebSocketMessageHandler())
|
|
||||||
# enter read loop
|
|
||||||
conn.handle()
|
|
40
owrx/controllers/__init__.py
Normal file
40
owrx/controllers/__init__.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(object):
|
||||||
|
def __init__(self, handler, request, options):
|
||||||
|
self.handler = handler
|
||||||
|
self.request = request
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None):
|
||||||
|
self.handler.send_response(code)
|
||||||
|
if content_type is not None:
|
||||||
|
self.handler.send_header("Content-Type", content_type)
|
||||||
|
if last_modified is not None:
|
||||||
|
self.handler.send_header("Last-Modified", last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT"))
|
||||||
|
if max_age is not None:
|
||||||
|
self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age))
|
||||||
|
self.handler.end_headers()
|
||||||
|
if type(content) == str:
|
||||||
|
content = content.encode()
|
||||||
|
self.handler.wfile.write(content)
|
||||||
|
|
||||||
|
def send_redirect(self, location, code=303, cookies=None):
|
||||||
|
self.handler.send_response(code)
|
||||||
|
if cookies is not None:
|
||||||
|
self.handler.send_header("Set-Cookie", cookies.output(header=''))
|
||||||
|
self.handler.send_header("Location", location)
|
||||||
|
self.handler.end_headers()
|
||||||
|
|
||||||
|
def get_body(self):
|
||||||
|
if "Content-Length" not in self.handler.headers:
|
||||||
|
return None
|
||||||
|
length = int(self.handler.headers["Content-Length"])
|
||||||
|
return self.handler.rfile.read(length)
|
||||||
|
|
||||||
|
def handle_request(self):
|
||||||
|
action = "indexAction"
|
||||||
|
if "action" in self.options:
|
||||||
|
action = self.options["action"]
|
||||||
|
getattr(self, action)()
|
33
owrx/controllers/admin.py
Normal file
33
owrx/controllers/admin.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from .template import WebpageController
|
||||||
|
from .session import SessionStorage
|
||||||
|
from owrx.config import Config
|
||||||
|
from urllib import parse
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Authentication(object):
|
||||||
|
def isAuthenticated(self, request):
|
||||||
|
if "owrx-session" in request.cookies:
|
||||||
|
session = SessionStorage.getSharedInstance().getSession(request.cookies["owrx-session"].value)
|
||||||
|
return session is not None
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class AdminController(WebpageController):
|
||||||
|
def __init__(self, handler, request, options):
|
||||||
|
self.authentication = Authentication()
|
||||||
|
super().__init__(handler, request, options)
|
||||||
|
|
||||||
|
def handle_request(self):
|
||||||
|
config = Config.get()
|
||||||
|
if "webadmin_enabled" not in config or not config["webadmin_enabled"]:
|
||||||
|
self.send_response("Web Admin is disabled", code=403)
|
||||||
|
return
|
||||||
|
if self.authentication.isAuthenticated(self.request):
|
||||||
|
super().handle_request()
|
||||||
|
else:
|
||||||
|
target = "/login?{0}".format(parse.urlencode({"ref": self.request.path}))
|
||||||
|
self.send_redirect(target)
|
15
owrx/controllers/api.py
Normal file
15
owrx/controllers/api.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from . import Controller
|
||||||
|
from owrx.feature import FeatureDetector
|
||||||
|
from owrx.details import ReceiverDetails
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class ApiController(Controller):
|
||||||
|
def indexAction(self):
|
||||||
|
data = json.dumps(FeatureDetector().feature_report())
|
||||||
|
self.send_response(data, content_type="application/json")
|
||||||
|
|
||||||
|
def receiverDetails(self):
|
||||||
|
receiver_details = ReceiverDetails()
|
||||||
|
data = json.dumps(receiver_details.__dict__())
|
||||||
|
self.send_response(data, content_type="application/json")
|
129
owrx/controllers/assets.py
Normal file
129
owrx/controllers/assets.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
from . import Controller
|
||||||
|
from owrx.config import Config
|
||||||
|
from datetime import datetime
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import pkg_resources
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class AssetsController(Controller, metaclass=ABCMeta):
|
||||||
|
def getModified(self, file):
|
||||||
|
return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)))
|
||||||
|
|
||||||
|
def openFile(self, file):
|
||||||
|
return open(self.getFilePath(file), "rb")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getFilePath(self, file):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def serve_file(self, file, content_type=None):
|
||||||
|
try:
|
||||||
|
modified = self.getModified(file)
|
||||||
|
|
||||||
|
if modified is not None and "If-Modified-Since" in self.handler.headers:
|
||||||
|
client_modified = datetime.strptime(
|
||||||
|
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
|
||||||
|
)
|
||||||
|
if modified <= client_modified:
|
||||||
|
self.send_response("", code=304)
|
||||||
|
return
|
||||||
|
|
||||||
|
f = self.openFile(file)
|
||||||
|
data = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
if content_type is None:
|
||||||
|
(content_type, encoding) = mimetypes.MimeTypes().guess_type(file)
|
||||||
|
self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600)
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.send_response("file not found", code=404)
|
||||||
|
|
||||||
|
def indexAction(self):
|
||||||
|
filename = self.request.matches.group(1)
|
||||||
|
self.serve_file(filename)
|
||||||
|
|
||||||
|
|
||||||
|
class OwrxAssetsController(AssetsController):
|
||||||
|
def getFilePath(self, file):
|
||||||
|
return pkg_resources.resource_filename("htdocs", file)
|
||||||
|
|
||||||
|
|
||||||
|
class AprsSymbolsController(AssetsController):
|
||||||
|
def __init__(self, handler, request, options):
|
||||||
|
pm = Config.get()
|
||||||
|
path = pm["aprs_symbols_path"]
|
||||||
|
if not path.endswith("/"):
|
||||||
|
path += "/"
|
||||||
|
self.path = path
|
||||||
|
super().__init__(handler, request, options)
|
||||||
|
|
||||||
|
def getFilePath(self, file):
|
||||||
|
return self.path + file
|
||||||
|
|
||||||
|
|
||||||
|
class CompiledAssetsController(Controller):
|
||||||
|
profiles = {
|
||||||
|
"receiver.js": [
|
||||||
|
"openwebrx.js",
|
||||||
|
"lib/jquery-3.2.1.min.js",
|
||||||
|
"lib/jquery.nanoscroller.js",
|
||||||
|
"lib/Header.js",
|
||||||
|
"lib/Demodulator.js",
|
||||||
|
"lib/DemodulatorPanel.js",
|
||||||
|
"lib/BookmarkBar.js",
|
||||||
|
"lib/BookmarkDialog.js",
|
||||||
|
"lib/AudioEngine.js",
|
||||||
|
"lib/ProgressBar.js",
|
||||||
|
"lib/Measurement.js",
|
||||||
|
"lib/FrequencyDisplay.js",
|
||||||
|
"lib/Js8Threads.js",
|
||||||
|
"lib/Modes.js",
|
||||||
|
],
|
||||||
|
"map.js": [
|
||||||
|
"lib/jquery-3.2.1.min.js",
|
||||||
|
"lib/chroma.min.js",
|
||||||
|
"lib/Header.js",
|
||||||
|
"map.js",
|
||||||
|
],
|
||||||
|
"settings.js": [
|
||||||
|
"lib/jquery-3.2.1.min.js",
|
||||||
|
"lib/Header.js",
|
||||||
|
"lib/settings/Input.js",
|
||||||
|
"lib/settings/SdrDevice.js",
|
||||||
|
"settings.js",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def indexAction(self):
|
||||||
|
profileName = self.request.matches.group(1)
|
||||||
|
if profileName not in CompiledAssetsController.profiles:
|
||||||
|
self.send_response("profile not found", code=404)
|
||||||
|
return
|
||||||
|
|
||||||
|
files = CompiledAssetsController.profiles[profileName]
|
||||||
|
files = [pkg_resources.resource_filename("htdocs", f) for f in files]
|
||||||
|
|
||||||
|
modified = self.getModified(files)
|
||||||
|
|
||||||
|
if modified is not None and "If-Modified-Since" in self.handler.headers:
|
||||||
|
client_modified = datetime.strptime(
|
||||||
|
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
|
||||||
|
)
|
||||||
|
if modified <= client_modified:
|
||||||
|
self.send_response("", code=304)
|
||||||
|
return
|
||||||
|
|
||||||
|
contents = [self.getContents(f) for f in files]
|
||||||
|
|
||||||
|
(content_type, encoding) = mimetypes.MimeTypes().guess_type(profileName)
|
||||||
|
self.send_response("\n".join(contents), content_type=content_type, last_modified=modified, max_age=3600)
|
||||||
|
|
||||||
|
def getContents(self, file):
|
||||||
|
with open(file) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def getModified(self, files):
|
||||||
|
modified = [datetime.fromtimestamp(os.path.getmtime(f)) for f in files]
|
||||||
|
return max(*modified)
|
9
owrx/controllers/metrics.py
Normal file
9
owrx/controllers/metrics.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from . import Controller
|
||||||
|
from owrx.metrics import Metrics
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsController(Controller):
|
||||||
|
def indexAction(self):
|
||||||
|
data = json.dumps(Metrics.getSharedInstance().getMetrics())
|
||||||
|
self.send_response(data, content_type="application/json")
|
59
owrx/controllers/session.py
Normal file
59
owrx/controllers/session.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from .template import WebpageController
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
from uuid import uuid4
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
|
from owrx.users import UserList
|
||||||
|
|
||||||
|
|
||||||
|
class SessionStorage(object):
|
||||||
|
sharedInstance = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getSharedInstance():
|
||||||
|
if SessionStorage.sharedInstance is None:
|
||||||
|
SessionStorage.sharedInstance = SessionStorage()
|
||||||
|
return SessionStorage.sharedInstance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.sessions = {}
|
||||||
|
|
||||||
|
def generateKey(self):
|
||||||
|
return str(uuid4())
|
||||||
|
|
||||||
|
def startSession(self, data):
|
||||||
|
key = self.generateKey()
|
||||||
|
self.updateSession(key, data)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def getSession(self, key):
|
||||||
|
if key not in self.sessions:
|
||||||
|
return None
|
||||||
|
return self.sessions[key]
|
||||||
|
|
||||||
|
def updateSession(self, key, data):
|
||||||
|
self.sessions[key] = data
|
||||||
|
|
||||||
|
|
||||||
|
class SessionController(WebpageController):
|
||||||
|
def loginAction(self):
|
||||||
|
self.serve_template("login.html", **self.template_variables())
|
||||||
|
|
||||||
|
def processLoginAction(self):
|
||||||
|
data = parse_qs(self.get_body().decode("utf-8"))
|
||||||
|
data = {k: v[0] for k, v in data.items()}
|
||||||
|
userlist = UserList.getSharedInstance()
|
||||||
|
if "user" in data and "password" in data:
|
||||||
|
if data["user"] in userlist:
|
||||||
|
user = userlist[data["user"]]
|
||||||
|
if user.password.is_valid(data["password"]):
|
||||||
|
# TODO evaluate password force_change and redirect to password change
|
||||||
|
key = SessionStorage.getSharedInstance().startSession({"user": user.name})
|
||||||
|
cookie = SimpleCookie()
|
||||||
|
cookie["owrx-session"] = key
|
||||||
|
target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings"
|
||||||
|
self.send_redirect(target, cookies=cookie)
|
||||||
|
return
|
||||||
|
self.send_redirect("/login")
|
||||||
|
|
||||||
|
def logoutAction(self):
|
||||||
|
self.send_redirect("logout happening here")
|
279
owrx/controllers/settings.py
Normal file
279
owrx/controllers/settings.py
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
from .admin import AdminController
|
||||||
|
from owrx.config import Config
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
from owrx.form import (
|
||||||
|
TextInput,
|
||||||
|
NumberInput,
|
||||||
|
FloatInput,
|
||||||
|
LocationInput,
|
||||||
|
TextAreaInput,
|
||||||
|
CheckboxInput,
|
||||||
|
DropdownInput,
|
||||||
|
Option,
|
||||||
|
ServicesCheckboxInput,
|
||||||
|
Js8ProfileCheckboxInput,
|
||||||
|
)
|
||||||
|
from urllib.parse import quote
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Section(object):
|
||||||
|
def __init__(self, title, *inputs):
|
||||||
|
self.title = title
|
||||||
|
self.inputs = inputs
|
||||||
|
|
||||||
|
def render_inputs(self):
|
||||||
|
config = Config.get()
|
||||||
|
return "".join([i.render(config) for i in self.inputs])
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
return """
|
||||||
|
<div class="col-12 settings-category">
|
||||||
|
<h3 class="settings-header">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{inputs}
|
||||||
|
</div>
|
||||||
|
""".format(
|
||||||
|
title=self.title, inputs=self.render_inputs()
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse(self, data):
|
||||||
|
return {k: v for i in self.inputs for k, v in i.parse(data).items()}
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsController(AdminController):
|
||||||
|
def indexAction(self):
|
||||||
|
self.serve_template("settings.html", **self.template_variables())
|
||||||
|
|
||||||
|
|
||||||
|
class SdrSettingsController(AdminController):
|
||||||
|
def template_variables(self):
|
||||||
|
variables = super().template_variables()
|
||||||
|
variables["devices"] = self.render_devices()
|
||||||
|
return variables
|
||||||
|
|
||||||
|
def render_devices(self):
|
||||||
|
return "".join(self.render_device(key, value) for key, value in Config.get()["sdrs"].items())
|
||||||
|
|
||||||
|
def render_device(self, device_id, config):
|
||||||
|
return """
|
||||||
|
<div class="card device bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
{device_name}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{form}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""".format(device_name=config["name"], form=self.render_form(device_id, config))
|
||||||
|
|
||||||
|
def render_form(self, device_id, config):
|
||||||
|
return """
|
||||||
|
<form class="sdrdevice" data-config="{formdata}"></form>
|
||||||
|
""".format(device_id=device_id, formdata=quote(json.dumps(config)))
|
||||||
|
|
||||||
|
def indexAction(self):
|
||||||
|
self.serve_template("sdrsettings.html", **self.template_variables())
|
||||||
|
|
||||||
|
|
||||||
|
class GeneralSettingsController(AdminController):
|
||||||
|
sections = [
|
||||||
|
Section(
|
||||||
|
"General settings",
|
||||||
|
TextInput("receiver_name", "Receiver name"),
|
||||||
|
TextInput("receiver_location", "Receiver location"),
|
||||||
|
NumberInput(
|
||||||
|
"receiver_asl",
|
||||||
|
"Receiver elevation",
|
||||||
|
infotext="Elevation in meters above mean see level",
|
||||||
|
),
|
||||||
|
TextInput("receiver_admin", "Receiver admin"),
|
||||||
|
LocationInput("receiver_gps", "Receiver coordinates"),
|
||||||
|
TextInput("photo_title", "Photo title"),
|
||||||
|
TextAreaInput("photo_desc", "Photo description"),
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
"Waterfall settings",
|
||||||
|
NumberInput(
|
||||||
|
"fft_fps",
|
||||||
|
"FFT frames per second",
|
||||||
|
infotext="This setting specifies how many lines are being added to the waterfall per second. "
|
||||||
|
+ "Higher values will give you a faster waterfall, but will also use more CPU.",
|
||||||
|
),
|
||||||
|
NumberInput("fft_size", "FFT size"),
|
||||||
|
FloatInput(
|
||||||
|
"fft_voverlap_factor",
|
||||||
|
"FFT vertical overlap factor",
|
||||||
|
infotext="If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the "
|
||||||
|
+ "diagram.",
|
||||||
|
),
|
||||||
|
NumberInput("waterfall_min_level", "Lowest waterfall level"),
|
||||||
|
NumberInput("waterfall_max_level", "Highest waterfall level"),
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
"Compression",
|
||||||
|
DropdownInput(
|
||||||
|
"audio_compression",
|
||||||
|
"Audio compression",
|
||||||
|
options=[Option("adpcm", "ADPCM"), Option("none", "None"),],
|
||||||
|
),
|
||||||
|
DropdownInput(
|
||||||
|
"fft_compression",
|
||||||
|
"Waterfall compression",
|
||||||
|
options=[Option("adpcm", "ADPCM"), Option("none", "None"),],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
"Digimodes",
|
||||||
|
CheckboxInput("digimodes_enable", "", checkboxText="Enable Digimodes"),
|
||||||
|
NumberInput("digimodes_fft_size", "Digimodes FFT size"),
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
"Digital voice",
|
||||||
|
NumberInput(
|
||||||
|
"digital_voice_unvoiced_quality",
|
||||||
|
"Quality of unvoiced sounds in synthesized voice",
|
||||||
|
infotext="Determines the quality, and thus the cpu usage, for the ambe codec used by digital voice"
|
||||||
|
+ "modes.<br />If you're running on a Raspi (up to 3B+) you should leave this set at 1",
|
||||||
|
),
|
||||||
|
CheckboxInput(
|
||||||
|
"digital_voice_dmr_id_lookup",
|
||||||
|
"DMR id lookup",
|
||||||
|
checkboxText="Enable lookup of DMR ids in the radioid database to show callsigns and names",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
"Experimental pipe settings",
|
||||||
|
CheckboxInput(
|
||||||
|
"csdr_dynamic_bufsize",
|
||||||
|
"",
|
||||||
|
checkboxText="Enable dynamic buffer sizes",
|
||||||
|
infotext="This allows you to change the buffering mode of csdr.",
|
||||||
|
),
|
||||||
|
CheckboxInput(
|
||||||
|
"csdr_print_bufsizes",
|
||||||
|
"",
|
||||||
|
checkboxText="Print buffer sizez",
|
||||||
|
infotext="This prints the buffer sizes used for csdr processes.",
|
||||||
|
),
|
||||||
|
CheckboxInput(
|
||||||
|
"csdr_through",
|
||||||
|
"",
|
||||||
|
checkboxText="Print throughput",
|
||||||
|
infotext="Enabling this will print out how much data is going into the DSP chains.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
"Map settings",
|
||||||
|
TextInput(
|
||||||
|
"google_maps_api_key",
|
||||||
|
"Google Maps API key",
|
||||||
|
infotext="Google Maps requires an API key, check out "
|
||||||
|
+ '<a href="https://developers.google.com/maps/documentation/embed/get-api-key" target="_blank">'
|
||||||
|
+ "their documentation</a> on how to obtain one.",
|
||||||
|
),
|
||||||
|
NumberInput(
|
||||||
|
"map_position_retention_time",
|
||||||
|
"Map retention time",
|
||||||
|
infotext="Unit is seconds<br/>Specifies how log markers / grids will remain visible on the map",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
"Decoding settings",
|
||||||
|
NumberInput("decoding_queue_workers", "Number of decoding workers"),
|
||||||
|
NumberInput("decoding_queue_length", "Maximum length of decoding job queue"),
|
||||||
|
NumberInput(
|
||||||
|
"wsjt_decoding_depth",
|
||||||
|
"Default WSJT decoding depth",
|
||||||
|
infotext="A higher decoding depth will allow more results, but will also consume more cpu",
|
||||||
|
),
|
||||||
|
NumberInput(
|
||||||
|
"js8_decoding_depth",
|
||||||
|
"Js8Call decoding depth",
|
||||||
|
infotext="A higher decoding depth will allow more results, but will also consume more cpu",
|
||||||
|
),
|
||||||
|
Js8ProfileCheckboxInput(
|
||||||
|
"js8_enabled_profiles",
|
||||||
|
"Js8Call enabled modes"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
"Background decoding",
|
||||||
|
CheckboxInput(
|
||||||
|
"services_enabled",
|
||||||
|
"Service",
|
||||||
|
checkboxText="Enable background decoding services",
|
||||||
|
),
|
||||||
|
ServicesCheckboxInput("services_decoders", "Enabled services"),
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
"APRS settings",
|
||||||
|
TextInput(
|
||||||
|
"aprs_callsign",
|
||||||
|
"APRS callsign",
|
||||||
|
infotext="This callsign will be used to send data to the APRS-IS network",
|
||||||
|
),
|
||||||
|
CheckboxInput(
|
||||||
|
"aprs_igate_enabled",
|
||||||
|
"APRS I-Gate",
|
||||||
|
checkboxText="Enable APRS receive-only I-Gate",
|
||||||
|
),
|
||||||
|
TextInput("aprs_igate_server", "APRS-IS server"),
|
||||||
|
TextInput("aprs_igate_password", "APRS-IS network password"),
|
||||||
|
CheckboxInput(
|
||||||
|
"aprs_igate_beacon",
|
||||||
|
"APRS beacon",
|
||||||
|
checkboxText="Send the receiver position to the APRS-IS network",
|
||||||
|
infotext="Please check that your receiver location is setup correctly",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
"pskreporter settings",
|
||||||
|
CheckboxInput(
|
||||||
|
"pskreporter_enabled",
|
||||||
|
"Reporting",
|
||||||
|
checkboxText="Enable sending spots to pskreporter.info",
|
||||||
|
),
|
||||||
|
TextInput(
|
||||||
|
"pskreporter_callsign",
|
||||||
|
"pskreporter callsign",
|
||||||
|
infotext="This callsign will be used to send spots to pskreporter.info",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def render_sections(self):
|
||||||
|
sections = "".join(section.render() for section in GeneralSettingsController.sections)
|
||||||
|
return """
|
||||||
|
<form class="settings-body" method="POST">
|
||||||
|
{sections}
|
||||||
|
<div class="buttons">
|
||||||
|
<button type="submit" class="btn btn-primary">Apply</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
""".format(
|
||||||
|
sections=sections
|
||||||
|
)
|
||||||
|
|
||||||
|
def indexAction(self):
|
||||||
|
self.serve_template("generalsettings.html", **self.template_variables())
|
||||||
|
|
||||||
|
def template_variables(self):
|
||||||
|
variables = super().template_variables()
|
||||||
|
variables["sections"] = self.render_sections()
|
||||||
|
return variables
|
||||||
|
|
||||||
|
def processFormData(self):
|
||||||
|
data = parse_qs(self.get_body().decode("utf-8"))
|
||||||
|
data = {
|
||||||
|
k: v for i in GeneralSettingsController.sections for k, v in i.parse(data).items()
|
||||||
|
}
|
||||||
|
config = Config.get()
|
||||||
|
for k, v in data.items():
|
||||||
|
config[k] = v
|
||||||
|
Config.store()
|
||||||
|
self.send_redirect("/admin")
|
43
owrx/controllers/status.py
Normal file
43
owrx/controllers/status.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from . import Controller
|
||||||
|
from owrx.client import ClientRegistry
|
||||||
|
from owrx.version import openwebrx_version
|
||||||
|
from owrx.sdr import SdrService
|
||||||
|
from owrx.config import Config
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
|
||||||
|
class StatusController(Controller):
|
||||||
|
def getProfileStats(self, profile):
|
||||||
|
return {
|
||||||
|
"name": profile["name"],
|
||||||
|
"center_freq": profile["center_freq"],
|
||||||
|
"sample_rate": profile["samp_rate"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def getReceiverStats(self, receiver):
|
||||||
|
stats = {
|
||||||
|
"name": receiver.getName(),
|
||||||
|
# TODO would be better to have types from the config here
|
||||||
|
"type": type(receiver).__name__,
|
||||||
|
"profiles": [self.getProfileStats(p) for p in receiver.getProfiles().values()]
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def indexAction(self):
|
||||||
|
pm = Config.get()
|
||||||
|
|
||||||
|
status = {
|
||||||
|
"receiver": {
|
||||||
|
"name": pm["receiver_name"],
|
||||||
|
"admin": pm["receiver_admin"],
|
||||||
|
"gps": pm["receiver_gps"],
|
||||||
|
"asl": pm["receiver_asl"],
|
||||||
|
"location": pm["receiver_location"],
|
||||||
|
},
|
||||||
|
"max_clients": pm["max_clients"],
|
||||||
|
"version": openwebrx_version,
|
||||||
|
"sdrs": [self.getReceiverStats(r) for r in SdrService.getSources().values()]
|
||||||
|
}
|
||||||
|
self.send_response(json.dumps(status), content_type="application/json")
|
44
owrx/controllers/template.py
Normal file
44
owrx/controllers/template.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from . import Controller
|
||||||
|
import pkg_resources
|
||||||
|
from string import Template
|
||||||
|
from owrx.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateController(Controller):
|
||||||
|
def render_template(self, file, **vars):
|
||||||
|
file_content = pkg_resources.resource_string("htdocs", file).decode("utf-8")
|
||||||
|
template = Template(file_content)
|
||||||
|
|
||||||
|
return template.safe_substitute(**vars)
|
||||||
|
|
||||||
|
def serve_template(self, file, **vars):
|
||||||
|
self.send_response(self.render_template(file, **vars), content_type="text/html")
|
||||||
|
|
||||||
|
def default_variables(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class WebpageController(TemplateController):
|
||||||
|
def template_variables(self):
|
||||||
|
settingslink = ""
|
||||||
|
pm = Config.get()
|
||||||
|
if "webadmin_enabled" in pm and pm["webadmin_enabled"]:
|
||||||
|
settingslink = """<a class="button" href="settings" target="openwebrx-settings"><img src="static/gfx/openwebrx-panel-settings.png" alt="Settings"/><br/>Settings</a>"""
|
||||||
|
header = self.render_template("include/header.include.html", settingslink=settingslink)
|
||||||
|
return {"header": header}
|
||||||
|
|
||||||
|
|
||||||
|
class IndexController(WebpageController):
|
||||||
|
def indexAction(self):
|
||||||
|
self.serve_template("index.html", **self.template_variables())
|
||||||
|
|
||||||
|
|
||||||
|
class MapController(WebpageController):
|
||||||
|
def indexAction(self):
|
||||||
|
# TODO check if we have a google maps api key first?
|
||||||
|
self.serve_template("map.html", **self.template_variables())
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureController(WebpageController):
|
||||||
|
def indexAction(self):
|
||||||
|
self.serve_template("features.html", **self.template_variables())
|
10
owrx/controllers/websocket.py
Normal file
10
owrx/controllers/websocket.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from . import Controller
|
||||||
|
from owrx.websocket import WebSocketConnection
|
||||||
|
from owrx.connection import WebSocketMessageHandler
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketController(Controller):
|
||||||
|
def indexAction(self):
|
||||||
|
conn = WebSocketConnection(self.handler, WebSocketMessageHandler())
|
||||||
|
# enter read loop
|
||||||
|
conn.handle()
|
21
owrx/details.py
Normal file
21
owrx/details.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from owrx.config import Config
|
||||||
|
from owrx.locator import Locator
|
||||||
|
from owrx.property import PropertyFilter
|
||||||
|
|
||||||
|
|
||||||
|
class ReceiverDetails(PropertyFilter):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
Config.get(),
|
||||||
|
"receiver_name",
|
||||||
|
"receiver_location",
|
||||||
|
"receiver_asl",
|
||||||
|
"receiver_gps",
|
||||||
|
"photo_title",
|
||||||
|
"photo_desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __dict__(self):
|
||||||
|
receiver_info = super().__dict__()
|
||||||
|
receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
|
||||||
|
return receiver_info
|
104
owrx/dsp.py
104
owrx/dsp.py
@ -1,9 +1,11 @@
|
|||||||
from owrx.config import PropertyManager
|
|
||||||
from owrx.meta import MetaParser
|
from owrx.meta import MetaParser
|
||||||
from owrx.wsjt import WsjtParser
|
from owrx.wsjt import WsjtParser
|
||||||
|
from owrx.js8 import Js8Parser
|
||||||
from owrx.aprs import AprsParser
|
from owrx.aprs import AprsParser
|
||||||
from owrx.pocsag import PocsagParser
|
from owrx.pocsag import PocsagParser
|
||||||
from owrx.source import SdrSource
|
from owrx.source import SdrSource
|
||||||
|
from owrx.property import PropertyStack, PropertyLayer
|
||||||
|
from owrx.modes import Modes
|
||||||
from csdr import csdr
|
from csdr import csdr
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@ -21,11 +23,24 @@ class DspManager(csdr.output):
|
|||||||
"wsjt_demod": WsjtParser(self.handler),
|
"wsjt_demod": WsjtParser(self.handler),
|
||||||
"packet_demod": AprsParser(self.handler),
|
"packet_demod": AprsParser(self.handler),
|
||||||
"pocsag_demod": PocsagParser(self.handler),
|
"pocsag_demod": PocsagParser(self.handler),
|
||||||
|
"js8_demod": Js8Parser(self.handler),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.localProps = (
|
self.props = PropertyStack()
|
||||||
self.sdrSource.getProps()
|
# local demodulator properties not forwarded to the sdr
|
||||||
.collect(
|
self.props.addLayer(0, PropertyLayer().filter(
|
||||||
|
"output_rate",
|
||||||
|
"squelch_level",
|
||||||
|
"secondary_mod",
|
||||||
|
"low_cut",
|
||||||
|
"high_cut",
|
||||||
|
"offset_freq",
|
||||||
|
"mod",
|
||||||
|
"secondary_offset_freq",
|
||||||
|
"dmr_filter",
|
||||||
|
))
|
||||||
|
# properties that we inherit from the sdr
|
||||||
|
self.props.addLayer(1, self.sdrSource.getProps().filter(
|
||||||
"audio_compression",
|
"audio_compression",
|
||||||
"fft_compression",
|
"fft_compression",
|
||||||
"digimodes_fft_size",
|
"digimodes_fft_size",
|
||||||
@ -35,12 +50,11 @@ class DspManager(csdr.output):
|
|||||||
"digimodes_enable",
|
"digimodes_enable",
|
||||||
"samp_rate",
|
"samp_rate",
|
||||||
"digital_voice_unvoiced_quality",
|
"digital_voice_unvoiced_quality",
|
||||||
"dmr_filter",
|
|
||||||
"temporary_directory",
|
"temporary_directory",
|
||||||
"center_freq",
|
"center_freq",
|
||||||
)
|
"start_mod",
|
||||||
.defaults(PropertyManager.getSharedInstance())
|
"start_freq",
|
||||||
)
|
))
|
||||||
|
|
||||||
self.dsp = csdr.dsp(self)
|
self.dsp = csdr.dsp(self)
|
||||||
self.dsp.nc_port = self.sdrSource.getPort()
|
self.dsp.nc_port = self.sdrSource.getPort()
|
||||||
@ -56,34 +70,47 @@ class DspManager(csdr.output):
|
|||||||
self.dsp.set_bpf(*bpf)
|
self.dsp.set_bpf(*bpf)
|
||||||
|
|
||||||
def set_dial_freq(key, value):
|
def set_dial_freq(key, value):
|
||||||
freq = self.localProps["center_freq"] + self.localProps["offset_freq"]
|
freq = self.props["center_freq"] + self.props["offset_freq"]
|
||||||
for parser in self.parsers.values():
|
for parser in self.parsers.values():
|
||||||
parser.setDialFrequency(freq)
|
parser.setDialFrequency(freq)
|
||||||
|
|
||||||
|
if "start_mod" in self.props:
|
||||||
|
self.dsp.set_demodulator(self.props["start_mod"])
|
||||||
|
mode = Modes.findByModulation(self.props["start_mod"])
|
||||||
|
|
||||||
|
if mode and mode.bandpass:
|
||||||
|
self.dsp.set_bpf(mode.bandpass.low_cut, mode.bandpass.high_cut)
|
||||||
|
else:
|
||||||
|
self.dsp.set_bpf(-4000, 4000)
|
||||||
|
|
||||||
|
if "start_freq" in self.props and "center_freq" in self.props:
|
||||||
|
self.dsp.set_offset_freq(self.props["start_freq"] - self.props["center_freq"])
|
||||||
|
else:
|
||||||
|
self.dsp.set_offset_freq(0)
|
||||||
|
|
||||||
self.subscriptions = [
|
self.subscriptions = [
|
||||||
self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression),
|
self.props.wireProperty("audio_compression", self.dsp.set_audio_compression),
|
||||||
self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression),
|
self.props.wireProperty("fft_compression", self.dsp.set_fft_compression),
|
||||||
self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size),
|
self.props.wireProperty("digimodes_fft_size", self.dsp.set_secondary_fft_size),
|
||||||
self.localProps.getProperty("samp_rate").wire(self.dsp.set_samp_rate),
|
self.props.wireProperty("samp_rate", self.dsp.set_samp_rate),
|
||||||
self.localProps.getProperty("output_rate").wire(self.dsp.set_output_rate),
|
self.props.wireProperty("output_rate", self.dsp.set_output_rate),
|
||||||
self.localProps.getProperty("offset_freq").wire(self.dsp.set_offset_freq),
|
self.props.wireProperty("offset_freq", self.dsp.set_offset_freq),
|
||||||
self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level),
|
self.props.wireProperty("center_freq", self.dsp.set_center_freq),
|
||||||
self.localProps.getProperty("low_cut").wire(set_low_cut),
|
self.props.wireProperty("squelch_level", self.dsp.set_squelch_level),
|
||||||
self.localProps.getProperty("high_cut").wire(set_high_cut),
|
self.props.wireProperty("low_cut", set_low_cut),
|
||||||
self.localProps.getProperty("mod").wire(self.dsp.set_demodulator),
|
self.props.wireProperty("high_cut", set_high_cut),
|
||||||
self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality),
|
self.props.wireProperty("mod", self.dsp.set_demodulator),
|
||||||
self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter),
|
self.props.wireProperty("digital_voice_unvoiced_quality", self.dsp.set_unvoiced_quality),
|
||||||
self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory),
|
self.props.wireProperty("dmr_filter", self.dsp.set_dmr_filter),
|
||||||
self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq),
|
self.props.wireProperty("temporary_directory", self.dsp.set_temporary_directory),
|
||||||
|
self.props.filter("center_freq", "offset_freq").wire(set_dial_freq),
|
||||||
]
|
]
|
||||||
|
|
||||||
self.dsp.set_offset_freq(0)
|
self.dsp.csdr_dynamic_bufsize = self.props["csdr_dynamic_bufsize"]
|
||||||
self.dsp.set_bpf(-4000, 4000)
|
self.dsp.csdr_print_bufsizes = self.props["csdr_print_bufsizes"]
|
||||||
self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"]
|
self.dsp.csdr_through = self.props["csdr_through"]
|
||||||
self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"]
|
|
||||||
self.dsp.csdr_through = self.localProps["csdr_through"]
|
|
||||||
|
|
||||||
if self.localProps["digimodes_enable"]:
|
if self.props["digimodes_enable"]:
|
||||||
|
|
||||||
def set_secondary_mod(mod):
|
def set_secondary_mod(mod):
|
||||||
if mod == False:
|
if mod == False:
|
||||||
@ -92,17 +119,19 @@ class DspManager(csdr.output):
|
|||||||
if mod is not None:
|
if mod is not None:
|
||||||
self.handler.write_secondary_dsp_config(
|
self.handler.write_secondary_dsp_config(
|
||||||
{
|
{
|
||||||
"secondary_fft_size": self.localProps["digimodes_fft_size"],
|
"secondary_fft_size": self.props["digimodes_fft_size"],
|
||||||
"if_samp_rate": self.dsp.if_samp_rate(),
|
"if_samp_rate": self.dsp.if_samp_rate(),
|
||||||
"secondary_bw": self.dsp.secondary_bw(),
|
"secondary_bw": self.dsp.secondary_bw(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.subscriptions += [
|
self.subscriptions += [
|
||||||
self.localProps.getProperty("secondary_mod").wire(set_secondary_mod),
|
self.props.wireProperty("secondary_mod", set_secondary_mod),
|
||||||
self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq),
|
self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
self.startOnAvailable = False
|
||||||
|
|
||||||
self.sdrSource.addClient(self)
|
self.sdrSource.addClient(self)
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -110,6 +139,8 @@ class DspManager(csdr.output):
|
|||||||
def start(self):
|
def start(self):
|
||||||
if self.sdrSource.isAvailable():
|
if self.sdrSource.isAvailable():
|
||||||
self.dsp.start()
|
self.dsp.start()
|
||||||
|
else:
|
||||||
|
self.startOnAvailable = True
|
||||||
|
|
||||||
def receive_output(self, t, read_fn):
|
def receive_output(self, t, read_fn):
|
||||||
logger.debug("adding new output of type %s", t)
|
logger.debug("adding new output of type %s", t)
|
||||||
@ -128,13 +159,18 @@ class DspManager(csdr.output):
|
|||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.dsp.stop()
|
self.dsp.stop()
|
||||||
|
self.startOnAvailable = False
|
||||||
self.sdrSource.removeClient(self)
|
self.sdrSource.removeClient(self)
|
||||||
for sub in self.subscriptions:
|
for sub in self.subscriptions:
|
||||||
sub.cancel()
|
sub.cancel()
|
||||||
self.subscriptions = []
|
self.subscriptions = []
|
||||||
|
|
||||||
|
def setProperties(self, props):
|
||||||
|
for k, v in props.items():
|
||||||
|
self.setProperty(k, v)
|
||||||
|
|
||||||
def setProperty(self, prop, value):
|
def setProperty(self, prop, value):
|
||||||
self.localProps.getProperty(prop).setValue(value)
|
self.props[prop] = value
|
||||||
|
|
||||||
def getClientClass(self):
|
def getClientClass(self):
|
||||||
return SdrSource.CLIENT_USER
|
return SdrSource.CLIENT_USER
|
||||||
@ -142,7 +178,9 @@ class DspManager(csdr.output):
|
|||||||
def onStateChange(self, state):
|
def onStateChange(self, state):
|
||||||
if state == SdrSource.STATE_RUNNING:
|
if state == SdrSource.STATE_RUNNING:
|
||||||
logger.debug("received STATE_RUNNING, attempting DspSource restart")
|
logger.debug("received STATE_RUNNING, attempting DspSource restart")
|
||||||
|
if self.startOnAvailable:
|
||||||
self.dsp.start()
|
self.dsp.start()
|
||||||
|
self.startOnAvailable = False
|
||||||
elif state == SdrSource.STATE_STOPPING:
|
elif state == SdrSource.STATE_STOPPING:
|
||||||
logger.debug("received STATE_STOPPING, shutting down DspSource")
|
logger.debug("received STATE_STOPPING, shutting down DspSource")
|
||||||
self.dsp.stop()
|
self.dsp.stop()
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user