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

|
||||

|
||||
|
||||
It has the following features:
|
||||
|
||||
@ -19,58 +19,21 @@ It has the following features:
|
||||
|
||||
## 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
|
||||
depencencies out of the box, and should work on all Raspberry Pis. It is based off the Raspbian Lite distribution,
|
||||
so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply.
|
||||
- Raspberry Pi SD card images
|
||||
- Debian repository
|
||||
- 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
|
||||
also checkout the `nightly` folder, which has the most recent builds, albeit untested.
|
||||
Please checkout the [setup guide on the wiki](https://github.com/jketterl/openwebrx/wiki/Setup-Guide) for more details
|
||||
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
|
||||
should make it available as https://openwebrx/ on most networks. This may vary depending on your specific setup.
|
||||
## Community
|
||||
|
||||
For Digital voice, the minimum requirement right now seems to be a Rasbperry Pi 3B+. I would like to work on optimizing
|
||||
this for lower specs, but at this point I am not sure how much can be done.
|
||||
|
||||
### Docker Images
|
||||
|
||||
For those familiar with docker, I am providing
|
||||
[recent builds and Releases for both x86 and arm processors on the Docker hub](https://hub.docker.com/r/jketterl/openwebrx).
|
||||
You can find a short introduction there.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
OpenWebRX currently requires Linux and python >= 3.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>*
|
||||
If you have trouble setting up or configuring your receiver, you have some great idea you want to see implemented, or
|
||||
you just generally want to have some OpenWebRX-related chat, come visit us over on
|
||||
[our groups.io group](https://groups.io/g/openwebrx).
|
||||
|
||||
## Usage tips
|
||||
|
||||
@ -83,7 +46,7 @@ However, if you hold down the shift key, you can drag the center line (BFO) or t
|
||||
## Licensing
|
||||
|
||||
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
|
||||
*<randras@sdr.hu>* for licensing options.
|
||||
|
100
bands.json
100
bands.json
@ -8,7 +8,8 @@
|
||||
"ft8": 1840000,
|
||||
"wspr": 1836600,
|
||||
"jt65": 1838000,
|
||||
"jt9": 1839000
|
||||
"jt9": 1839000,
|
||||
"js8": 1842000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -21,7 +22,8 @@
|
||||
"wspr": 3592600,
|
||||
"jt65": 3570000,
|
||||
"jt9": 3572000,
|
||||
"ft4": [3568000, 3575000]
|
||||
"ft4": [3568000, 3575000],
|
||||
"js8": 3578000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -43,7 +45,8 @@
|
||||
"wspr": 7038600,
|
||||
"jt65": 7076000,
|
||||
"jt9": 7078000,
|
||||
"ft4": 7047500
|
||||
"ft4": 7047500,
|
||||
"js8": 7078000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -56,7 +59,8 @@
|
||||
"wspr": 10138700,
|
||||
"jt65": 10138000,
|
||||
"jt9": 10140000,
|
||||
"ft4": 10140000
|
||||
"ft4": 10140000,
|
||||
"js8": 10130000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -69,7 +73,8 @@
|
||||
"wspr": 14095600,
|
||||
"jt65": 14076000,
|
||||
"jt9": 14078000,
|
||||
"ft4": 14080000
|
||||
"ft4": 14080000,
|
||||
"js8": 14078000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -82,7 +87,8 @@
|
||||
"wspr": 18104600,
|
||||
"jt65": 18102000,
|
||||
"jt9": 18104000,
|
||||
"ft4": 18104000
|
||||
"ft4": 18104000,
|
||||
"js8": 18104000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -95,7 +101,8 @@
|
||||
"wspr": 21094600,
|
||||
"jt65": 21076000,
|
||||
"jt9": 21078000,
|
||||
"ft4": 21140000
|
||||
"ft4": 21140000,
|
||||
"js8": 21078000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -108,7 +115,8 @@
|
||||
"wspr": 24924600,
|
||||
"jt65": 24917000,
|
||||
"jt9": 24919000,
|
||||
"ft4": 24919000
|
||||
"ft4": 24919000,
|
||||
"js8": 24922000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -121,7 +129,8 @@
|
||||
"wspr": 28124600,
|
||||
"jt65": 28076000,
|
||||
"jt9": 28078000,
|
||||
"ft4": 28180000
|
||||
"ft4": 28180000,
|
||||
"js8": 28078000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -134,7 +143,8 @@
|
||||
"wspr": 50293000,
|
||||
"jt65": 50310000,
|
||||
"jt9": 50312000,
|
||||
"ft4": 50318000
|
||||
"ft4": 50318000,
|
||||
"js8": 50318000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -189,5 +199,75 @@
|
||||
"name": "3cm",
|
||||
"lower_bound": 10000000000,
|
||||
"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
|
||||
set -euxo pipefail
|
||||
|
||||
ARCH=$(uname -m)
|
||||
|
||||
TAG="latest"
|
||||
ARCHTAG="$TAG-$ARCH"
|
||||
. docker/env
|
||||
|
||||
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 .
|
||||
@ -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-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-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 .
|
||||
|
119
config_webrx.py
119
config_webrx.py
@ -32,8 +32,11 @@ config_webrx: configuration options for 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:
|
||||
# https://github.com/simonyiszk/openwebrx/wiki
|
||||
# https://github.com/jketterl/openwebrx/wiki/Configuration-guide
|
||||
|
||||
# ==== Server settings ====
|
||||
web_port = 8073
|
||||
@ -44,24 +47,30 @@ receiver_name = "[Callsign]"
|
||||
receiver_location = "Budapest, Hungary"
|
||||
receiver_asl = 200
|
||||
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_desc allows you to put pretty much any HTML you like into the receiver description.
|
||||
# The lines below should give you some examples of what's possible.
|
||||
photo_desc = """
|
||||
You can add your own background photo and receiver information.<br />
|
||||
Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/>
|
||||
Device: %[RX_DEVICE]<br />
|
||||
Antenna: %[RX_ANT]<br />
|
||||
Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
|
||||
Device: Receiver Device<br />
|
||||
Antenna: Receiver Antenna<br />
|
||||
Website: <a href="http://localhost" target="_blank">http://localhost</a>
|
||||
"""
|
||||
|
||||
# ==== 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"
|
||||
# ==== Public receiver listings ====
|
||||
# You can publish your receiver on online receiver directories, like https://www.receiverbook.de
|
||||
# You will receive a receiver key from the directory that will authenticate you as the operator of this receiver.
|
||||
# Please note that you not share your receiver keys publicly since anyone that obtains your receiver key can take over
|
||||
# your public listing.
|
||||
# Your receiver keys should be placed into this array:
|
||||
receiver_keys = []
|
||||
# If you list your receiver on multiple sites, you can place all your keys into the array above, or you can append
|
||||
# keys to the arraylike this:
|
||||
# receiver_keys += ["my-receiver-key"]
|
||||
|
||||
# If you're not sure, simply copy & paste the code you received from your listing site below this line:
|
||||
|
||||
# ==== DSP/RX settings ====
|
||||
fft_fps = 9
|
||||
@ -93,13 +102,14 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
|
||||
# ==== I/Q sources ====
|
||||
# (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.)
|
||||
|
||||
#################################################################################################
|
||||
# Is my SDR hardware supported? #
|
||||
# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support #
|
||||
#################################################################################################
|
||||
###############################################################################
|
||||
# Is my SDR hardware supported? #
|
||||
# Check here: https://github.com/jketterl/openwebrx/wiki/Supported-Hardware #
|
||||
###############################################################################
|
||||
|
||||
# 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 sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the
|
||||
@ -107,8 +117,13 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
|
||||
#
|
||||
# https://github.com/jketterl/owrx_connector
|
||||
#
|
||||
# NOTE: The connector sources have replaced the old piped nmux style of reading input. If you still have any sdrs
|
||||
# configured that have type endin in "_connector", simply remove that suffix.
|
||||
# In order to use Perseus HF you need to install the libperseus-sdr
|
||||
#
|
||||
# https://github.com/Microtelecom/libperseus-sdr
|
||||
#
|
||||
# and do the proper changes to the sdrs object below
|
||||
# (see also Wiki in https://github.com/jketterl/openwebrx/wiki/Sample-configuration-for-Perseus-HF-receiver).
|
||||
#
|
||||
|
||||
sdrs = {
|
||||
"rtlsdr": {
|
||||
@ -141,19 +156,18 @@ sdrs = {
|
||||
"name": "Airspy HF+",
|
||||
"type": "airspyhf",
|
||||
"ppm": 0,
|
||||
"rf_gain": "auto",
|
||||
"profiles": {
|
||||
"20m": {
|
||||
"name": "20m",
|
||||
"center_freq": 14150000,
|
||||
"rf_gain": 10,
|
||||
"samp_rate": 768000,
|
||||
"samp_rate": 384000,
|
||||
"start_freq": 14070000,
|
||||
"start_mod": "usb",
|
||||
},
|
||||
"30m": {
|
||||
"name": "30m",
|
||||
"center_freq": 10125000,
|
||||
"rf_gain": 10,
|
||||
"samp_rate": 192000,
|
||||
"start_freq": 10142000,
|
||||
"start_mod": "usb",
|
||||
@ -161,24 +175,21 @@ sdrs = {
|
||||
"40m": {
|
||||
"name": "40m",
|
||||
"center_freq": 7100000,
|
||||
"rf_gain": 10,
|
||||
"samp_rate": 256000,
|
||||
"start_freq": 7070000,
|
||||
"start_mod": "usb",
|
||||
"start_mod": "lsb",
|
||||
},
|
||||
"80m": {
|
||||
"name": "80m",
|
||||
"center_freq": 3650000,
|
||||
"rf_gain": 10,
|
||||
"samp_rate": 768000,
|
||||
"samp_rate": 384000,
|
||||
"start_freq": 3570000,
|
||||
"start_mod": "usb",
|
||||
"start_mod": "lsb",
|
||||
},
|
||||
"49m": {
|
||||
"name": "49m Broadcast",
|
||||
"center_freq": 6000000,
|
||||
"rf_gain": 10,
|
||||
"samp_rate": 768000,
|
||||
"center_freq": 6050000,
|
||||
"samp_rate": 384000,
|
||||
"start_freq": 6070000,
|
||||
"start_mod": "am",
|
||||
},
|
||||
@ -188,6 +199,7 @@ sdrs = {
|
||||
"name": "SDRPlay RSP2",
|
||||
"type": "sdrplay",
|
||||
"ppm": 0,
|
||||
"antenna": "Antenna A",
|
||||
"profiles": {
|
||||
"20m": {
|
||||
"name": "20m",
|
||||
@ -196,7 +208,6 @@ sdrs = {
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 14070000,
|
||||
"start_mod": "usb",
|
||||
"antenna": "Antenna A",
|
||||
},
|
||||
"30m": {
|
||||
"name": "30m",
|
||||
@ -212,8 +223,7 @@ sdrs = {
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 7070000,
|
||||
"start_mod": "usb",
|
||||
"antenna": "Antenna A",
|
||||
"start_mod": "lsb",
|
||||
},
|
||||
"80m": {
|
||||
"name": "80m",
|
||||
@ -221,8 +231,7 @@ sdrs = {
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 3570000,
|
||||
"start_mod": "usb",
|
||||
"antenna": "Antenna A",
|
||||
"start_mod": "lsb",
|
||||
},
|
||||
"49m": {
|
||||
"name": "49m Broadcast",
|
||||
@ -231,7 +240,6 @@ sdrs = {
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 6070000,
|
||||
"start_mod": "am",
|
||||
"antenna": "Antenna A",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -245,21 +253,21 @@ sdrs = {
|
||||
waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF]
|
||||
waterfall_min_level = -88 # in dB
|
||||
waterfall_max_level = -20
|
||||
waterfall_auto_level_margin = (5, 40)
|
||||
waterfall_auto_level_margin = {"min": 5, "max": 40}
|
||||
### old theme by HA7ILM:
|
||||
# waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
|
||||
# waterfall_min_level = -115 #in dB
|
||||
# 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.
|
||||
|
||||
# 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_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]]
|
||||
# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin["min"]]
|
||||
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin["max"]]
|
||||
#
|
||||
# ___|____________________________________|____________________________________|____________________________________|___> signal power
|
||||
# \_waterfall_auto_level_margin[0]_/ |__ current_min_power_level | \_waterfall_auto_level_margin[1]_/
|
||||
# current_max_power_level __|
|
||||
# ___|________________________________________|____________________________________|________________________________________|___> signal power
|
||||
# \_waterfall_auto_level_margin["min"]_/ |__ current_min_power_level | \_waterfall_auto_level_margin["max"]_/
|
||||
# current_max_power_level __|
|
||||
|
||||
# === Experimental settings ===
|
||||
# Warning! The settings below are very experimental.
|
||||
@ -276,22 +284,28 @@ google_maps_api_key = ""
|
||||
# in seconds; default: 2 hours
|
||||
map_position_retention_time = 2 * 60 * 60
|
||||
|
||||
# wsjt decoder queue configuration
|
||||
# due to the nature of the wsjt operating modes (ft8, ft8, jt9, jt65 and wspr), the data is recorded for a given amount
|
||||
# of time (6.5 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
|
||||
# decoder queue configuration
|
||||
# due to the nature of some operating modes (ft8, ft8, jt9, jt65, wspr and js8), the data is recorded for a given amount
|
||||
# of time (6 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
|
||||
# to mitigate this, the recordings will be queued and processed in sequence.
|
||||
# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
|
||||
wsjt_queue_workers = 2
|
||||
decoding_queue_workers = 2
|
||||
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up
|
||||
# if you are running background services, make sure this number is high enough to accept the task influx during peaks
|
||||
# i.e. this should be higher than the number of wsjt services running at the same time
|
||||
wsjt_queue_length = 10
|
||||
# i.e. this should be higher than the number of decoding services running at the same time
|
||||
decoding_queue_length = 10
|
||||
|
||||
# wsjt decoding depth will allow more results, but will also consume more cpu
|
||||
wsjt_decoding_depth = 3
|
||||
# can also be set for each mode separately
|
||||
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
|
||||
wsjt_decoding_depths = {"jt65": 1}
|
||||
|
||||
# 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"
|
||||
|
||||
services_enabled = False
|
||||
@ -314,3 +328,8 @@ aprs_symbols_path = "/opt/aprs-symbols/png"
|
||||
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator
|
||||
pskreporter_enabled = False
|
||||
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
|
||||
|
431
csdr/csdr.py
431
csdr/csdr.py
@ -29,7 +29,9 @@ import math
|
||||
from functools import partial
|
||||
|
||||
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
|
||||
|
||||
@ -64,12 +66,105 @@ class output(object):
|
||||
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):
|
||||
def __init__(self, output):
|
||||
self.samp_rate = 250000
|
||||
self.output_rate = 11025
|
||||
self.fft_size = 1024
|
||||
self.fft_fps = 5
|
||||
self.center_freq = 0
|
||||
self.offset_freq = 0
|
||||
self.low_cut = -4000
|
||||
self.high_cut = 4000
|
||||
@ -82,6 +177,8 @@ class dsp(object):
|
||||
self.demodulator = "nfm"
|
||||
self.name = "csdr"
|
||||
self.base_bufsize = 512
|
||||
self.decimation = None
|
||||
self.last_decimation = None
|
||||
self.nc_port = None
|
||||
self.csdr_dynamic_bufsize = False
|
||||
self.csdr_print_bufsizes = False
|
||||
@ -94,31 +191,38 @@ class dsp(object):
|
||||
self.secondary_fft_size = 1024
|
||||
self.secondary_process_fft = None
|
||||
self.secondary_process_demod = None
|
||||
self.pipe_names = [
|
||||
"bpf_pipe",
|
||||
"shift_pipe",
|
||||
"squelch_pipe",
|
||||
"smeter_pipe",
|
||||
"meta_pipe",
|
||||
"iqtee_pipe",
|
||||
"iqtee2_pipe",
|
||||
"dmr_control_pipe",
|
||||
]
|
||||
self.secondary_pipe_names = ["secondary_shift_pipe"]
|
||||
self.pipe_names = {
|
||||
"bpf_pipe": Pipe.WRITE,
|
||||
"shift_pipe": Pipe.WRITE,
|
||||
"squelch_pipe": Pipe.WRITE,
|
||||
"smeter_pipe": Pipe.READ,
|
||||
"meta_pipe": Pipe.READ,
|
||||
"iqtee_pipe": Pipe.NONE,
|
||||
"iqtee2_pipe": Pipe.NONE,
|
||||
"dmr_control_pipe": Pipe.WRITE,
|
||||
}
|
||||
self.pipes = {}
|
||||
self.secondary_pipe_names = {"secondary_shift_pipe": Pipe.WRITE}
|
||||
self.secondary_offset_freq = 1000
|
||||
self.unvoiced_quality = 1
|
||||
self.modification_lock = threading.Lock()
|
||||
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.direwolf_config = None
|
||||
self.direwolf_port = None
|
||||
self.process = None
|
||||
|
||||
def set_service(self, flag=True):
|
||||
self.is_service = flag
|
||||
|
||||
def set_temporary_directory(self, 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):
|
||||
chain = ["nc -v 127.0.0.1 {nc_port}"]
|
||||
@ -137,7 +241,7 @@ class dsp(object):
|
||||
if self.fft_compression == "adpcm":
|
||||
chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"]
|
||||
return chain
|
||||
chain += ["csdr shift_addition_cc --fifo {shift_pipe}"]
|
||||
chain += ["csdr shift_addfast_cc --fifo {shift_pipe}"]
|
||||
if self.decimation > 1:
|
||||
chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"]
|
||||
chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"]
|
||||
@ -227,14 +331,14 @@ class dsp(object):
|
||||
return chain
|
||||
elif which == "bpsk31" or which == "bpsk63":
|
||||
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 simple_agc_cc 0.001 0.5",
|
||||
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q",
|
||||
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8",
|
||||
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8",
|
||||
]
|
||||
elif self.isWsjtMode(which):
|
||||
elif self.isWsjtMode(which) or self.isJs8(which):
|
||||
chain += ["csdr realpart_cf"]
|
||||
if self.last_decimation != 1.0:
|
||||
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
||||
@ -243,7 +347,7 @@ class dsp(object):
|
||||
chain += ["csdr fmdemod_quadri_cf"]
|
||||
if self.last_decimation != 1.0:
|
||||
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":
|
||||
chain += ["csdr fmdemod_quadri_cf"]
|
||||
if self.last_decimation != 1.0:
|
||||
@ -301,8 +405,8 @@ class dsp(object):
|
||||
self.try_create_configs(secondary_command_demod)
|
||||
|
||||
secondary_command_demod = secondary_command_demod.format(
|
||||
input_pipe=self.iqtee2_pipe,
|
||||
secondary_shift_pipe=self.secondary_shift_pipe,
|
||||
input_pipe=self.pipes["iqtee2_pipe"],
|
||||
secondary_shift_pipe=self.pipes["secondary_shift_pipe"],
|
||||
secondary_decimation=self.secondary_decimation(),
|
||||
secondary_samples_per_bits=self.secondary_samples_per_bits(),
|
||||
secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
|
||||
@ -321,7 +425,7 @@ class dsp(object):
|
||||
if self.output.supports_type("secondary_fft"):
|
||||
secondary_command_fft = " | ".join(self.secondary_chain("fft"))
|
||||
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_size=self.secondary_fft_size,
|
||||
secondary_fft_block_size=self.secondary_fft_block_size(),
|
||||
@ -347,18 +451,25 @@ class dsp(object):
|
||||
|
||||
if self.isWsjtMode():
|
||||
smd = self.get_secondary_demodulator()
|
||||
chopper_profile = None
|
||||
if smd == "ft8":
|
||||
chopper = Ft8Chopper(self.secondary_process_demod.stdout)
|
||||
chopper_profile = Ft8Profile()
|
||||
elif smd == "wspr":
|
||||
chopper = WsprChopper(self.secondary_process_demod.stdout)
|
||||
chopper_profile = WsprProfile()
|
||||
elif smd == "jt65":
|
||||
chopper = Jt65Chopper(self.secondary_process_demod.stdout)
|
||||
chopper_profile = Jt65Profile()
|
||||
elif smd == "jt9":
|
||||
chopper = Jt9Chopper(self.secondary_process_demod.stdout)
|
||||
chopper_profile = Jt9Profile()
|
||||
elif smd == "ft4":
|
||||
chopper = Ft4Chopper(self.secondary_process_demod.stdout)
|
||||
chopper_profile = Ft4Profile()
|
||||
if chopper_profile is not None:
|
||||
chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile)
|
||||
chopper.start()
|
||||
self.output.send_output("wsjt_demod", chopper.read)
|
||||
elif self.isJs8():
|
||||
chopper = AudioChopper(self, self.secondary_process_demod.stdout, *Js8Profiles.getEnabledProfiles())
|
||||
chopper.start()
|
||||
self.output.send_output("wsjt_demod", chopper.read)
|
||||
self.output.send_output("js8_demod", chopper.read)
|
||||
elif self.isPacket():
|
||||
# we best get the ax25 packets from the kiss socket
|
||||
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))
|
||||
|
||||
# open control pipes for csdr and send initialization data
|
||||
if self.secondary_shift_pipe != None: # TODO digimodes
|
||||
self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w") # TODO digimodes
|
||||
if self.has_pipe("secondary_shift_pipe"): # TODO digimodes
|
||||
self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes
|
||||
|
||||
def set_secondary_offset_freq(self, value):
|
||||
self.secondary_offset_freq = value
|
||||
if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"):
|
||||
self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
|
||||
self.secondary_shift_pipe_file.flush()
|
||||
if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"):
|
||||
self.pipes["secondary_shift_pipe"].write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
|
||||
|
||||
def stop_secondary_demodulator(self):
|
||||
if self.secondary_processes_running == False:
|
||||
if not self.secondary_processes_running:
|
||||
return
|
||||
self.try_delete_pipes(self.secondary_pipe_names)
|
||||
self.try_delete_configs()
|
||||
@ -447,7 +556,7 @@ class dsp(object):
|
||||
decimation += 1
|
||||
fraction = float(input_rate / decimation) / output_rate
|
||||
intermediate_rate = input_rate / decimation
|
||||
return (decimation, fraction, intermediate_rate)
|
||||
return decimation, fraction, intermediate_rate
|
||||
|
||||
def if_samp_rate(self):
|
||||
return self.samp_rate / self.decimation
|
||||
@ -461,7 +570,7 @@ class dsp(object):
|
||||
def get_audio_rate(self):
|
||||
if self.isDigitalVoice() or self.isPacket() or self.isPocsag():
|
||||
return 48000
|
||||
elif self.isWsjtMode():
|
||||
elif self.isWsjtMode() or self.isJs8():
|
||||
return 12000
|
||||
return self.get_output_rate()
|
||||
|
||||
@ -475,6 +584,11 @@ class dsp(object):
|
||||
demodulator = self.get_secondary_demodulator()
|
||||
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):
|
||||
if demodulator is None:
|
||||
demodulator = self.get_secondary_demodulator()
|
||||
@ -493,6 +607,8 @@ class dsp(object):
|
||||
self.restart()
|
||||
|
||||
def set_demodulator(self, demodulator):
|
||||
if demodulator in ["usb", "lsb", "cw"]:
|
||||
demodulator = "ssb"
|
||||
if self.demodulator == demodulator:
|
||||
return
|
||||
self.demodulator = demodulator
|
||||
@ -523,21 +639,22 @@ class dsp(object):
|
||||
def set_offset_freq(self, offset_freq):
|
||||
self.offset_freq = offset_freq
|
||||
if self.running:
|
||||
self.modification_lock.acquire()
|
||||
self.shift_pipe_file.write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
|
||||
self.shift_pipe_file.flush()
|
||||
self.modification_lock.release()
|
||||
self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
|
||||
|
||||
def set_center_freq(self, center_freq):
|
||||
# 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):
|
||||
self.low_cut = low_cut
|
||||
self.high_cut = high_cut
|
||||
if self.running:
|
||||
self.modification_lock.acquire()
|
||||
self.bpf_pipe_file.write(
|
||||
self.pipes["bpf_pipe"].write(
|
||||
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
|
||||
)
|
||||
self.bpf_pipe_file.flush()
|
||||
self.modification_lock.release()
|
||||
|
||||
def get_bpf(self):
|
||||
return [self.low_cut, self.high_cut]
|
||||
@ -550,10 +667,7 @@ class dsp(object):
|
||||
# no squelch required on digital voice modes
|
||||
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() else self.squelch_level
|
||||
if self.running:
|
||||
self.modification_lock.acquire()
|
||||
self.squelch_pipe_file.write("%g\n" % (self.convertToLinear(actual_squelch)))
|
||||
self.squelch_pipe_file.flush()
|
||||
self.modification_lock.release()
|
||||
self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
|
||||
|
||||
def set_unvoiced_quality(self, q):
|
||||
self.unvoiced_quality = q
|
||||
@ -563,39 +677,33 @@ class dsp(object):
|
||||
return self.unvoiced_quality
|
||||
|
||||
def set_dmr_filter(self, filter):
|
||||
if self.dmr_control_pipe_file:
|
||||
self.dmr_control_pipe_file.write("{0}\n".format(filter))
|
||||
self.dmr_control_pipe_file.flush()
|
||||
|
||||
def mkfifo(self, path):
|
||||
try:
|
||||
os.unlink(path)
|
||||
except:
|
||||
pass
|
||||
os.mkfifo(path)
|
||||
if self.has_pipe("dmr_control_pipe"):
|
||||
self.pipes["dmr_control_pipe"].write("{0}\n".format(filter))
|
||||
|
||||
def ddc_transition_bw(self):
|
||||
return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate))
|
||||
|
||||
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:
|
||||
setattr(self, pipe_name, self.pipe_base_path + pipe_name)
|
||||
self.mkfifo(getattr(self, pipe_name))
|
||||
p = self.pipe_base_path + 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:
|
||||
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):
|
||||
for pipe_name in pipe_names:
|
||||
pipe_path = getattr(self, pipe_name, None)
|
||||
if pipe_path:
|
||||
try:
|
||||
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()")
|
||||
if self.has_pipe(pipe_name):
|
||||
self.pipes[pipe_name].close()
|
||||
self.pipes[pipe_name] = None
|
||||
|
||||
def try_create_configs(self, command):
|
||||
if "{direwolf_config}" in command:
|
||||
@ -622,108 +730,94 @@ class dsp(object):
|
||||
self.direwolf_config = None
|
||||
|
||||
def start(self):
|
||||
self.modification_lock.acquire()
|
||||
if self.running:
|
||||
self.modification_lock.release()
|
||||
return
|
||||
self.running = True
|
||||
with self.modification_lock:
|
||||
if self.running:
|
||||
return
|
||||
self.running = True
|
||||
|
||||
command_base = " | ".join(self.chain(self.demodulator))
|
||||
command_base = " | ".join(self.chain(self.demodulator))
|
||||
|
||||
# create control pipes for csdr
|
||||
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self))
|
||||
# create control pipes for csdr
|
||||
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
|
||||
command = command_base.format(
|
||||
bpf_pipe=self.bpf_pipe,
|
||||
shift_pipe=self.shift_pipe,
|
||||
decimation=self.decimation,
|
||||
last_decimation=self.last_decimation,
|
||||
fft_size=self.fft_size,
|
||||
fft_block_size=self.fft_block_size(),
|
||||
fft_averages=self.fft_averages,
|
||||
bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(),
|
||||
ddc_transition_bw=self.ddc_transition_bw(),
|
||||
flowcontrol=int(self.samp_rate * 2),
|
||||
start_bufsize=self.base_bufsize * self.decimation,
|
||||
nc_port=self.nc_port,
|
||||
squelch_pipe=self.squelch_pipe,
|
||||
smeter_pipe=self.smeter_pipe,
|
||||
meta_pipe=self.meta_pipe,
|
||||
iqtee_pipe=self.iqtee_pipe,
|
||||
iqtee2_pipe=self.iqtee2_pipe,
|
||||
output_rate=self.get_output_rate(),
|
||||
smeter_report_every=int(self.if_samp_rate() / 6000),
|
||||
unvoiced_quality=self.get_unvoiced_quality(),
|
||||
dmr_control_pipe=self.dmr_control_pipe,
|
||||
audio_rate=self.get_audio_rate(),
|
||||
)
|
||||
|
||||
logger.debug("Command = %s", command)
|
||||
my_env = os.environ.copy()
|
||||
if self.csdr_dynamic_bufsize:
|
||||
my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
|
||||
if self.csdr_print_bufsizes:
|
||||
my_env["CSDR_PRINT_BUFSIZES"] = "1"
|
||||
|
||||
out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
|
||||
self.process = subprocess.Popen(command, stdout=out, shell=True, start_new_session=True, env=my_env)
|
||||
|
||||
def watch_thread():
|
||||
rc = self.process.wait()
|
||||
logger.debug("dsp thread ended with rc=%d", rc)
|
||||
if rc == 0 and self.running and not self.modification_lock.locked():
|
||||
logger.debug("restarting since rc = 0, self.running = true, and no modification")
|
||||
self.restart()
|
||||
|
||||
threading.Thread(target=watch_thread).start()
|
||||
|
||||
if self.output.supports_type("audio"):
|
||||
self.output.send_output(
|
||||
"audio",
|
||||
partial(
|
||||
self.process.stdout.read,
|
||||
self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(),
|
||||
),
|
||||
# run the command
|
||||
command = command_base.format(
|
||||
bpf_pipe=self.pipes["bpf_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,
|
||||
last_decimation=self.last_decimation,
|
||||
fft_size=self.fft_size,
|
||||
fft_block_size=self.fft_block_size(),
|
||||
fft_averages=self.fft_averages,
|
||||
bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(),
|
||||
ddc_transition_bw=self.ddc_transition_bw(),
|
||||
flowcontrol=int(self.samp_rate * 2),
|
||||
start_bufsize=self.base_bufsize * self.decimation,
|
||||
nc_port=self.nc_port,
|
||||
output_rate=self.get_output_rate(),
|
||||
smeter_report_every=int(self.if_samp_rate() / 6000),
|
||||
unvoiced_quality=self.get_unvoiced_quality(),
|
||||
audio_rate=self.get_audio_rate(),
|
||||
)
|
||||
|
||||
# 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()
|
||||
logger.debug("Command = %s", command)
|
||||
my_env = os.environ.copy()
|
||||
if self.csdr_dynamic_bufsize:
|
||||
my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
|
||||
if self.csdr_print_bufsizes:
|
||||
my_env["CSDR_PRINT_BUFSIZES"] = "1"
|
||||
|
||||
self.modification_lock.release()
|
||||
out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
|
||||
self.process = subprocess.Popen(command, stdout=out, shell=True, start_new_session=True, env=my_env)
|
||||
|
||||
# send initial config through the pipes
|
||||
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 watch_thread():
|
||||
rc = self.process.wait()
|
||||
logger.debug("dsp thread ended with rc=%d", rc)
|
||||
if rc == 0 and self.running and not self.modification_lock.locked():
|
||||
logger.debug("restarting since rc = 0, self.running = true, and no modification")
|
||||
self.restart()
|
||||
|
||||
threading.Thread(target=watch_thread).start()
|
||||
|
||||
if self.output.supports_type("audio"):
|
||||
self.output.send_output(
|
||||
"audio",
|
||||
partial(
|
||||
self.process.stdout.read,
|
||||
self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(),
|
||||
),
|
||||
)
|
||||
|
||||
self.start_secondary_demodulator()
|
||||
|
||||
if self.has_pipe("smeter_pipe"):
|
||||
def read_smeter():
|
||||
raw = self.smeter_pipe_file.readline()
|
||||
raw = self.pipes["smeter_pipe"].readline()
|
||||
if len(raw) == 0:
|
||||
return None
|
||||
else:
|
||||
return float(raw.rstrip("\n"))
|
||||
|
||||
self.output.send_output("smeter", read_smeter)
|
||||
if self.meta_pipe != None:
|
||||
# TODO make digiham output unicode and then change this here
|
||||
self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437")
|
||||
|
||||
if self.has_pipe("meta_pipe"):
|
||||
def read_meta():
|
||||
raw = self.meta_pipe_file.readline()
|
||||
raw = self.pipes["meta_pipe"].readline()
|
||||
if len(raw) == 0:
|
||||
return None
|
||||
else:
|
||||
@ -731,23 +825,23 @@ class dsp(object):
|
||||
|
||||
self.output.send_output("meta", read_meta)
|
||||
|
||||
if self.dmr_control_pipe:
|
||||
self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w")
|
||||
if self.csdr_dynamic_bufsize:
|
||||
self.process.stdout.read(8) # dummy read to skip bufsize & preamble
|
||||
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
|
||||
|
||||
def stop(self):
|
||||
self.modification_lock.acquire()
|
||||
self.running = False
|
||||
if hasattr(self, "process"):
|
||||
try:
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
# been killed by something else, ignore
|
||||
pass
|
||||
self.stop_secondary_demodulator()
|
||||
with self.modification_lock:
|
||||
self.running = False
|
||||
if self.process is not None:
|
||||
try:
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
||||
self.process = None
|
||||
except ProcessLookupError:
|
||||
# been killed by something else, ignore
|
||||
pass
|
||||
self.stop_secondary_demodulator()
|
||||
|
||||
self.try_delete_pipes(self.pipe_names)
|
||||
|
||||
self.modification_lock.release()
|
||||
self.try_delete_pipes(self.pipe_names)
|
||||
|
||||
def restart(self):
|
||||
if not self.running:
|
||||
@ -757,4 +851,3 @@ class dsp(object):
|
||||
|
||||
def __del__(self):
|
||||
self.stop()
|
||||
del self.process
|
||||
|
136
debian/changelog
vendored
136
debian/changelog
vendored
@ -1,5 +1,135 @@
|
||||
openwebrx (0.18) UNRELEASED; urgency=low
|
||||
openwebrx (0.19.1) buster focal; urgency=low
|
||||
|
||||
* Initial release.
|
||||
* Added ability to authenticate receivers with listing sites using
|
||||
"receiver id" tokens
|
||||
|
||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 08 Dec 2019 12:35:48 +0000
|
||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sat, 13 Jun 2020 16:46:00 +0000
|
||||
|
||||
openwebrx (0.19.0) buster focal; urgency=low
|
||||
* Fix direwolf connection setup by implementing a retry loop
|
||||
* Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector
|
||||
* OSM maps instead of Google when google_maps_api_key is not set (thanks
|
||||
@jquagga)
|
||||
* Improved logic to pass parameters to soapy devices.
|
||||
- `rtl_sdr_soapy`: added support for `bias_tee`
|
||||
- `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch`
|
||||
- `airspy`: added support for `bitpack`
|
||||
* Added support for Perseus-SDR devices, (thanks @amontefusco)
|
||||
* Property System has been rewritten so that defaults on sdr behave as
|
||||
expected
|
||||
* Waterfall range auto-adjustment now only takes the center 80% of the
|
||||
spectrum into account, which should work better with SDRs that oversample
|
||||
or have rather flat filter curves towards the spectrum edges
|
||||
* Bugfix for negative network usage
|
||||
* FiFi SDR: prevent arecord from shutting down after 2GB of data has been
|
||||
sent
|
||||
* Added support for bias tee control on rtl_sdr devices
|
||||
* All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
|
||||
* `rtl_sdr` type now also supports the `direct_sampling` option
|
||||
* Added decoding implementation for for digimode "JS8Call" (requires an
|
||||
installation of js8call and the js8py library)
|
||||
* Reorganization of the frontend demodulator code
|
||||
* Improve receiver load time by concatenating javascript assets
|
||||
* HackRF support is now based on SoapyHackRF
|
||||
* Removed sdr.hu server listing support since the site has been shut down
|
||||
* Added support for Radioberry 2 Rasbperry Pi SDR Cape
|
||||
|
||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Mon, 01 Jun 2020 17:02:00 +0000
|
||||
|
||||
openwebrx (0.18.0) buster; urgency=low
|
||||
|
||||
* Compression, resampling and filtering in the frontend have been rewritten
|
||||
in javascript, sdr.js has been removed
|
||||
* Decoding of Pocsag modulation is now possible
|
||||
* Removed the 3D waterfall since it had no real application and required ~1MB
|
||||
of javascript code to be downloaded
|
||||
* Improved the frontend handling of the "too many users" scenario
|
||||
* PSK63 digimode is now available (same decoding pipeline as PSK31, but with
|
||||
adopted parameters)
|
||||
* The frequency can now be manipulated with the mousewheel, which should
|
||||
allow the user to tune more precise. The tuning step size is determined by
|
||||
the digit the mouse cursor is hovering over.
|
||||
* Clicking on the frequency now opens an input for direct frequency selection
|
||||
* URL hashes have been fixed and improved: They are now updated
|
||||
automatically, so a shared URL will include frequency and demodulator,
|
||||
which allows for improved sharing and linking.
|
||||
* New daylight scheduler for background decoding, allows profiles to be
|
||||
selected by local sunrise / sunset times
|
||||
* The owrx_connector is now the default way of communicating with sdr
|
||||
devices. The old sdr types have been replaced, all `_connector` suffixes on
|
||||
the type must be removed!
|
||||
* The sources have been refactored, making it a lot easier to add support for
|
||||
other devices
|
||||
* SDR device failure handling has been improved, including user feedback
|
||||
* New devices supported:
|
||||
* wsjt-x updated to 2.1.2
|
||||
* The rtl_tcp compatibility mode of the owrx_connector is now configurable
|
||||
using the `rtltcp_compat` flag
|
||||
* explicit device filter for soapy devices for multi-device setups
|
||||
* compatibility fixes for safari browsers (ios and mac)
|
||||
* Offset tuning using the `lfo_offset` has been reworked in a way that
|
||||
`center_freq` has to be set to the frequency you actually want to listen
|
||||
to. If you're using an `lfo_offset` already, you will probably need to
|
||||
change its sign.
|
||||
* `initial_squelch_level` can now be set on each profile.
|
||||
* Part of the frontend code has been reworked
|
||||
- Audio buffer minimums have been completely stripped. As a result, you
|
||||
should get better latency. Unfortunately, this also means there will be
|
||||
some skipping when audio starts.
|
||||
- Now also supports AudioWorklets (for those browser that have it).
|
||||
- Mousewheel controls for the receiver sliders
|
||||
* Error handling for failed SDR devices
|
||||
* One of the most-requested features is finally coming to OpenWebRX:
|
||||
Bookmarks (sometimes also referred to as labels).
|
||||
There's two kinds of bookmarks available:
|
||||
- Serverside bookmarks that are set up by the receiver administrator.
|
||||
Check the file `bookmarks.json` for examples!
|
||||
- Clientside bookmarks which every user can store for themselves. They are
|
||||
stored in the browser's localStorage.
|
||||
* Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is
|
||||
now possible. Please have a look at the configuration on how to set it up.
|
||||
* Websocket communication has been overhauled in large parts. It should now
|
||||
be more reliable, and failing connections should now have no impact on
|
||||
other users.
|
||||
* Profile scheduling allows to set up band-hopping if you are running
|
||||
background services.
|
||||
* APRS now has the ability to show symbols on the map, if a corresponding
|
||||
symbol set has been installed. Check the config!
|
||||
* Debug logging has been disabled in a handful of modules, expect vastly
|
||||
reduced output on the shell.
|
||||
* New set of APRS-related features
|
||||
- Decode Packet transmissions using direwolf (1k2 only for now)
|
||||
- APRS packets are mostly decoded and shown both in a new panel and on the
|
||||
map
|
||||
- APRS is also available as a background service
|
||||
- direwolfs I-gate functionality can be enabled, which allows your receiver
|
||||
to work as a receive-only I-gate for the APRS network in the background
|
||||
* Demodulation for background services has been optimized to use less total
|
||||
bandwidth, saving CPU
|
||||
* More metrics have been added; they can be used together with collectd and
|
||||
its curl_json plugin for now, with some limitations.
|
||||
* New bandplan feature, the first thing visible is the "dial" indicator that
|
||||
brings you right to the dial frequency for digital modes
|
||||
* fixed some bugs in the websocket communication which broke the map
|
||||
* WSJT-X integration (FT8, FT4, WSPR, JT65, JT9 using wsjt-x demodulators)
|
||||
* New Map Feature that shows both decoded grid squares from FT8 and Locations
|
||||
decoded from YSF digital voice
|
||||
* New Feature report that will show what functionality is available
|
||||
* major rework on the openwebrx core
|
||||
* Support of multiple SDR devices simultaneously
|
||||
* Support for multiple profiles per SDR that allow the user to listen to
|
||||
different frequencies
|
||||
* Support for digital voice decoding
|
||||
* Feature detection that will disable functionality when dependencies are not
|
||||
available (if you're missing the digital
|
||||
buttons, this is probably why)
|
||||
* Support added for the following SDR sources:
|
||||
- LimeSDR (`"type": "lime_sdr"`)
|
||||
- PlutoSDR (`"type": "pluto_sdr"`)
|
||||
- RTL_SDR via Soapy (`"type": "rtl_sdr_soapy"`) on special request to allow
|
||||
use of the direct sampling mode
|
||||
- SoapyRemote (`"type": "soapy_remote"`)
|
||||
- FiFiSDR (`"type": "fifi_sdr"`)
|
||||
- airspyhf devices (Airspy HF+ / Discovery) (`"type": "airspyhf"`)
|
||||
|
||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 18 Feb 2020 20:09:00 +0000
|
||||
|
9
debian/control
vendored
9
debian/control
vendored
@ -3,11 +3,14 @@ Maintainer: Jakob Ketterl <jakob.ketterl@gmx.de>
|
||||
Section: hamradio
|
||||
Priority: optional
|
||||
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
|
||||
Architecture: all
|
||||
Depends: python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.1), ${python3:Depends}
|
||||
Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx
|
||||
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, soapysdr-tools
|
||||
Description: multi-user web sdr
|
||||
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/
|
||||
bands.json etc/openwebrx/
|
||||
bookmarks.json etc/openwebrx/
|
||||
users.json etc/openwebrx/
|
||||
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
|
||||
|
||||
ADD docker/scripts/install-dependencies-airspy.sh /
|
||||
RUN /install-dependencies-airspy.sh
|
||||
RUN rm /install-dependencies-airspy.sh
|
||||
RUN /install-dependencies-airspy.sh &&\
|
||||
rm /install-dependencies-airspy.sh
|
||||
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
RUN /install-connectors.sh
|
||||
RUN rm /install-connectors.sh
|
||||
ADD . /opt/openwebrx
|
||||
|
@ -1,19 +1,18 @@
|
||||
FROM alpine:3.10
|
||||
FROM debian:buster-slim
|
||||
|
||||
RUN apk add --no-cache bash
|
||||
|
||||
RUN ln -s /usr/local/lib /usr/local/lib64
|
||||
|
||||
ADD docker/scripts/direwolf-1.5.patch /
|
||||
ADD docker/files/js8call/js8call-hamlib.patch /
|
||||
ADD docker/files/wsjtx/*.patch /
|
||||
ADD docker/scripts/install-dependencies.sh /
|
||||
RUN /install-dependencies.sh
|
||||
RUN rm /install-dependencies.sh
|
||||
RUN /install-dependencies.sh && \
|
||||
rm /install-dependencies.sh && \
|
||||
rm /*.patch
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
ENTRYPOINT ["/init"]
|
||||
|
||||
WORKDIR /opt/openwebrx
|
||||
|
||||
VOLUME /etc/openwebrx
|
||||
|
||||
ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ]
|
||||
CMD [ "/opt/openwebrx/docker/scripts/run.sh" ]
|
||||
|
||||
EXPOSE 8073
|
||||
|
@ -2,16 +2,25 @@ ARG ARCHTAG
|
||||
FROM openwebrx-base:$ARCHTAG
|
||||
|
||||
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-hackrf.sh
|
||||
RUN /install-dependencies-soapysdr.sh
|
||||
RUN /install-dependencies-sdrplay.sh
|
||||
RUN /install-dependencies-airspy.sh
|
||||
RUN /install-dependencies-rtlsdr-soapy.sh
|
||||
RUN rm /install-dependencies-*.sh
|
||||
RUN /install-dependencies-rtlsdr.sh &&\
|
||||
/install-dependencies-soapysdr.sh &&\
|
||||
/install-dependencies-hackrf.sh &&\
|
||||
/install-dependencies-sdrplay.sh &&\
|
||||
/install-dependencies-airspy.sh &&\
|
||||
/install-dependencies-rtlsdr-soapy.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 /
|
||||
RUN /install-connectors.sh
|
||||
RUN rm /install-connectors.sh
|
||||
RUN /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
|
||||
FROM openwebrx-base:$ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-hackrf.sh /
|
||||
RUN /install-dependencies-hackrf.sh
|
||||
RUN rm /install-dependencies-hackrf.sh
|
||||
RUN /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
|
||||
|
||||
ADD docker/scripts/install-dependencies-rtlsdr.sh /
|
||||
RUN /install-dependencies-rtlsdr.sh
|
||||
RUN rm /install-dependencies-rtlsdr.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
|
||||
|
||||
ADD docker/scripts/install-dependencies-rtlsdr-soapy.sh /
|
||||
RUN /install-dependencies-rtlsdr-soapy.sh
|
||||
RUN rm /install-dependencies-rtlsdr-soapy.sh
|
||||
RUN /install-dependencies-rtlsdr-soapy.sh &&\
|
||||
rm /install-dependencies-rtlsdr-soapy.sh
|
||||
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
RUN /install-connectors.sh
|
||||
RUN rm /install-connectors.sh
|
||||
ADD . /opt/openwebrx
|
||||
|
@ -2,10 +2,11 @@ ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
ADD docker/scripts/install-dependencies-sdrplay.sh /
|
||||
ADD docker/scripts/install-lib.*.patch /
|
||||
RUN /install-dependencies-sdrplay.sh
|
||||
RUN rm /install-dependencies-sdrplay.sh
|
||||
ADD docker/files/sdrplay/install-lib.*.patch /
|
||||
RUN /install-dependencies-sdrplay.sh &&\
|
||||
rm /install-dependencies-sdrplay.sh &&\
|
||||
rm /install-lib.*.patch
|
||||
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
RUN /install-connectors.sh
|
||||
RUN rm /install-connectors.sh
|
||||
ADD docker/files/services/sdrplay /etc/services.d/sdrplay
|
||||
|
||||
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
|
||||
|
||||
ADD docker/scripts/install-dependencies-soapysdr.sh /
|
||||
RUN /install-dependencies-soapysdr.sh
|
||||
RUN rm /install-dependencies-soapysdr.sh
|
||||
|
||||
ADD docker/scripts/install-connectors.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
|
||||
set -euxo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
@ -17,12 +18,14 @@ function cmakebuild() {
|
||||
|
||||
cd /tmp
|
||||
|
||||
BUILD_PACKAGES="git cmake make gcc g++ musl-dev"
|
||||
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
BUILD_PACKAGES="git cmake make gcc g++"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $BUILD_PACKAGES
|
||||
|
||||
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
|
||||
set -euxo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
@ -17,22 +18,24 @@ function cmakebuild() {
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb"
|
||||
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
|
||||
STATIC_PACKAGES="libusb-1.0-0"
|
||||
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/airspy/airspyone_host.git
|
||||
cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3
|
||||
|
||||
git clone https://github.com/pothosware/SoapyAirspy.git
|
||||
cmakebuild SoapyAirspy 99756be5c3413a2d447baf70cb5a880662452655
|
||||
cmakebuild SoapyAirspy 10d697b209e7f1acc8b2c8d24851d46170ef77e3
|
||||
|
||||
git clone https://github.com/airspy/airspyhf.git
|
||||
cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475
|
||||
|
||||
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
|
||||
set -euxo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
@ -17,17 +18,22 @@ function cmakebuild() {
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb fftw udev"
|
||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev"
|
||||
STATIC_PACKAGES="libusb-1.0-0 libfftw3-3 udev"
|
||||
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
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/mossmann/hackrf.git
|
||||
cd hackrf
|
||||
git checkout 06eb9192cd348083f5f7de9c0da9ead276020011
|
||||
git checkout 43e6f99fe8543094d18ff3a6550ed2066c398862
|
||||
cmakebuild host
|
||||
cd ..
|
||||
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
|
||||
set -euo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
@ -17,16 +18,18 @@ function cmakebuild() {
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb"
|
||||
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
|
||||
STATIC_PACKAGES="libusb-1.0-0"
|
||||
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
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
|
||||
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
|
||||
set -euxo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
@ -17,13 +18,15 @@ function cmakebuild() {
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb"
|
||||
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
|
||||
STATIC_PACKAGES="libusb-1.0.0"
|
||||
BUILD_PACKAGES="git libusb-1.0.0-dev cmake make gcc g++ pkg-config"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
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
|
||||
set -euxo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
@ -17,23 +18,23 @@ function cmakebuild() {
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb udev"
|
||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev"
|
||||
STATIC_PACKAGES="libusb-1.0.0 udev"
|
||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case $ARCH in
|
||||
x86_64)
|
||||
BINARY=SDRplay_RSP_API-Linux-2.13.1.run
|
||||
BINARY=SDRplay_RSP_API-Linux-3.07.1.run
|
||||
;;
|
||||
armv*)
|
||||
BINARY=SDRplay_RSP_API-RPi-2.13.1.run
|
||||
BINARY=SDRplay_RSP_API-ARM32-3.07.2.run
|
||||
;;
|
||||
aarch64)
|
||||
BINARY=SDRplay_RSP_API-ARM64-2.13.1.run
|
||||
BINARY=SDRplay_RSP_API-ARM64-3.07.1.run
|
||||
;;
|
||||
esac
|
||||
|
||||
@ -47,7 +48,9 @@ cd ..
|
||||
rm -rf sdrplay
|
||||
rm $BINARY
|
||||
|
||||
git clone https://github.com/pothosware/SoapySDRPlay.git
|
||||
cmakebuild SoapySDRPlay 14ec39e4ff0dab7ae7fdf1afbbd2d28b49b0ffae
|
||||
git clone https://github.com/SDRplay/SoapySDRPlay.git
|
||||
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
|
||||
set -euxo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
@ -17,13 +18,15 @@ function cmakebuild() {
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="udev"
|
||||
STATIC_PACKAGES="libudev1"
|
||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/pothosware/SoapySDR
|
||||
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
|
||||
set -euxo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
@ -8,7 +9,7 @@ function cmakebuild() {
|
||||
fi
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
cmake ${CMAKE_ARGS:-} ..
|
||||
make
|
||||
make install
|
||||
cd ../..
|
||||
@ -17,18 +18,46 @@ function cmakebuild() {
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools alsa-lib"
|
||||
BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers"
|
||||
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="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
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
apt-get update
|
||||
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
|
||||
cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d
|
||||
|
||||
git clone https://github.com/jketterl/csdr.git
|
||||
cd csdr
|
||||
git checkout 43c36df5dcd92d3bdb322f9d53f99ca0c7c816a4
|
||||
git checkout c4d8a8a5590898e8c9e94b88b96a2fdc7cd0493a
|
||||
autoreconf -i
|
||||
./configure
|
||||
make
|
||||
make install
|
||||
cd ..
|
||||
@ -38,20 +67,33 @@ git clone https://github.com/szechyjs/mbelib.git
|
||||
cmakebuild mbelib 9a04ed5c78176a9965f3d43f7aa1b1f5330e771f
|
||||
|
||||
git clone https://github.com/jketterl/digiham.git
|
||||
cmakebuild digiham e5e11ce9611e3d8f5f9dce7dee97f86a31af107c
|
||||
cmakebuild digiham 95206501be89b38d0267bf6c29a6898e7c65656f
|
||||
|
||||
git clone https://github.com/f4exb/dsd.git
|
||||
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_TGZ=${WSJT_DIR}.tgz
|
||||
wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ
|
||||
tar xvfz $WSJT_TGZ
|
||||
cmakebuild $WSJT_DIR
|
||||
wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
|
||||
tar xfz ${WSJT_TGZ}
|
||||
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
|
||||
cd direwolf
|
||||
patch -Np1 < /direwolf-1.5.patch
|
||||
make
|
||||
make install
|
||||
cd ..
|
||||
@ -62,4 +104,6 @@ pushd /opt/aprs-symbols
|
||||
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
|
||||
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
|
||||
cp bookmarks.json /etc/openwebrx/
|
||||
fi
|
||||
if [[ ! -f /etc/openwebrx/users.json ]] ; then
|
||||
cp users.json /etc/openwebrx/
|
||||
fi
|
||||
|
||||
|
||||
_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,12 +1,7 @@
|
||||
@import url("openwebrx-header.css");
|
||||
@import url("openwebrx-globals.css");
|
||||
|
||||
/* expandable photo not implemented on features page */
|
||||
#webrx-top-photo-clip {
|
||||
max-height: 67px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 50px 0;
|
||||
}
|
||||
}
|
||||
|
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-globals.css");
|
||||
|
||||
/* expandable photo not implemented on map page */
|
||||
#webrx-top-photo-clip {
|
||||
max-height: 67px;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -2,6 +2,7 @@
|
||||
{
|
||||
position: relative;
|
||||
z-index:1000;
|
||||
background-color: #575757;
|
||||
}
|
||||
|
||||
#webrx-top-photo
|
||||
@ -13,7 +14,8 @@
|
||||
#webrx-top-photo-clip
|
||||
{
|
||||
min-height: 67px;
|
||||
max-height: 350px;
|
||||
max-height: 67px;
|
||||
height: 350px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
@ -41,18 +43,21 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#webrx-tob-container, #webrx-top-container * {
|
||||
line-height: initial;
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
#webrx-top-container img {
|
||||
vertical-align: initial;
|
||||
}
|
||||
|
||||
#webrx-top-logo
|
||||
{
|
||||
padding: 12px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#webrx-ha5kfu-top-logo
|
||||
{
|
||||
float: right;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#webrx-rx-avatar
|
||||
{
|
||||
background-color: rgba(154, 154, 154, .5);
|
||||
@ -107,46 +112,38 @@
|
||||
cursor:pointer;
|
||||
position: absolute;
|
||||
left: 470px;
|
||||
top: 51px;
|
||||
top: 55px;
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow a
|
||||
{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow-down
|
||||
{
|
||||
display:none;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons ul
|
||||
{
|
||||
display: table;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
|
||||
#openwebrx-main-buttons ul li
|
||||
{
|
||||
display: table-cell;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
#openwebrx-main-buttons .button {
|
||||
display: block;
|
||||
width: 55px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons .button img {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons li:hover
|
||||
#openwebrx-main-buttons .button:hover
|
||||
{
|
||||
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);
|
||||
}
|
||||
@ -154,6 +151,9 @@
|
||||
|
||||
#openwebrx-main-buttons
|
||||
{
|
||||
padding: 5px 15px;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
float: right;
|
||||
margin:0;
|
||||
color: white;
|
||||
|
@ -150,6 +150,10 @@ input[type=range]:focus::-ms-fill-upper
|
||||
background: #B6B6B6;
|
||||
}
|
||||
|
||||
input[type=range]:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#webrx-page-container
|
||||
{
|
||||
height: 100%;
|
||||
@ -311,7 +315,7 @@ input[type=range]:focus::-ms-fill-upper
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
#webrx-actual-freq {
|
||||
.webrx-actual-freq {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
@ -320,11 +324,11 @@ input[type=range]:focus::-ms-fill-upper
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#webrx-actual-freq > * {
|
||||
.webrx-actual-freq > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#webrx-actual-freq input {
|
||||
.webrx-actual-freq input {
|
||||
font-family: 'roboto-mono';
|
||||
width: 0;
|
||||
box-sizing: border-box;
|
||||
@ -334,14 +338,13 @@ input[type=range]:focus::-ms-fill-upper
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#webrx-actual-freq, #webrx-actual-freq input {
|
||||
.webrx-actual-freq, .webrx-actual-freq input {
|
||||
font-size: 16pt;
|
||||
font-family: 'roboto-mono';
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
#webrx-mouse-freq
|
||||
{
|
||||
.webrx-mouse-freq {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 10pt;
|
||||
@ -381,6 +384,7 @@ input[type=range]:focus::-ms-fill-upper
|
||||
border-radius: 15px;
|
||||
-moz-border-radius: 15px;
|
||||
margin: 5.9px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.openwebrx-panel a
|
||||
@ -435,6 +439,10 @@ input[type=range]:focus::-ms-fill-upper
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.openwebrx-button.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.openwebrx-demodulator-button
|
||||
{
|
||||
width: 38px;
|
||||
@ -445,6 +453,10 @@ input[type=range]:focus::-ms-fill-upper
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.openwebrx-demodulator-button.same-mod {
|
||||
color: #FFC;
|
||||
}
|
||||
|
||||
.openwebrx-square-button img
|
||||
{
|
||||
height: 27px;
|
||||
@ -723,8 +735,7 @@ img.openwebrx-mirror-img
|
||||
color: White;
|
||||
}
|
||||
|
||||
#openwebrx-secondary-demod-listbox
|
||||
{
|
||||
.openwebrx-secondary-demod-listbox {
|
||||
width: 173px;
|
||||
height: 27px;
|
||||
padding-left:3px;
|
||||
@ -923,37 +934,23 @@ img.openwebrx-mirror-img
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message,
|
||||
#openwebrx-panel-packet-message,
|
||||
#openwebrx-panel-pocsag-message
|
||||
{
|
||||
.openwebrx-message-panel {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message tbody,
|
||||
#openwebrx-panel-packet-message tbody,
|
||||
#openwebrx-panel-pocsag-message tbody
|
||||
{
|
||||
.openwebrx-message-panel tbody {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message thead tr,
|
||||
#openwebrx-panel-packet-message thead tr,
|
||||
#openwebrx-panel-pocsag-message thead tr
|
||||
{
|
||||
.openwebrx-message-panel thead tr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message th,
|
||||
#openwebrx-panel-wsjt-message td,
|
||||
#openwebrx-panel-packet-message th,
|
||||
#openwebrx-panel-packet-message td,
|
||||
#openwebrx-panel-pocsag-message th,
|
||||
#openwebrx-panel-pocsag-message td
|
||||
{
|
||||
.openwebrx-message-panel th,
|
||||
.openwebrx-message-panel td {
|
||||
width: 50px;
|
||||
text-align: left;
|
||||
padding: 1px 3px;
|
||||
@ -972,6 +969,31 @@ img.openwebrx-mirror-img
|
||||
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 {
|
||||
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="packet"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel,
|
||||
#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;
|
||||
}
|
||||
@ -1095,7 +1119,8 @@ img.openwebrx-mirror-img
|
||||
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container,
|
||||
#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;
|
||||
margin: -10px;
|
||||
|
@ -1,10 +1,12 @@
|
||||
<HTML><HEAD>
|
||||
<TITLE>OpenWebRX Feature report</TITLE>
|
||||
<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">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script>
|
||||
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
||||
<script src="static/lib/Header.js"></script>
|
||||
<script src="static/features.js"></script>
|
||||
</HEAD><BODY>
|
||||
${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-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">
|
||||
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
|
||||
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
|
||||
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png"/>
|
||||
<a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a>
|
||||
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
|
||||
<div id="webrx-rx-texts">
|
||||
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
|
||||
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
|
||||
</div>
|
||||
<div id="openwebrx-rx-details-arrow">
|
||||
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger"><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>
|
||||
</div>
|
||||
<section id="openwebrx-main-buttons">
|
||||
<ul>
|
||||
<li data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" /><br/>Status</li>
|
||||
<li data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" /><br/>Log</li>
|
||||
<li data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li>
|
||||
<li><a href="map" target="_blank"><img src="static/gfx/openwebrx-panel-map.png" /><br/>Map</a></li>
|
||||
</ul>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" alt="Status"/><br/>Status</div>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" alt="Log"/><br/>Log</div>
|
||||
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" alt="Receiver"/><br/>Receiver</div>
|
||||
<a class="button" href="map" target="openwebrx-map"><img src="static/gfx/openwebrx-panel-map.png" alt="Map"/><br/>Map</a>
|
||||
${settingslink}
|
||||
</section>
|
||||
</div>
|
||||
<div id="webrx-rx-photo-title"></div>
|
||||
|
@ -24,14 +24,7 @@
|
||||
<head>
|
||||
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||
<script src="static/openwebrx.js"></script>
|
||||
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
||||
<script src="static/lib/jquery.nanoscroller.js"></script>
|
||||
<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>
|
||||
<script src="compiled/receiver.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
|
||||
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
|
||||
<meta charset="utf-8">
|
||||
@ -52,10 +45,6 @@
|
||||
</div>
|
||||
<div id="openwebrx-panels-container">
|
||||
<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 id="openwebrx-digimode-canvas-container">
|
||||
<div id="openwebrx-digimode-select-channel"></div>
|
||||
@ -67,7 +56,7 @@
|
||||
</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>
|
||||
<th>UTC</th>
|
||||
<th class="decimal">dB</th>
|
||||
@ -77,7 +66,15 @@
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</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>
|
||||
<th>UTC</th>
|
||||
<th class="callsign">Callsign</th>
|
||||
@ -86,7 +83,7 @@
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</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>
|
||||
<th class="address">Address</th>
|
||||
<th class="message">Message</th>
|
||||
@ -126,26 +123,30 @@
|
||||
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
|
||||
<div class="nano-content">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<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-output"> <span class="openwebrx-progressbar-text">Audio output [0 sps]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed"> <span class="openwebrx-progressbar-text">Audio stream [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed"> <span class="openwebrx-progressbar-text">Network usage [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer" data-type="audiobuffer"></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output" data-type="audiooutput"></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed" data-type="audiospeed"></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed" data-type="networkspeed"></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu" data-type="cpu"></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-clients" data-type="clients"></div>
|
||||
</div>
|
||||
</div>
|
||||
<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-line frequencies-container">
|
||||
<div class="frequencies">
|
||||
<div id="webrx-actual-freq"></div>
|
||||
<div id="webrx-mouse-freq"></div>
|
||||
<div class="webrx-actual-freq"></div>
|
||||
<div class="webrx-mouse-freq"></div>
|
||||
</div>
|
||||
<div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark...">
|
||||
<img src="static/gfx/openwebrx-bookmark.png">
|
||||
@ -155,47 +156,7 @@
|
||||
<select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();">
|
||||
</select>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
|
||||
onclick="demodulator_analog_replace('nfm');">FM</div>
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-am"
|
||||
onclick="demodulator_analog_replace('am');">AM</div>
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-lsb"
|
||||
onclick="demodulator_analog_replace('lsb');">LSB</div>
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-usb"
|
||||
onclick="demodulator_analog_replace('usb');">USB</div>
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
|
||||
onclick="demodulator_analog_replace('cw');">CW</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line 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-modes openwebrx-panel-line"></div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
|
||||
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
|
||||
@ -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()">
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
|
||||
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()">
|
||||
<div title="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" 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>
|
||||
<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>
|
||||
@ -249,17 +210,7 @@
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="modulation">Modulation:</label>
|
||||
<select name="modulation" id="modulation">
|
||||
<option value="nfm">FM</option>
|
||||
<option value="am">AM</option>
|
||||
<option value="usb">USB</option>
|
||||
<option value="lsb">LSB</option>
|
||||
<option value="cw">CW</option>
|
||||
<option value="dmr">DMR</option>
|
||||
<option value="dstar">D-Star</option>
|
||||
<option value="nxdn">NXDN</option>
|
||||
<option value="ysf">YSF</option>
|
||||
</select>
|
||||
<select name="modulation" id="modulation"></select>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div class="openwebrx-button" data-action="cancel">Cancel</div>
|
||||
|
@ -8,12 +8,10 @@ function BookmarkBar() {
|
||||
var $bookmark = $(e.target).closest('.bookmark');
|
||||
me.$container.find('.bookmark').removeClass('selected');
|
||||
var b = $bookmark.data();
|
||||
if (!b || !b.frequency || (!b.modulation && !b.digital_modulation)) return;
|
||||
demodulators[0].set_offset_frequency(b.frequency - center_freq);
|
||||
if (!b || !b.frequency || !b.modulation) return;
|
||||
me.getDemodulator().set_offset_frequency(b.frequency - center_freq);
|
||||
if (b.modulation) {
|
||||
demodulator_analog_replace(b.modulation);
|
||||
} else if (b.digital_modulation) {
|
||||
demodulator_digital_replace(b.digital_modulation);
|
||||
me.getDemodulatorPanel().setMode(b.modulation);
|
||||
}
|
||||
$bookmark.addClass('selected');
|
||||
});
|
||||
@ -104,40 +102,26 @@ BookmarkBar.prototype.render = function(){
|
||||
};
|
||||
|
||||
BookmarkBar.prototype.showEditDialog = function(bookmark) {
|
||||
var $form = this.$dialog.find("form");
|
||||
if (!bookmark) {
|
||||
bookmark = {
|
||||
name: "",
|
||||
frequency: center_freq + demodulators[0].offset_frequency,
|
||||
modulation: demodulators[0].subtype
|
||||
frequency: center_freq + this.getDemodulator().get_offset_frequency(),
|
||||
modulation: this.getDemodulator().get_secondary_demod() || this.getDemodulator().get_modulation()
|
||||
}
|
||||
}
|
||||
['name', 'frequency', 'modulation'].forEach(function(key){
|
||||
$form.find('#' + key).val(bookmark[key]);
|
||||
});
|
||||
this.$dialog.data('id', bookmark.id);
|
||||
this.$dialog.bookmarkDialog().setValues(bookmark);
|
||||
this.$dialog.show();
|
||||
this.$dialog.find('#name').focus();
|
||||
};
|
||||
|
||||
BookmarkBar.prototype.storeBookmark = function() {
|
||||
var me = this;
|
||||
var bookmark = {};
|
||||
var valid = true;
|
||||
['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;
|
||||
}
|
||||
var bookmark = this.$dialog.bookmarkDialog().getValues();
|
||||
if (!bookmark) return;
|
||||
bookmark.frequency = Number(bookmark.frequency);
|
||||
|
||||
var bookmarks = me.localBookmarks.getBookmarks();
|
||||
|
||||
bookmark.id = me.$dialog.data('id');
|
||||
if (!bookmark.id) {
|
||||
if (bookmarks.length) {
|
||||
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();
|
||||
};
|
||||
|
||||
BookmarkBar.prototype.getDemodulatorPanel = function() {
|
||||
return $('#openwebrx-panel-receiver').demodulatorPanel();
|
||||
};
|
||||
|
||||
BookmarkBar.prototype.getDemodulator = function() {
|
||||
return this.getDemodulatorPanel().getDemodulator();
|
||||
};
|
||||
|
||||
BookmarkLocalStorage = function(){
|
||||
};
|
||||
|
||||
@ -171,7 +163,3 @@ BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
|
||||
bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
|
||||
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() {
|
||||
var me = this;
|
||||
me.listeners = [];
|
||||
|
||||
me.element.on('wheel', function(e){
|
||||
e.preventDefault();
|
||||
@ -63,17 +62,13 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
||||
if (e.originalEvent.deltaY > 0) delta *= -1;
|
||||
var newFrequency = me.frequency + delta;
|
||||
|
||||
me.listeners.forEach(function(l) {
|
||||
l(newFrequency);
|
||||
});
|
||||
me.element.trigger('frequencychange', newFrequency);
|
||||
});
|
||||
|
||||
var submit = function(){
|
||||
var freq = parseInt(me.input.val());
|
||||
if (!isNaN(freq)) {
|
||||
me.listeners.forEach(function(l) {
|
||||
l(freq);
|
||||
});
|
||||
me.element.trigger('frequencychange', freq);
|
||||
}
|
||||
me.input.hide();
|
||||
me.displayContainer.show();
|
||||
@ -96,6 +91,16 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
||||
});
|
||||
};
|
||||
|
||||
TuneableFrequencyDisplay.prototype.onFrequencyChange = function(listener){
|
||||
this.listeners.push(listener);
|
||||
};
|
||||
$.fn.frequencyDisplay = function() {
|
||||
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() {
|
||||
this.reporters = [];
|
||||
this.reset();
|
||||
}
|
||||
|
||||
@ -21,10 +22,13 @@ Measurement.prototype.getRate = function() {
|
||||
Measurement.prototype.reset = function() {
|
||||
this.value = 0;
|
||||
this.start = new Date();
|
||||
this.reporters.forEach(function(r){ r.reset(); });
|
||||
};
|
||||
|
||||
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) {
|
||||
@ -59,4 +63,8 @@ Reporter.prototype.report = function(){
|
||||
var accumulated = newest.value - oldest.value;
|
||||
// we want rate per second, but our time is in milliseconds... compensate by 1000
|
||||
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) {
|
||||
this.$el = $(el);
|
||||
this.$innerText = this.$el.find('.openwebrx-progressbar-text');
|
||||
this.$innerBar = this.$el.find('.openwebrx-progressbar-bar');
|
||||
this.$innerText = $('<span class="openwebrx-progressbar-text">' + this.getDefaultText() + '</span>');
|
||||
this.$innerBar = $('<div class="openwebrx-progressbar-bar"></div>');
|
||||
this.$el.empty().append(this.$innerText, this.$innerBar);
|
||||
this.$innerBar.css('width', '0%');
|
||||
};
|
||||
|
||||
ProgressBar.prototype.getDefaultText = function() {
|
||||
return '';
|
||||
}
|
||||
|
||||
ProgressBar.prototype.set = function(val, text, over) {
|
||||
this.setValue(val);
|
||||
this.setText(text);
|
||||
@ -25,13 +30,20 @@ ProgressBar.prototype.setOver = function(over) {
|
||||
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
|
||||
};
|
||||
|
||||
AudioBufferProgressBar = function(el, sampleRate) {
|
||||
AudioBufferProgressBar = function(el) {
|
||||
ProgressBar.call(this, el);
|
||||
this.sampleRate = sampleRate;
|
||||
};
|
||||
|
||||
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) {
|
||||
var audio_buffer_value = buffersize / this.sampleRate;
|
||||
var overrun = audio_buffer_value > audio_buffer_maximal_length_sec;
|
||||
@ -53,6 +65,10 @@ NetworkSpeedProgressBar = function(el) {
|
||||
|
||||
NetworkSpeedProgressBar.prototype = new ProgressBar();
|
||||
|
||||
NetworkSpeedProgressBar.prototype.getDefaultText = function() {
|
||||
return 'Network usage [0 kbps]';
|
||||
};
|
||||
|
||||
NetworkSpeedProgressBar.prototype.setSpeed = function(speed) {
|
||||
var speedInKilobits = speed * 8 / 1000;
|
||||
this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false);
|
||||
@ -64,18 +80,29 @@ AudioSpeedProgressBar = function(el) {
|
||||
|
||||
AudioSpeedProgressBar.prototype = new ProgressBar();
|
||||
|
||||
AudioSpeedProgressBar.prototype.getDefaultText = function() {
|
||||
return 'Audio stream [0 kbps]';
|
||||
};
|
||||
|
||||
AudioSpeedProgressBar.prototype.setSpeed = function(speed) {
|
||||
this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false);
|
||||
};
|
||||
|
||||
AudioOutputProgressBar = function(el, sampleRate) {
|
||||
ProgressBar.call(this, el);
|
||||
this.maxRate = sampleRate * 1.25;
|
||||
this.minRate = sampleRate * .25;
|
||||
};
|
||||
|
||||
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) {
|
||||
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.getDefaultText = function() {
|
||||
return 'Clients [1]';
|
||||
};
|
||||
|
||||
ClientsProgressBar.prototype.setClients = function(clients) {
|
||||
this.clients = clients;
|
||||
this.render();
|
||||
@ -108,6 +139,27 @@ CpuProgressBar = function(el) {
|
||||
|
||||
CpuProgressBar.prototype = new ProgressBar();
|
||||
|
||||
CpuProgressBar.prototype.getDefaultText = function() {
|
||||
return 'Server CPU [0%]';
|
||||
};
|
||||
|
||||
CpuProgressBar.prototype.setUsage = function(usage) {
|
||||
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>
|
||||
<title>OpenWebRX Map</title>
|
||||
<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="static/lib/chroma.min.js"></script>
|
||||
<script src="static/map.js"></script>
|
||||
<script src="compiled/map.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="static/css/map.css" />
|
||||
<meta charset="utf-8">
|
||||
|
@ -135,7 +135,11 @@
|
||||
if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
|
||||
map.panTo(pos);
|
||||
showMarkerInfoWindow(update.callsign, pos);
|
||||
delete(expectedCallsign);
|
||||
expectedCallsign = false;
|
||||
}
|
||||
|
||||
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign.trim()) {
|
||||
showMarkerInfoWindow(infowindow.callsign, pos);
|
||||
}
|
||||
break;
|
||||
case 'locator':
|
||||
@ -176,7 +180,11 @@
|
||||
if (expectedLocator && expectedLocator == update.location.locator) {
|
||||
map.panTo(center);
|
||||
showLocatorInfoWindow(expectedLocator, center);
|
||||
delete(expectedLocator);
|
||||
expectedLocator = false;
|
||||
}
|
||||
|
||||
if (infowindow && infowindow.locator && infowindow.locator == update.location.locator) {
|
||||
showLocatorInfoWindow(infowindow.locator, center);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -215,13 +223,26 @@
|
||||
case "config":
|
||||
var config = json.value;
|
||||
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], {
|
||||
center: {
|
||||
lat: config.receiver_gps[0],
|
||||
lng: config.receiver_gps[1]
|
||||
lat: config.receiver_gps.lat,
|
||||
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(){
|
||||
nite.init(map);
|
||||
setInterval(function() { nite.refresh() }, 10000); // every 10s
|
||||
@ -237,6 +258,11 @@
|
||||
case "update":
|
||||
processUpdates(json.value);
|
||||
break;
|
||||
case 'receiver_details':
|
||||
$('#webrx-top-container').header().setDetails(json['value']);
|
||||
break;
|
||||
default:
|
||||
console.warn('received message of unknown type: ' + json['type']);
|
||||
}
|
||||
} catch (e) {
|
||||
// don't lose exception
|
||||
@ -269,9 +295,21 @@
|
||||
|
||||
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 showLocatorInfoWindow = function(locator, pos) {
|
||||
if (!infowindow) infowindow = new google.maps.InfoWindow();
|
||||
var infowindow = getInfoWindow();
|
||||
infowindow.locator = locator;
|
||||
var inLocator = $.map(rectangles, function(r, callsign) {
|
||||
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
|
||||
}).filter(function(d) {
|
||||
@ -297,7 +335,8 @@
|
||||
};
|
||||
|
||||
var showMarkerInfoWindow = function(callsign, pos) {
|
||||
if (!infowindow) infowindow = new google.maps.InfoWindow();
|
||||
var infowindow = getInfoWindow();
|
||||
infowindow.callsign = callsign;
|
||||
var marker = markers[callsign];
|
||||
var timestring = moment(marker.lastseen).fromNow();
|
||||
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
|
||||
|
||||
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):
|
||||
@ -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>
|
||||
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()
|
||||
if not featureDetector.is_available("core"):
|
||||
print(
|
||||
logger.error(
|
||||
"you are missing required dependencies to run openwebrx. "
|
||||
"please check that the following core requirements are installed:"
|
||||
)
|
||||
print(", ".join(featureDetector.get_requirements("core")))
|
||||
logger.error(", ".join(featureDetector.get_requirements("core")))
|
||||
return
|
||||
|
||||
# Get error messages about unknown / unavailable features as soon as possible
|
||||
SdrService.loadProps()
|
||||
|
||||
if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
|
||||
updater = SdrHuUpdater()
|
||||
updater.start()
|
||||
|
||||
Services.start()
|
||||
|
||||
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()
|
||||
except KeyboardInterrupt:
|
||||
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
|
||||
import threading
|
||||
|
||||
@ -33,7 +33,7 @@ class ClientRegistry(object):
|
||||
c.write_clients(n)
|
||||
|
||||
def addClient(self, client):
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
pm = Config.get()
|
||||
if len(self.clients) >= pm["max_clients"]:
|
||||
raise TooManyClientsException()
|
||||
self.clients.append(client)
|
||||
|
@ -33,6 +33,9 @@ class CommandMapper(object):
|
||||
self.static = static
|
||||
return self
|
||||
|
||||
def keys(self):
|
||||
return self.mappings.keys()
|
||||
|
||||
|
||||
class CommandMapping(ABC):
|
||||
@abstractmethod
|
||||
|
247
owrx/config.py
247
owrx/config.py
@ -1,149 +1,134 @@
|
||||
from owrx.property import PropertyManager, PropertyLayer
|
||||
import importlib.util
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
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):
|
||||
pass
|
||||
|
||||
|
||||
class PropertyManager(object):
|
||||
sharedInstance = None
|
||||
class ConfigError(object):
|
||||
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
|
||||
def getSharedInstance():
|
||||
if PropertyManager.sharedInstance is None:
|
||||
PropertyManager.sharedInstance = PropertyManager()
|
||||
return PropertyManager.sharedInstance
|
||||
def _loadPythonFile(file):
|
||||
spec = importlib.util.spec_from_file_location("config_webrx", file)
|
||||
cfg = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(cfg)
|
||||
pm = PropertyLayer()
|
||||
for name, value in cfg.__dict__.items():
|
||||
if name.startswith("__"):
|
||||
continue
|
||||
pm[name] = value
|
||||
return pm
|
||||
|
||||
def collect(self, *props):
|
||||
return PropertyManager(
|
||||
{name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}
|
||||
)
|
||||
@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
|
||||
|
||||
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"]:
|
||||
@staticmethod
|
||||
def _loadConfig():
|
||||
for file in ["./settings.json", "/etc/openwebrx/config_webrx.py", "./config_webrx.py"]:
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("config_webrx", file)
|
||||
cfg = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(cfg)
|
||||
for name, value in cfg.__dict__.items():
|
||||
if name.startswith("__"):
|
||||
continue
|
||||
self[name] = value
|
||||
return self
|
||||
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:
|
||||
pass
|
||||
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.cpu import CpuUsageThread
|
||||
from owrx.sdr import SdrService
|
||||
@ -9,9 +10,12 @@ from owrx.version import openwebrx_version
|
||||
from owrx.bands import Bandplan
|
||||
from owrx.bookmarks import Bookmarks
|
||||
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 queue import Full
|
||||
from js8py import Js8Frame
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
import json
|
||||
import threading
|
||||
|
||||
@ -20,7 +24,7 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Client(object):
|
||||
class Client(ABC):
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.multiprocessingPipe = Queue(100)
|
||||
@ -49,6 +53,7 @@ class Client(object):
|
||||
except Full:
|
||||
self.close()
|
||||
|
||||
@abstractmethod
|
||||
def handleTextMessage(self, conn, message):
|
||||
pass
|
||||
|
||||
@ -59,7 +64,25 @@ class Client(object):
|
||||
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 = [
|
||||
"waterfall_colors",
|
||||
"waterfall_min_level",
|
||||
@ -67,7 +90,6 @@ class OpenWebRxReceiverClient(Client):
|
||||
"waterfall_auto_level_margin",
|
||||
"samp_rate",
|
||||
"fft_size",
|
||||
"fft_fps",
|
||||
"audio_compression",
|
||||
"fft_compression",
|
||||
"max_clients",
|
||||
@ -93,28 +115,16 @@ class OpenWebRxReceiverClient(Client):
|
||||
self.close()
|
||||
raise
|
||||
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
|
||||
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()
|
||||
self.write_features(features)
|
||||
|
||||
modes = Modes.getModes()
|
||||
self.write_modes(modes)
|
||||
|
||||
self.__sendProfiles()
|
||||
|
||||
CpuUsageThread.getSharedInstance().add_client(self)
|
||||
|
||||
def __sendProfiles(self):
|
||||
@ -134,8 +144,12 @@ class OpenWebRxReceiverClient(Client):
|
||||
self.startDsp()
|
||||
|
||||
if "params" in message:
|
||||
params = message["params"]
|
||||
self.setDspProperties(params)
|
||||
dsp = self.getDsp()
|
||||
if dsp is None:
|
||||
logger.warning("DSP not available; discarding client data")
|
||||
else:
|
||||
params = message["params"]
|
||||
dsp.setProperties(params)
|
||||
|
||||
elif message["type"] == "config":
|
||||
if "params" in message:
|
||||
@ -152,7 +166,7 @@ class OpenWebRxReceiverClient(Client):
|
||||
if "params" in message:
|
||||
self.connectionProperties = message["params"]
|
||||
if self.dsp:
|
||||
self.setDspProperties(self.connectionProperties)
|
||||
self.getDsp().setProperties(self.connectionProperties)
|
||||
|
||||
else:
|
||||
logger.warning("received message without type: {0}".format(message))
|
||||
@ -169,6 +183,7 @@ class OpenWebRxReceiverClient(Client):
|
||||
next = SdrService.getFirstSource()
|
||||
if next is None:
|
||||
# exit condition: no sdrs available
|
||||
logger.warning("no more SDR devices available")
|
||||
self.handleNoSdrsAvailable()
|
||||
return
|
||||
|
||||
@ -184,25 +199,25 @@ class OpenWebRxReceiverClient(Client):
|
||||
|
||||
self.sdr = next
|
||||
|
||||
self.startDsp()
|
||||
self.getDsp()
|
||||
|
||||
# keep trying until we find a suitable SDR
|
||||
if self.sdr.getState() == SdrSource.STATE_FAILED:
|
||||
self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
|
||||
else:
|
||||
# found a working sdr, exit the loop
|
||||
if self.sdr.getState() != SdrSource.STATE_FAILED:
|
||||
break
|
||||
|
||||
# send initial config
|
||||
self.setDspProperties(self.connectionProperties)
|
||||
logger.warning('SDR device "%s" has failed, selecing new device', self.sdr.getName())
|
||||
self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
|
||||
|
||||
configProps = (
|
||||
self.sdr.getProps()
|
||||
.collect(*OpenWebRxReceiverClient.config_keys)
|
||||
.defaults(PropertyManager.getSharedInstance())
|
||||
)
|
||||
# send initial config
|
||||
self.getDsp().setProperties(self.connectionProperties)
|
||||
|
||||
stack = PropertyStack()
|
||||
stack.addLayer(0, self.sdr.getProps())
|
||||
stack.addLayer(1, Config.get())
|
||||
configProps = stack.filter(*OpenWebRxReceiverClient.config_keys)
|
||||
|
||||
def sendConfig(key, value):
|
||||
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
|
||||
config = configProps.__dict__()
|
||||
# TODO mathematical properties? hmmmm
|
||||
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
|
||||
# TODO this is a hack to support multiple sdrs
|
||||
@ -226,9 +241,7 @@ class OpenWebRxReceiverClient(Client):
|
||||
self.write_sdr_error("No SDR Devices available")
|
||||
|
||||
def startDsp(self):
|
||||
if self.dsp is None and self.sdr is not None:
|
||||
self.dsp = DspManager(self, self.sdr)
|
||||
self.dsp.start()
|
||||
self.getDsp().start()
|
||||
|
||||
def close(self):
|
||||
self.stopDsp()
|
||||
@ -247,18 +260,28 @@ class OpenWebRxReceiverClient(Client):
|
||||
self.sdr.removeSpectrumClient(self)
|
||||
|
||||
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
|
||||
protected = (
|
||||
self.sdr.getProps()
|
||||
.collect("samp_rate", "center_freq", "rf_gain", "type")
|
||||
.defaults(PropertyManager.getSharedInstance())
|
||||
)
|
||||
stack = PropertyStack()
|
||||
stack.addLayer(0, self.sdr.getProps())
|
||||
stack.addLayer(1, config)
|
||||
protected = stack.filter(*keys)
|
||||
for key, value in params.items():
|
||||
protected[key] = value
|
||||
try:
|
||||
protected[key] = value
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def setDspProperties(self, params):
|
||||
for key, value in params.items():
|
||||
self.dsp.setProperty(key, value)
|
||||
def getDsp(self):
|
||||
if self.dsp is None and self.sdr is not None:
|
||||
self.dsp = DspManager(self, self.sdr)
|
||||
return self.dsp
|
||||
|
||||
def write_spectrum_data(self, data):
|
||||
self.mp_send(bytes([0x01]) + data)
|
||||
@ -288,9 +311,6 @@ class OpenWebRxReceiverClient(Client):
|
||||
def write_config(self, cfg):
|
||||
self.send({"type": "config", "value": cfg})
|
||||
|
||||
def write_receiver_details(self, details):
|
||||
self.send({"type": "receiver_details", "value": details})
|
||||
|
||||
def write_profiles(self, profiles):
|
||||
self.send({"type": "profiles", "value": profiles})
|
||||
|
||||
@ -324,13 +344,44 @@ class OpenWebRxReceiverClient(Client):
|
||||
def write_backoff_message(self, 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):
|
||||
super().__init__(conn)
|
||||
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
self.write_config(pm.collect("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__())
|
||||
pm = Config.get()
|
||||
self.write_config(pm.filter("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__())
|
||||
|
||||
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()
|
44
owrx/controllers/__init__.py
Normal file
44
owrx/controllers/__init__.py
Normal file
@ -0,0 +1,44 @@
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, handler, request, options):
|
||||
self.handler = handler
|
||||
self.request = request
|
||||
self.options = options
|
||||
|
||||
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None):
|
||||
self.handler.send_response(code)
|
||||
if headers is None:
|
||||
headers = {}
|
||||
if content_type is not None:
|
||||
headers["Content-Type"] = content_type
|
||||
if last_modified is not None:
|
||||
headers["Last-Modified"] = last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||
if max_age is not None:
|
||||
headers["Cache-Control"] = "max-age: {0}".format(max_age)
|
||||
for key, value in headers.items():
|
||||
self.handler.send_header(key, value)
|
||||
self.handler.end_headers()
|
||||
if type(content) == str:
|
||||
content = content.encode()
|
||||
self.handler.wfile.write(content)
|
||||
|
||||
def send_redirect(self, location, code=303, cookies=None):
|
||||
self.handler.send_response(code)
|
||||
if cookies is not None:
|
||||
self.handler.send_header("Set-Cookie", cookies.output(header=''))
|
||||
self.handler.send_header("Location", location)
|
||||
self.handler.end_headers()
|
||||
|
||||
def get_body(self):
|
||||
if "Content-Length" not in self.handler.headers:
|
||||
return None
|
||||
length = int(self.handler.headers["Content-Length"])
|
||||
return self.handler.rfile.read(length)
|
||||
|
||||
def handle_request(self):
|
||||
action = "indexAction"
|
||||
if "action" in self.options:
|
||||
action = self.options["action"]
|
||||
getattr(self, action)()
|
33
owrx/controllers/admin.py
Normal file
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")
|
50
owrx/controllers/status.py
Normal file
50
owrx/controllers/status.py
Normal file
@ -0,0 +1,50 @@
|
||||
from . import Controller
|
||||
from owrx.version import openwebrx_version
|
||||
from owrx.sdr import SdrService
|
||||
from owrx.config import Config
|
||||
from owrx.receiverid import ReceiverId, KeyException
|
||||
import json
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StatusController(Controller):
|
||||
def getProfileStats(self, profile):
|
||||
return {
|
||||
"name": profile["name"],
|
||||
"center_freq": profile["center_freq"],
|
||||
"sample_rate": profile["samp_rate"],
|
||||
}
|
||||
|
||||
def getReceiverStats(self, receiver):
|
||||
stats = {
|
||||
"name": receiver.getName(),
|
||||
# TODO would be better to have types from the config here
|
||||
"type": type(receiver).__name__,
|
||||
"profiles": [self.getProfileStats(p) for p in receiver.getProfiles().values()]
|
||||
}
|
||||
return stats
|
||||
|
||||
def indexAction(self):
|
||||
pm = Config.get()
|
||||
headers = None
|
||||
if "Authorization" in self.request.headers:
|
||||
try:
|
||||
headers = ReceiverId.getResponseHeader(self.request.headers["Authorization"])
|
||||
except KeyException:
|
||||
logger.exception("error processing authorization header")
|
||||
status = {
|
||||
"receiver": {
|
||||
"name": pm["receiver_name"],
|
||||
"admin": pm["receiver_admin"],
|
||||
"gps": pm["receiver_gps"],
|
||||
"asl": pm["receiver_asl"],
|
||||
"location": pm["receiver_location"],
|
||||
},
|
||||
"max_clients": pm["max_clients"],
|
||||
"version": openwebrx_version,
|
||||
"sdrs": [self.getReceiverStats(r) for r in SdrService.getSources().values()]
|
||||
}
|
||||
self.send_response(json.dumps(status), content_type="application/json", headers=headers)
|
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
|
128
owrx/dsp.py
128
owrx/dsp.py
@ -1,9 +1,11 @@
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.meta import MetaParser
|
||||
from owrx.wsjt import WsjtParser
|
||||
from owrx.js8 import Js8Parser
|
||||
from owrx.aprs import AprsParser
|
||||
from owrx.pocsag import PocsagParser
|
||||
from owrx.source import SdrSource
|
||||
from owrx.property import PropertyStack, PropertyLayer
|
||||
from owrx.modes import Modes
|
||||
from csdr import csdr
|
||||
import threading
|
||||
|
||||
@ -21,26 +23,38 @@ class DspManager(csdr.output):
|
||||
"wsjt_demod": WsjtParser(self.handler),
|
||||
"packet_demod": AprsParser(self.handler),
|
||||
"pocsag_demod": PocsagParser(self.handler),
|
||||
"js8_demod": Js8Parser(self.handler),
|
||||
}
|
||||
|
||||
self.localProps = (
|
||||
self.sdrSource.getProps()
|
||||
.collect(
|
||||
"audio_compression",
|
||||
"fft_compression",
|
||||
"digimodes_fft_size",
|
||||
"csdr_dynamic_bufsize",
|
||||
"csdr_print_bufsizes",
|
||||
"csdr_through",
|
||||
"digimodes_enable",
|
||||
"samp_rate",
|
||||
"digital_voice_unvoiced_quality",
|
||||
"dmr_filter",
|
||||
"temporary_directory",
|
||||
"center_freq",
|
||||
)
|
||||
.defaults(PropertyManager.getSharedInstance())
|
||||
)
|
||||
self.props = PropertyStack()
|
||||
# local demodulator properties not forwarded to the sdr
|
||||
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",
|
||||
"fft_compression",
|
||||
"digimodes_fft_size",
|
||||
"csdr_dynamic_bufsize",
|
||||
"csdr_print_bufsizes",
|
||||
"csdr_through",
|
||||
"digimodes_enable",
|
||||
"samp_rate",
|
||||
"digital_voice_unvoiced_quality",
|
||||
"temporary_directory",
|
||||
"center_freq",
|
||||
"start_mod",
|
||||
"start_freq",
|
||||
))
|
||||
|
||||
self.dsp = csdr.dsp(self)
|
||||
self.dsp.nc_port = self.sdrSource.getPort()
|
||||
@ -56,34 +70,47 @@ class DspManager(csdr.output):
|
||||
self.dsp.set_bpf(*bpf)
|
||||
|
||||
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():
|
||||
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.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression),
|
||||
self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression),
|
||||
self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size),
|
||||
self.localProps.getProperty("samp_rate").wire(self.dsp.set_samp_rate),
|
||||
self.localProps.getProperty("output_rate").wire(self.dsp.set_output_rate),
|
||||
self.localProps.getProperty("offset_freq").wire(self.dsp.set_offset_freq),
|
||||
self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level),
|
||||
self.localProps.getProperty("low_cut").wire(set_low_cut),
|
||||
self.localProps.getProperty("high_cut").wire(set_high_cut),
|
||||
self.localProps.getProperty("mod").wire(self.dsp.set_demodulator),
|
||||
self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality),
|
||||
self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter),
|
||||
self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory),
|
||||
self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq),
|
||||
self.props.wireProperty("audio_compression", self.dsp.set_audio_compression),
|
||||
self.props.wireProperty("fft_compression", self.dsp.set_fft_compression),
|
||||
self.props.wireProperty("digimodes_fft_size", self.dsp.set_secondary_fft_size),
|
||||
self.props.wireProperty("samp_rate", self.dsp.set_samp_rate),
|
||||
self.props.wireProperty("output_rate", self.dsp.set_output_rate),
|
||||
self.props.wireProperty("offset_freq", self.dsp.set_offset_freq),
|
||||
self.props.wireProperty("center_freq", self.dsp.set_center_freq),
|
||||
self.props.wireProperty("squelch_level", self.dsp.set_squelch_level),
|
||||
self.props.wireProperty("low_cut", set_low_cut),
|
||||
self.props.wireProperty("high_cut", set_high_cut),
|
||||
self.props.wireProperty("mod", self.dsp.set_demodulator),
|
||||
self.props.wireProperty("digital_voice_unvoiced_quality", self.dsp.set_unvoiced_quality),
|
||||
self.props.wireProperty("dmr_filter", self.dsp.set_dmr_filter),
|
||||
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.set_bpf(-4000, 4000)
|
||||
self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"]
|
||||
self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"]
|
||||
self.dsp.csdr_through = self.localProps["csdr_through"]
|
||||
self.dsp.csdr_dynamic_bufsize = self.props["csdr_dynamic_bufsize"]
|
||||
self.dsp.csdr_print_bufsizes = self.props["csdr_print_bufsizes"]
|
||||
self.dsp.csdr_through = self.props["csdr_through"]
|
||||
|
||||
if self.localProps["digimodes_enable"]:
|
||||
if self.props["digimodes_enable"]:
|
||||
|
||||
def set_secondary_mod(mod):
|
||||
if mod == False:
|
||||
@ -92,17 +119,19 @@ class DspManager(csdr.output):
|
||||
if mod is not None:
|
||||
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(),
|
||||
"secondary_bw": self.dsp.secondary_bw(),
|
||||
}
|
||||
)
|
||||
|
||||
self.subscriptions += [
|
||||
self.localProps.getProperty("secondary_mod").wire(set_secondary_mod),
|
||||
self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq),
|
||||
self.props.wireProperty("secondary_mod", set_secondary_mod),
|
||||
self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq),
|
||||
]
|
||||
|
||||
self.startOnAvailable = False
|
||||
|
||||
self.sdrSource.addClient(self)
|
||||
|
||||
super().__init__()
|
||||
@ -110,6 +139,8 @@ class DspManager(csdr.output):
|
||||
def start(self):
|
||||
if self.sdrSource.isAvailable():
|
||||
self.dsp.start()
|
||||
else:
|
||||
self.startOnAvailable = True
|
||||
|
||||
def receive_output(self, t, read_fn):
|
||||
logger.debug("adding new output of type %s", t)
|
||||
@ -128,13 +159,18 @@ class DspManager(csdr.output):
|
||||
|
||||
def stop(self):
|
||||
self.dsp.stop()
|
||||
self.startOnAvailable = False
|
||||
self.sdrSource.removeClient(self)
|
||||
for sub in self.subscriptions:
|
||||
sub.cancel()
|
||||
self.subscriptions = []
|
||||
|
||||
def setProperties(self, props):
|
||||
for k, v in props.items():
|
||||
self.setProperty(k, v)
|
||||
|
||||
def setProperty(self, prop, value):
|
||||
self.localProps.getProperty(prop).setValue(value)
|
||||
self.props[prop] = value
|
||||
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_USER
|
||||
@ -142,7 +178,9 @@ class DspManager(csdr.output):
|
||||
def onStateChange(self, state):
|
||||
if state == SdrSource.STATE_RUNNING:
|
||||
logger.debug("received STATE_RUNNING, attempting DspSource restart")
|
||||
self.dsp.start()
|
||||
if self.startOnAvailable:
|
||||
self.dsp.start()
|
||||
self.startOnAvailable = False
|
||||
elif state == SdrSource.STATE_STOPPING:
|
||||
logger.debug("received STATE_STOPPING, shutting down DspSource")
|
||||
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